Line data Source code
1 : /**
2 : * Defines a construction plan, ie a building.
3 : * We'll try to fing a good position if non has been provided
4 : */
5 :
6 0 : PETRA.ConstructionPlan = function(gameState, type, metadata, position)
7 : {
8 0 : if (!PETRA.QueuePlan.call(this, gameState, type, metadata))
9 0 : return false;
10 :
11 0 : this.position = position ? position : 0;
12 :
13 0 : this.category = "building";
14 :
15 0 : return true;
16 : };
17 :
18 0 : PETRA.ConstructionPlan.prototype = Object.create(PETRA.QueuePlan.prototype);
19 :
20 0 : PETRA.ConstructionPlan.prototype.canStart = function(gameState)
21 : {
22 0 : if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn
23 0 : return false;
24 :
25 0 : if (!this.isGo(gameState))
26 0 : return false;
27 :
28 0 : if (!this.template.available(gameState))
29 0 : return false;
30 :
31 0 : return gameState.ai.HQ.buildManager.hasBuilder(this.type);
32 : };
33 :
34 0 : PETRA.ConstructionPlan.prototype.start = function(gameState)
35 : {
36 0 : Engine.ProfileStart("Building construction start");
37 :
38 : // We don't care which builder we assign, since they won't actually do
39 : // the building themselves - all we care about is that there is at least
40 : // one unit that can start the foundation (should always be the case here).
41 0 : let builder = gameState.findBuilder(this.type);
42 0 : if (!builder)
43 : {
44 0 : API3.warn("petra error: builder not found when starting construction.");
45 0 : Engine.ProfileStop();
46 0 : return;
47 : }
48 :
49 0 : let pos = this.findGoodPosition(gameState);
50 0 : if (!pos)
51 : {
52 0 : gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room");
53 0 : Engine.ProfileStop();
54 0 : return;
55 : }
56 :
57 0 : if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("Market") ||
58 : gameState.getOwnEntitiesByClass("Market", true).hasEntities()))
59 : {
60 : // Check if this Market is still worth building (others may have been built making it useless).
61 0 : let tradeManager = gameState.ai.HQ.tradeManager;
62 0 : tradeManager.checkRoutes(gameState);
63 0 : if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain))
64 : {
65 0 : Engine.ProfileStop();
66 0 : return;
67 : }
68 : }
69 0 : gameState.ai.HQ.turnCache.buildingBuilt = true;
70 :
71 0 : if (this.metadata === undefined)
72 0 : this.metadata = { "base": pos.base };
73 0 : else if (this.metadata.base === undefined)
74 0 : this.metadata.base = pos.base;
75 :
76 0 : if (pos.access)
77 0 : this.metadata.access = pos.access; // needed for Docks whose position is on water
78 : else
79 0 : this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]);
80 :
81 0 : if (this.template.buildPlacementType() == "shore")
82 : {
83 : // adjust a bit the position if needed
84 0 : let cosa = Math.cos(pos.angle);
85 0 : let sina = Math.sin(pos.angle);
86 0 : let shiftMax = gameState.ai.HQ.territoryMap.cellSize;
87 0 : for (let shift = 0; shift <= shiftMax; shift += 2)
88 : {
89 0 : builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata);
90 0 : if (shift > 0)
91 0 : builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata);
92 : }
93 : }
94 0 : else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz)
95 0 : builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata);
96 : else // try with the lowest, move towards us unless we're same
97 : {
98 0 : for (let step = 0; step <= 1; step += 0.2)
99 0 : builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz,
100 : pos.angle, this.metadata);
101 : }
102 0 : this.onStart(gameState);
103 0 : Engine.ProfileStop();
104 :
105 0 : if (this.metadata && this.metadata.proximity)
106 0 : gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access);
107 : };
108 :
109 0 : PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState)
110 : {
111 0 : let template = this.template;
112 :
113 0 : if (template.buildPlacementType() == "shore")
114 0 : return this.findDockPosition(gameState);
115 :
116 0 : let HQ = gameState.ai.HQ;
117 0 : if (template.hasClass("Storehouse") && this.metadata && this.metadata.base)
118 : {
119 : // recompute the best dropsite location in case some conditions have changed
120 0 : let base = HQ.getBaseByID(this.metadata.base);
121 0 : let type = this.metadata.type ? this.metadata.type : "wood";
122 0 : const newpos = base.findBestDropsiteLocation(gameState, type, template._templateName);
123 0 : if (newpos && newpos.quality > 0)
124 : {
125 0 : let pos = newpos.pos;
126 0 : return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base };
127 : }
128 : }
129 :
130 0 : if (!this.position)
131 : {
132 0 : if (template.hasClass("CivCentre"))
133 : {
134 : let pos;
135 0 : if (this.metadata && this.metadata.resource)
136 : {
137 0 : let proximity = this.metadata.proximity ? this.metadata.proximity : undefined;
138 0 : pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity);
139 : }
140 : else
141 0 : pos = HQ.findStrategicCCLocation(gameState, template);
142 :
143 0 : if (pos)
144 0 : return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 };
145 : // No possible location, try to build instead a dock in a not-enemy island
146 0 : let templateName = gameState.applyCiv("structures/{civ}/dock");
147 0 : if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName))
148 : {
149 0 : template = gameState.getTemplate(templateName);
150 0 : if (template && gameState.getResources().canAfford(new API3.Resources(template.cost())))
151 0 : this.buildOverseaDock(gameState, template);
152 : }
153 0 : return false;
154 : }
155 0 : else if (template.hasClasses(["Tower", "Fortress", "ArmyCamp"]))
156 : {
157 0 : let pos = HQ.findDefensiveLocation(gameState, template);
158 0 : if (pos)
159 0 : return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] };
160 : // if this fortress is our first one, just try the standard placement
161 0 : if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities())
162 0 : return false;
163 : }
164 0 : else if (template.hasClass("Market")) // Docks are done before.
165 : {
166 0 : let pos = HQ.findMarketLocation(gameState, template);
167 0 : if (pos && pos[2] > 0)
168 : {
169 0 : if (!this.metadata)
170 0 : this.metadata = {};
171 0 : this.metadata.expectedGain = pos[3];
172 0 : return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] };
173 : }
174 0 : else if (!pos)
175 0 : return false;
176 : }
177 : }
178 :
179 : // Compute each tile's closeness to friendly structures:
180 :
181 0 : let placement = new API3.Map(gameState.sharedScript, "territory");
182 0 : let cellSize = placement.cellSize; // size of each tile
183 :
184 0 : let alreadyHasHouses = false;
185 :
186 0 : if (this.position) // If a position was specified then place the building as close to it as possible
187 : {
188 0 : let x = Math.floor(this.position[0] / cellSize);
189 0 : let z = Math.floor(this.position[1] / cellSize);
190 0 : placement.addInfluence(x, z, 255);
191 : }
192 : else // No position was specified so try and find a sensible place to build
193 : {
194 : // give a small > 0 level as the result of addInfluence is constrained to be > 0
195 : // if we really need houses (i.e. Phasing without enough village building), do not apply these constraints
196 0 : if (this.metadata && this.metadata.base !== undefined)
197 : {
198 0 : let base = this.metadata.base;
199 0 : for (let j = 0; j < placement.map.length; ++j)
200 0 : if (HQ.baseAtIndex(j) == base)
201 0 : placement.set(j, 45);
202 : }
203 : else
204 : {
205 0 : for (let j = 0; j < placement.map.length; ++j)
206 0 : if (HQ.baseAtIndex(j) != 0)
207 0 : placement.set(j, 45);
208 : }
209 :
210 0 : if (!HQ.requireHouses || !template.hasClass("House"))
211 : {
212 0 : gameState.getOwnStructures().forEach(function(ent) {
213 0 : let pos = ent.position();
214 0 : let x = Math.round(pos[0] / cellSize);
215 0 : let z = Math.round(pos[1] / cellSize);
216 :
217 0 : let struct = PETRA.getBuiltEntity(gameState, ent);
218 0 : if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1)
219 : {
220 0 : if (template.hasClasses(["Field", "Corral"]))
221 0 : placement.addInfluence(x, z, 80 / cellSize, 50);
222 : else // If this is not a field add a negative influence because we want to leave this area for fields
223 0 : placement.addInfluence(x, z, 80 / cellSize, -20);
224 : }
225 0 : else if (template.hasClass("House"))
226 : {
227 0 : if (ent.hasClass("House"))
228 : {
229 0 : placement.addInfluence(x, z, 60 / cellSize, 40); // houses are close to other houses
230 0 : alreadyHasHouses = true;
231 : }
232 0 : else if (ent.hasClasses(["Gate", "!Wall"]))
233 0 : placement.addInfluence(x, z, 60 / cellSize, -40); // and further away from other stuffs
234 : }
235 0 : else if (template.hasClass("Farmstead") && !ent.hasClasses(["Field", "Corral"]) &&
236 : ent.hasClasses(["Gate", "!Wall"]))
237 0 : placement.addInfluence(x, z, 100 / cellSize, -25); // move farmsteads away to make room (Wall test needed for iber)
238 0 : else if (template.hasClass("GarrisonFortress") && ent.hasClass("House"))
239 0 : placement.addInfluence(x, z, 120 / cellSize, -50);
240 0 : else if (template.hasClass("Military"))
241 0 : placement.addInfluence(x, z, 40 / cellSize, -40);
242 0 : else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field"))
243 0 : placement.addInfluence(x, z, 60 / cellSize, 40);
244 : });
245 : }
246 0 : if (template.hasClass("Farmstead"))
247 : {
248 0 : for (let j = 0; j < placement.map.length; ++j)
249 : {
250 0 : let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3;
251 0 : if (HQ.borderMap.map[j] & PETRA.fullBorder_Mask)
252 0 : value /= 2; // we need space around farmstead, so disfavor map border
253 0 : placement.set(j, value);
254 : }
255 : }
256 : }
257 :
258 : // Requires to be inside our territory, and inside our base territory if required
259 : // and if our first market, put it on border if possible to maximize distance with next Market.
260 0 : let favorBorder = template.hasClass("Market");
261 0 : let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire();
262 0 : let favoredBase = this.metadata && (this.metadata.favoredBase ||
263 : (this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined));
264 0 : if (this.metadata && this.metadata.base !== undefined)
265 : {
266 0 : let base = this.metadata.base;
267 0 : for (let j = 0; j < placement.map.length; ++j)
268 : {
269 0 : if (HQ.baseAtIndex(j) != base)
270 0 : placement.map[j] = 0;
271 0 : else if (placement.map[j] > 0)
272 : {
273 0 : if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask)
274 0 : placement.set(j, placement.map[j] + 50);
275 0 : else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask))
276 0 : placement.set(j, placement.map[j] + 10);
277 :
278 0 : let x = (j % placement.width + 0.5) * cellSize;
279 0 : let z = (Math.floor(j / placement.width) + 0.5) * cellSize;
280 0 : if (HQ.isNearInvadingArmy([x, z]))
281 0 : placement.map[j] = 0;
282 : }
283 : }
284 : }
285 : else
286 : {
287 0 : for (let j = 0; j < placement.map.length; ++j)
288 : {
289 0 : if (HQ.baseAtIndex(j) == 0)
290 0 : placement.map[j] = 0;
291 0 : else if (placement.map[j] > 0)
292 : {
293 0 : if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask)
294 0 : placement.set(j, placement.map[j] + 50);
295 0 : else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask))
296 0 : placement.set(j, placement.map[j] + 10);
297 :
298 0 : let x = (j % placement.width + 0.5) * cellSize;
299 0 : let z = (Math.floor(j / placement.width) + 0.5) * cellSize;
300 0 : if (HQ.isNearInvadingArmy([x, z]))
301 0 : placement.map[j] = 0;
302 0 : else if (favoredBase && HQ.baseAtIndex(j) == favoredBase)
303 0 : placement.set(j, placement.map[j] + 100);
304 : }
305 : }
306 : }
307 :
308 : // Find the best non-obstructed:
309 : // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close,
310 : // this allows room for units to walk between buildings.
311 : // note: not for houses and dropsites who ought to be closer to either each other or a resource.
312 : // also not for fields who can be stacked quite a bit
313 :
314 0 : let obstructions = PETRA.createObstructionMap(gameState, 0, template);
315 : // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png");
316 :
317 0 : let radius = 0;
318 0 : if (template.hasClasses(["Fortress", "Arsenal"]) ||
319 : this.type == gameState.applyCiv("structures/{civ}/elephant_stable"))
320 0 : radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize);
321 0 : else if (template.resourceDropsiteTypes() === undefined && !template.hasClasses(["House", "Field", "Market"]))
322 0 : radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize);
323 : else
324 0 : radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
325 :
326 : let bestTile;
327 0 : if (template.hasClass("House") && !alreadyHasHouses)
328 : {
329 : // try to get some space to place several houses first
330 0 : bestTile = placement.findBestTile(3*radius, obstructions);
331 0 : if (!bestTile.val)
332 0 : bestTile = undefined;
333 : }
334 :
335 0 : if (!bestTile)
336 0 : bestTile = placement.findBestTile(radius, obstructions);
337 :
338 0 : if (!bestTile.val)
339 0 : return false;
340 :
341 0 : let bestIdx = bestTile.idx;
342 :
343 0 : let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
344 0 : let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
345 :
346 0 : let territorypos = placement.gamePosToMapPos([x, z]);
347 0 : let territoryIndex = territorypos[0] + territorypos[1]*placement.width;
348 : // default angle = 3*Math.PI/4;
349 0 : return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.baseAtIndex(territoryIndex) };
350 : };
351 :
352 : /**
353 : * Placement of buildings with Dock build category
354 : * metadata.proximity is defined when first dock without any territory
355 : * => we try to minimize distance from our current point
356 : * metadata.oversea is defined for dock in oversea islands
357 : * => we try to maximize distance to our current docks (for trade)
358 : * otherwise standard dock on an island where we already have a cc
359 : * => we try not to be too far from our territory
360 : * In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it.
361 : */
362 0 : PETRA.ConstructionPlan.prototype.findDockPosition = function(gameState)
363 : {
364 0 : let template = this.template;
365 0 : let territoryMap = gameState.ai.HQ.territoryMap;
366 :
367 0 : let obstructions = PETRA.createObstructionMap(gameState, 0, template);
368 : // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png");
369 :
370 : let bestIdx;
371 : let bestJdx;
372 : let bestAngle;
373 : let bestLand;
374 : let bestWater;
375 0 : let bestVal = -1;
376 0 : let navalPassMap = gameState.ai.accessibility.navalPassMap;
377 :
378 0 : let width = gameState.ai.HQ.territoryMap.width;
379 0 : let cellSize = gameState.ai.HQ.territoryMap.cellSize;
380 :
381 0 : let nbShips = gameState.ai.HQ.navalManager.transportShips.length;
382 0 : let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null;
383 0 : let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null;
384 0 : let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null;
385 0 : let oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null;
386 0 : if (nbShips == 0 && proxyAccess && proxyAccess > 1)
387 : {
388 0 : wantedLand = {};
389 0 : wantedLand[proxyAccess] = true;
390 : }
391 0 : let dropsiteTypes = template.resourceDropsiteTypes();
392 0 : let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
393 :
394 0 : let halfSize = 0; // used for dock angle
395 0 : let halfDepth = 0; // used by checkPlacement
396 0 : let halfWidth = 0; // used by checkPlacement
397 0 : if (template.get("Footprint/Square"))
398 : {
399 0 : halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
400 0 : halfDepth = +template.get("Footprint/Square/@depth") / 2;
401 0 : halfWidth = +template.get("Footprint/Square/@width") / 2;
402 : }
403 0 : else if (template.get("Footprint/Circle"))
404 : {
405 0 : halfSize = +template.get("Footprint/Circle/@radius");
406 0 : halfDepth = halfSize;
407 0 : halfWidth = halfSize;
408 : }
409 :
410 : // res is a measure of the amount of resources around, and maxRes is the max value taken into account
411 : // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement
412 0 : const maxRes = 10;
413 0 : const maxWater = 16;
414 0 : let ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")) : null;
415 0 : let docks = oversea ? gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")) : null;
416 : // Normalisation factors (only guessed, no attempt to optimize them)
417 0 : let factor = proxyAccess ? 1 : oversea ? 0.2 : 40;
418 0 : for (let j = 0; j < territoryMap.length; ++j)
419 : {
420 0 : if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea))
421 0 : continue;
422 0 : let score = 0;
423 0 : if (!proxyAccess && !oversea)
424 : {
425 : // if not in our (or allied) territory, we do not want it too far to be able to defend it
426 0 : score = this.getFrontierProximity(gameState, j);
427 0 : if (score > 4)
428 0 : continue;
429 0 : score *= factor;
430 : }
431 0 : let i = territoryMap.getNonObstructedTile(j, radius, obstructions);
432 0 : if (i < 0)
433 0 : continue;
434 0 : if (wantedSea && navalPassMap[i] != wantedSea)
435 0 : continue;
436 :
437 0 : let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes;
438 0 : let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
439 :
440 : // If proximity is given, we look for the nearest point
441 0 : if (proxyAccess)
442 0 : score = API3.VectorDistance(this.metadata.proximity, pos);
443 :
444 : // Bonus for resources
445 0 : score += 20 * (maxRes - res);
446 :
447 0 : if (oversea)
448 : {
449 : // Not much farther to one of our cc than to enemy ones
450 : let enemyDist;
451 : let ownDist;
452 0 : for (let cc of ccEnts.values())
453 : {
454 0 : let owner = cc.owner();
455 0 : if (owner != PlayerID && !gameState.isPlayerEnemy(owner))
456 0 : continue;
457 0 : let dist = API3.SquareVectorDistance(pos, cc.position());
458 0 : if (owner == PlayerID && (!ownDist || dist < ownDist))
459 0 : ownDist = dist;
460 0 : if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist))
461 0 : enemyDist = dist;
462 : }
463 0 : if (ownDist && enemyDist && enemyDist < 0.5 * ownDist)
464 0 : continue;
465 :
466 : // And maximize distance for trade.
467 0 : let dockDist = 0;
468 0 : for (let dock of docks.values())
469 : {
470 0 : if (PETRA.getSeaAccess(gameState, dock) != navalPassMap[i])
471 0 : continue;
472 0 : let dist = API3.SquareVectorDistance(pos, dock.position());
473 0 : if (dist > dockDist)
474 0 : dockDist = dist;
475 : }
476 0 : if (dockDist > 0)
477 : {
478 0 : dockDist = Math.sqrt(dockDist);
479 0 : if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away
480 0 : continue;
481 0 : score += factor * (width * cellSize - dockDist);
482 : }
483 : }
484 :
485 : // Add a penalty if on the map border as ship movement will be difficult
486 0 : if (gameState.ai.HQ.borderMap.map[j] & PETRA.fullBorder_Mask)
487 0 : score += 20;
488 :
489 : // Do a pre-selection, supposing we will have the best possible water
490 0 : if (bestIdx !== undefined && score > bestVal + 5 * maxWater)
491 0 : continue;
492 :
493 0 : let x = (i % obstructions.width + 0.5) * obstructions.cellSize;
494 0 : let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize;
495 0 : let angle = this.getDockAngle(gameState, x, z, halfSize);
496 0 : if (angle == false)
497 0 : continue;
498 0 : let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle);
499 0 : if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land])
500 0 : continue;
501 : // Final selection now that the checkDockPlacement water is known
502 0 : if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal)
503 0 : continue;
504 0 : if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000)
505 0 : continue;
506 0 : if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize))
507 0 : continue;
508 :
509 0 : bestVal = score + maxWater - ret.water;
510 0 : bestIdx = i;
511 0 : bestJdx = j;
512 0 : bestAngle = angle;
513 0 : bestLand = ret.land;
514 0 : bestWater = ret.water;
515 : }
516 0 : if (bestVal < 0)
517 0 : return false;
518 :
519 : // if no good place with enough water around and still in first phase, wait for expansion at the next phase
520 0 : if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1)
521 0 : return false;
522 :
523 0 : let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
524 0 : let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
525 :
526 : // Assign this dock to a base
527 0 : let baseIndex = gameState.ai.HQ.baseAtIndex(bestJdx);
528 0 : if (!baseIndex)
529 0 : baseIndex = -2; // We'll do an anchorless base around it
530 :
531 0 : return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand };
532 : };
533 :
534 : /**
535 : * Find a good island to build a dock.
536 : */
537 0 : PETRA.ConstructionPlan.prototype.buildOverseaDock = function(gameState, template)
538 : {
539 0 : let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock"));
540 0 : if (!docks.hasEntities())
541 0 : return;
542 :
543 0 : let passabilityMap = gameState.getPassabilityMap();
544 0 : let cellArea = passabilityMap.cellSize * passabilityMap.cellSize;
545 0 : let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
546 :
547 0 : let land = {};
548 : let found;
549 0 : for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i)
550 : {
551 0 : if (gameState.ai.accessibility.regionType[i] != "land" ||
552 : cellArea * gameState.ai.accessibility.regionSize[i] < 3600)
553 0 : continue;
554 0 : let keep = true;
555 0 : for (let dock of docks.values())
556 : {
557 0 : if (PETRA.getLandAccess(gameState, dock) != i)
558 0 : continue;
559 0 : keep = false;
560 0 : break;
561 : }
562 0 : if (!keep)
563 0 : continue;
564 : let sea;
565 0 : for (let cc of ccEnts.values())
566 : {
567 0 : let ccAccess = PETRA.getLandAccess(gameState, cc);
568 0 : if (ccAccess != i)
569 : {
570 0 : if (cc.owner() == PlayerID && !sea)
571 0 : sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i);
572 0 : continue;
573 : }
574 : // Docks on island where we have a cc are already done elsewhere
575 0 : if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner()))
576 : {
577 0 : keep = false;
578 0 : break;
579 : }
580 : }
581 0 : if (!keep || !sea)
582 0 : continue;
583 0 : land[i] = true;
584 0 : found = true;
585 : }
586 0 : if (!found)
587 0 : return;
588 0 : if (!gameState.ai.HQ.navalMap)
589 0 : API3.warn("petra.findOverseaLand on a non-naval map??? we should never go there ");
590 :
591 0 : let oldTemplate = this.template;
592 0 : let oldMetadata = this.metadata;
593 0 : this.template = template;
594 : let pos;
595 0 : this.metadata = { "land": land, "oversea": true };
596 0 : pos = this.findDockPosition(gameState);
597 0 : if (pos)
598 : {
599 0 : let type = template.templateName();
600 0 : let builder = gameState.findBuilder(type);
601 0 : this.metadata.base = pos.base;
602 : // Adjust a bit the position if needed
603 0 : let cosa = Math.cos(pos.angle);
604 0 : let sina = Math.sin(pos.angle);
605 0 : let shiftMax = gameState.ai.HQ.territoryMap.cellSize;
606 0 : for (let shift = 0; shift <= shiftMax; shift += 2)
607 : {
608 0 : builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata);
609 0 : if (shift > 0)
610 0 : builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata);
611 : }
612 : }
613 0 : this.template = oldTemplate;
614 0 : this.metadata = oldMetadata;
615 : };
616 :
617 : /** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */
618 0 : PETRA.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size)
619 : {
620 0 : let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]);
621 0 : let k = pos[0] + pos[1]*gameState.ai.accessibility.width;
622 0 : let seaRef = gameState.ai.accessibility.navalPassMap[k];
623 0 : if (seaRef < 2)
624 0 : return false;
625 0 : const numPoints = 16;
626 0 : for (let dist = 0; dist < 4; ++dist)
627 : {
628 0 : let waterPoints = [];
629 0 : for (let i = 0; i < numPoints; ++i)
630 : {
631 0 : let angle = 2 * Math.PI * i / numPoints;
632 0 : pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)];
633 0 : pos = gameState.ai.accessibility.gamePosToMapPos(pos);
634 0 : if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
635 : pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
636 0 : continue;
637 0 : let j = pos[0] + pos[1]*gameState.ai.accessibility.width;
638 0 : if (gameState.ai.accessibility.navalPassMap[j] == seaRef)
639 0 : waterPoints.push(i);
640 : }
641 0 : let length = waterPoints.length;
642 0 : if (!length)
643 0 : continue;
644 0 : let consec = [];
645 0 : for (let i = 0; i < length; ++i)
646 : {
647 0 : let count = 0;
648 0 : for (let j = 0; j < length-1; ++j)
649 : {
650 0 : if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length])
651 0 : ++count;
652 : else
653 0 : break;
654 : }
655 0 : consec[i] = count;
656 : }
657 0 : let start = 0;
658 0 : let count = 0;
659 0 : for (let c in consec)
660 : {
661 0 : if (consec[c] > count)
662 : {
663 0 : start = c;
664 0 : count = consec[c];
665 : }
666 : }
667 :
668 : // If we've found a shoreline, stop searching
669 0 : if (count != numPoints-1)
670 0 : return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI;
671 : }
672 0 : return false;
673 : };
674 :
675 : /**
676 : * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js
677 : * to determine the special dock requirements
678 : * returns {"land": land index for this dock, "water": amount of water around this spot}
679 : */
680 0 : PETRA.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle)
681 : {
682 0 : let sz = halfDepth * Math.sin(angle);
683 0 : let cz = halfDepth * Math.cos(angle);
684 : // center back position
685 0 : let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]);
686 0 : let j = pos[0] + pos[1]*gameState.ai.accessibility.width;
687 0 : let land = gameState.ai.accessibility.landPassMap[j];
688 0 : if (land < 2)
689 0 : return null;
690 : // center front position
691 0 : pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]);
692 0 : j = pos[0] + pos[1]*gameState.ai.accessibility.width;
693 0 : if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
694 0 : return null;
695 : // additional constraints compared to BuildRestriction.js to assure we have enough place to build
696 0 : let sw = halfWidth * Math.cos(angle) * 3 / 4;
697 0 : let cw = halfWidth * Math.sin(angle) * 3 / 4;
698 0 : pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]);
699 0 : j = pos[0] + pos[1]*gameState.ai.accessibility.width;
700 0 : if (gameState.ai.accessibility.landPassMap[j] != land)
701 0 : return null;
702 0 : pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]);
703 0 : j = pos[0] + pos[1]*gameState.ai.accessibility.width;
704 0 : if (gameState.ai.accessibility.landPassMap[j] != land)
705 0 : return null;
706 0 : let water = 0;
707 0 : let sp = 15 * Math.sin(angle);
708 0 : let cp = 15 * Math.cos(angle);
709 0 : for (let i = 1; i < 5; ++i)
710 : {
711 0 : pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]);
712 0 : if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
713 : pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
714 0 : break;
715 0 : j = pos[0] + pos[1]*gameState.ai.accessibility.width;
716 0 : if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
717 0 : break;
718 0 : pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]);
719 0 : if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
720 : pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
721 0 : break;
722 0 : j = pos[0] + pos[1]*gameState.ai.accessibility.width;
723 0 : if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
724 0 : break;
725 0 : pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]);
726 0 : if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
727 : pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
728 0 : break;
729 0 : j = pos[0] + pos[1]*gameState.ai.accessibility.width;
730 0 : if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
731 0 : break;
732 0 : water += 4;
733 : }
734 0 : return { "land": land, "water": water };
735 : };
736 :
737 : /**
738 : * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension
739 : * if the (object) wantedLand is given, this nearest land should have one of these accessibility
740 : * if wantedSea is given, this tile should be inside this sea
741 : */
742 0 : PETRA.ConstructionPlan.prototype.around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50],
743 : [-1.0, 0.0], [-0.87, -0.50], [-0.50, -0.87], [ 0.0, -1.0], [ 0.50, -0.87], [ 0.87, -0.50]];
744 :
745 0 : PETRA.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea)
746 : {
747 0 : let width = gameState.ai.HQ.territoryMap.width;
748 0 : let cellSize = gameState.ai.HQ.territoryMap.cellSize;
749 0 : let dimLand = dimension + 1.5 * cellSize;
750 0 : let dimSea = dimension + 2 * cellSize;
751 :
752 0 : let accessibility = gameState.ai.accessibility;
753 0 : let x = (j%width + 0.5) * cellSize;
754 0 : let z = (Math.floor(j/width) + 0.5) * cellSize;
755 0 : let pos = accessibility.gamePosToMapPos([x, z]);
756 0 : let k = pos[0] + pos[1]*accessibility.width;
757 0 : let landPass = accessibility.landPassMap[k];
758 0 : if (landPass > 1 && wantedLand && !wantedLand[landPass] ||
759 : landPass < 2 && accessibility.navalPassMap[k] < 2)
760 0 : return false;
761 :
762 0 : for (let a of this.around)
763 : {
764 0 : pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]);
765 0 : if (pos[0] < 0 || pos[0] >= accessibility.width)
766 0 : continue;
767 0 : if (pos[1] < 0 || pos[1] >= accessibility.height)
768 0 : continue;
769 0 : k = pos[0] + pos[1]*accessibility.width;
770 0 : landPass = accessibility.landPassMap[k];
771 0 : if (landPass < 2 || wantedLand && !wantedLand[landPass])
772 0 : continue;
773 0 : pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]);
774 0 : if (pos[0] < 0 || pos[0] >= accessibility.width)
775 0 : continue;
776 0 : if (pos[1] < 0 || pos[1] >= accessibility.height)
777 0 : continue;
778 0 : k = pos[0] + pos[1]*accessibility.width;
779 0 : if (wantedSea && accessibility.navalPassMap[k] != wantedSea ||
780 : !wantedSea && accessibility.navalPassMap[k] < 2)
781 0 : continue;
782 0 : return true;
783 : }
784 :
785 0 : return false;
786 : };
787 :
788 : /**
789 : * return a measure of the proximity to our frontier (including our allies)
790 : * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m
791 : */
792 0 : PETRA.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j)
793 : {
794 0 : let alliedVictory = gameState.getAlliedVictory();
795 0 : let territoryMap = gameState.ai.HQ.territoryMap;
796 0 : let territoryOwner = territoryMap.getOwnerIndex(j);
797 0 : if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner))
798 0 : return 0;
799 :
800 0 : let borderMap = gameState.ai.HQ.borderMap;
801 0 : let width = territoryMap.width;
802 0 : let step = Math.round(24 / territoryMap.cellSize);
803 0 : let ix = j % width;
804 0 : let iz = Math.floor(j / width);
805 0 : let best = 5;
806 0 : for (let a of this.around)
807 : {
808 0 : for (let i = 1; i < 5; ++i)
809 : {
810 0 : let jx = ix + Math.round(i*step*a[0]);
811 0 : if (jx < 0 || jx >= width)
812 0 : continue;
813 0 : let jz = iz + Math.round(i*step*a[1]);
814 0 : if (jz < 0 || jz >= width)
815 0 : continue;
816 0 : if (borderMap.map[jx+width*jz] & PETRA.outside_Mask)
817 0 : continue;
818 0 : territoryOwner = territoryMap.getOwnerIndex(jx+width*jz);
819 0 : if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID)
820 : {
821 0 : best = Math.min(best, i);
822 0 : break;
823 : }
824 : }
825 0 : if (best == 1)
826 0 : break;
827 : }
828 :
829 0 : return best;
830 : };
831 :
832 : /**
833 : * get the sum of the resources (except food) around, inside a given radius
834 : * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood
835 : */
836 0 : PETRA.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius)
837 : {
838 0 : let resourceMaps = gameState.sharedScript.resourceMaps;
839 0 : let w = resourceMaps.wood.width;
840 0 : let cellSize = resourceMaps.wood.cellSize;
841 0 : let size = Math.floor(radius / cellSize);
842 0 : let ix = i % w;
843 0 : let iy = Math.floor(i / w);
844 0 : let total = 0;
845 0 : let nbcell = 0;
846 0 : for (let k of types)
847 : {
848 0 : if (k == "food" || !resourceMaps[k])
849 0 : continue;
850 0 : let weigh0 = k == "wood" ? 2 : 1;
851 0 : for (let dy = 0; dy <= size; ++dy)
852 : {
853 0 : let dxmax = size - dy;
854 0 : let ky = iy + dy;
855 0 : if (ky >= 0 && ky < w)
856 : {
857 0 : for (let dx = -dxmax; dx <= dxmax; ++dx)
858 : {
859 0 : let kx = ix + dx;
860 0 : if (kx < 0 || kx >= w)
861 0 : continue;
862 0 : let ddx = dx > 0 ? dx : -dx;
863 0 : let weight = weigh0 * (dxmax - ddx) / size;
864 0 : total += weight * resourceMaps[k].map[kx + w * ky];
865 0 : nbcell += weight;
866 : }
867 : }
868 0 : if (dy == 0)
869 0 : continue;
870 0 : ky = iy - dy;
871 0 : if (ky >= 0 && ky < w)
872 : {
873 0 : for (let dx = -dxmax; dx <= dxmax; ++dx)
874 : {
875 0 : let kx = ix + dx;
876 0 : if (kx < 0 || kx >= w)
877 0 : continue;
878 0 : let ddx = dx > 0 ? dx : -dx;
879 0 : let weight = weigh0 * (dxmax - ddx) / size;
880 0 : total += weight * resourceMaps[k].map[kx + w * ky];
881 0 : nbcell += weight;
882 : }
883 : }
884 : }
885 : }
886 0 : return nbcell ? total / nbcell : 0;
887 : };
888 :
889 0 : PETRA.ConstructionPlan.prototype.isGo = function(gameState)
890 : {
891 0 : if (this.goRequirement && this.goRequirement == "houseNeeded")
892 : {
893 0 : if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/house") &&
894 : !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/apartment"))
895 0 : return false;
896 0 : if (gameState.getPopulationMax() <= gameState.getPopulationLimit())
897 0 : return false;
898 0 : let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation();
899 0 : for (let ent of gameState.getOwnFoundations().values())
900 : {
901 0 : let template = gameState.getBuiltTemplate(ent.templateName());
902 0 : if (template)
903 0 : freeSlots += template.getPopulationBonus();
904 : }
905 :
906 0 : if (gameState.ai.HQ.saveResources)
907 0 : return freeSlots <= 10;
908 0 : if (gameState.getPopulation() > 55)
909 0 : return freeSlots <= 21;
910 0 : if (gameState.getPopulation() > 30)
911 0 : return freeSlots <= 15;
912 0 : return freeSlots <= 10;
913 : }
914 0 : return true;
915 : };
916 :
917 0 : PETRA.ConstructionPlan.prototype.onStart = function(gameState)
918 : {
919 0 : if (this.queueToReset)
920 0 : gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]);
921 : };
922 :
923 0 : PETRA.ConstructionPlan.prototype.Serialize = function()
924 : {
925 0 : return {
926 : "category": this.category,
927 : "type": this.type,
928 : "ID": this.ID,
929 : "metadata": this.metadata,
930 : "cost": this.cost.Serialize(),
931 : "number": this.number,
932 : "position": this.position,
933 : "goRequirement": this.goRequirement || undefined,
934 : "queueToReset": this.queueToReset || undefined
935 : };
936 : };
937 :
938 0 : PETRA.ConstructionPlan.prototype.Deserialize = function(gameState, data)
939 : {
940 0 : for (let key in data)
941 0 : this[key] = data[key];
942 :
943 0 : this.cost = new API3.Resources();
944 0 : this.cost.Deserialize(data.cost);
945 : };
|