Line data Source code
1 : /**
2 : * Naval Manager
3 : * Will deal with anything ships.
4 : * -Basically trade over water (with fleets and goals commissioned by the economy manager)
5 : * -Defense over water (commissioned by the defense manager)
6 : * -Transport of units over water (a few units).
7 : * -Scouting, ultimately.
8 : * Also deals with handling docks, making sure we have access and stuffs like that.
9 : */
10 0 : PETRA.NavalManager = function(Config)
11 : {
12 0 : this.Config = Config;
13 :
14 : // ship subCollections. Also exist for land zones, idem, not caring.
15 0 : this.seaShips = [];
16 0 : this.seaTransportShips = [];
17 0 : this.seaWarShips = [];
18 0 : this.seaFishShips = [];
19 :
20 : // wanted NB per zone.
21 0 : this.wantedTransportShips = [];
22 0 : this.wantedWarShips = [];
23 0 : this.wantedFishShips = [];
24 : // needed NB per zone.
25 0 : this.neededTransportShips = [];
26 0 : this.neededWarShips = [];
27 :
28 0 : this.transportPlans = [];
29 :
30 : // shore-line regions where we can load and unload units
31 0 : this.landingZones = {};
32 : };
33 :
34 : /** More initialisation for stuff that needs the gameState */
35 0 : PETRA.NavalManager.prototype.init = function(gameState, deserializing)
36 : {
37 : // docks
38 0 : this.docks = gameState.getOwnStructures().filter(API3.Filters.byClasses(["Dock", "Shipyard"]));
39 0 : this.docks.registerUpdates();
40 :
41 0 : this.ships = gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byClass("Ship"), API3.Filters.not(API3.Filters.byMetadata(PlayerID, "role", PETRA.Worker.ROLE_TRADER))));
42 : // note: those two can overlap (some transport ships are warships too and vice-versa).
43 0 : this.transportShips = this.ships.filter(API3.Filters.and(API3.Filters.byCanGarrison(), API3.Filters.not(API3.Filters.byClass("FishingBoat"))));
44 0 : this.warShips = this.ships.filter(API3.Filters.byClass("Warship"));
45 0 : this.fishShips = this.ships.filter(API3.Filters.byClass("FishingBoat"));
46 :
47 0 : this.ships.registerUpdates();
48 0 : this.transportShips.registerUpdates();
49 0 : this.warShips.registerUpdates();
50 0 : this.fishShips.registerUpdates();
51 :
52 0 : let availableFishes = {};
53 0 : for (let fish of gameState.getFishableSupplies().values())
54 : {
55 0 : let sea = this.getFishSea(gameState, fish);
56 0 : if (sea && availableFishes[sea])
57 0 : availableFishes[sea] += fish.resourceSupplyAmount();
58 0 : else if (sea)
59 0 : availableFishes[sea] = fish.resourceSupplyAmount();
60 : }
61 :
62 0 : for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i)
63 : {
64 0 : if (!gameState.ai.HQ.navalRegions[i])
65 : {
66 : // push dummies
67 0 : this.seaShips.push(undefined);
68 0 : this.seaTransportShips.push(undefined);
69 0 : this.seaWarShips.push(undefined);
70 0 : this.seaFishShips.push(undefined);
71 0 : this.wantedTransportShips.push(0);
72 0 : this.wantedWarShips.push(0);
73 0 : this.wantedFishShips.push(0);
74 0 : this.neededTransportShips.push(0);
75 0 : this.neededWarShips.push(0);
76 : }
77 : else
78 : {
79 0 : let collec = this.ships.filter(API3.Filters.byMetadata(PlayerID, "sea", i));
80 0 : collec.registerUpdates();
81 0 : this.seaShips.push(collec);
82 0 : collec = this.transportShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i));
83 0 : collec.registerUpdates();
84 0 : this.seaTransportShips.push(collec);
85 0 : collec = this.warShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i));
86 0 : collec.registerUpdates();
87 0 : this.seaWarShips.push(collec);
88 0 : collec = this.fishShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i));
89 0 : collec.registerUpdates();
90 0 : this.seaFishShips.push(collec);
91 0 : this.wantedTransportShips.push(0);
92 0 : this.wantedWarShips.push(0);
93 0 : if (availableFishes[i] && availableFishes[i] > 1000)
94 0 : this.wantedFishShips.push(this.Config.Economy.targetNumFishers);
95 : else
96 0 : this.wantedFishShips.push(0);
97 0 : this.neededTransportShips.push(0);
98 0 : this.neededWarShips.push(0);
99 : }
100 : }
101 :
102 0 : if (deserializing)
103 0 : return;
104 :
105 : // determination of the possible landing zones
106 0 : let width = gameState.getPassabilityMap().width;
107 0 : let length = width * gameState.getPassabilityMap().height;
108 0 : for (let i = 0; i < length; ++i)
109 : {
110 0 : let land = gameState.ai.accessibility.landPassMap[i];
111 0 : if (land < 2)
112 0 : continue;
113 0 : let naval = gameState.ai.accessibility.navalPassMap[i];
114 0 : if (naval < 2)
115 0 : continue;
116 0 : if (!this.landingZones[land])
117 0 : this.landingZones[land] = {};
118 0 : if (!this.landingZones[land][naval])
119 0 : this.landingZones[land][naval] = new Set();
120 0 : this.landingZones[land][naval].add(i);
121 : }
122 : // and keep only thoses with enough room around when possible
123 0 : for (let land in this.landingZones)
124 : {
125 0 : for (let sea in this.landingZones[land])
126 : {
127 0 : let landing = this.landingZones[land][sea];
128 0 : let nbaround = {};
129 0 : let nbcut = 0;
130 0 : for (let i of landing)
131 : {
132 0 : let nb = 0;
133 0 : if (landing.has(i-1))
134 0 : nb++;
135 0 : if (landing.has(i+1))
136 0 : nb++;
137 0 : if (landing.has(i+width))
138 0 : nb++;
139 0 : if (landing.has(i-width))
140 0 : nb++;
141 0 : nbaround[i] = nb;
142 0 : nbcut = Math.max(nb, nbcut);
143 : }
144 0 : nbcut = Math.min(2, nbcut);
145 0 : for (let i of landing)
146 : {
147 0 : if (nbaround[i] < nbcut)
148 0 : landing.delete(i);
149 : }
150 : }
151 : }
152 :
153 : // Assign our initial docks and ships
154 0 : for (let ship of this.ships.values())
155 0 : PETRA.setSeaAccess(gameState, ship);
156 0 : for (let dock of this.docks.values())
157 0 : PETRA.setSeaAccess(gameState, dock);
158 : };
159 :
160 0 : PETRA.NavalManager.prototype.updateFishingBoats = function(sea, num)
161 : {
162 0 : if (this.wantedFishShips[sea])
163 0 : this.wantedFishShips[sea] = num;
164 : };
165 :
166 0 : PETRA.NavalManager.prototype.resetFishingBoats = function(gameState, sea)
167 : {
168 0 : if (sea !== undefined)
169 0 : this.wantedFishShips[sea] = 0;
170 : else
171 0 : this.wantedFishShips.fill(0);
172 : };
173 :
174 : /** Get the sea, cache it if not yet done and check if in opensea */
175 0 : PETRA.NavalManager.prototype.getFishSea = function(gameState, fish)
176 : {
177 0 : let sea = fish.getMetadata(PlayerID, "sea");
178 0 : if (sea)
179 0 : return sea;
180 0 : const ntry = 4;
181 0 : const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]];
182 0 : let pos = gameState.ai.accessibility.gamePosToMapPos(fish.position());
183 0 : let width = gameState.ai.accessibility.width;
184 0 : let k = pos[0] + pos[1]*width;
185 0 : sea = gameState.ai.accessibility.navalPassMap[k];
186 0 : fish.setMetadata(PlayerID, "sea", sea);
187 0 : let radius = 120 / gameState.ai.accessibility.cellSize / ntry;
188 0 : if (around.every(a => {
189 0 : for (let t = 0; t < ntry; ++t)
190 : {
191 0 : let i = pos[0] + Math.round(a[0]*radius*(ntry-t));
192 0 : let j = pos[1] + Math.round(a[1]*radius*(ntry-t));
193 0 : if (i < 0 || i >= width || j < 0 || j >= width)
194 0 : continue;
195 0 : if (gameState.ai.accessibility.landPassMap[i + j*width] === 1)
196 : {
197 0 : let navalPass = gameState.ai.accessibility.navalPassMap[i + j*width];
198 0 : if (navalPass == sea)
199 0 : return true;
200 0 : else if (navalPass == 1) // we could be outside the map
201 0 : continue;
202 : }
203 0 : return false;
204 : }
205 0 : return true;
206 : }))
207 0 : fish.setMetadata(PlayerID, "opensea", true);
208 0 : return sea;
209 : };
210 :
211 : /** check if we can safely fish at the fish position */
212 0 : PETRA.NavalManager.prototype.canFishSafely = function(gameState, fish)
213 : {
214 0 : if (fish.getMetadata(PlayerID, "opensea"))
215 0 : return true;
216 0 : const ntry = 2;
217 0 : const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]];
218 0 : let territoryMap = gameState.ai.HQ.territoryMap;
219 0 : let width = territoryMap.width;
220 0 : let radius = 120 / territoryMap.cellSize / ntry;
221 0 : let pos = territoryMap.gamePosToMapPos(fish.position());
222 0 : return around.every(a => {
223 0 : for (let t = 0; t < ntry; ++t)
224 : {
225 0 : let i = pos[0] + Math.round(a[0]*radius*(ntry-t));
226 0 : let j = pos[1] + Math.round(a[1]*radius*(ntry-t));
227 0 : if (i < 0 || i >= width || j < 0 || j >= width)
228 0 : continue;
229 0 : let owner = territoryMap.getOwnerIndex(i + j*width);
230 0 : if (owner != 0 && gameState.isPlayerEnemy(owner))
231 0 : return false;
232 : }
233 0 : return true;
234 : });
235 : };
236 :
237 : /** get the list of seas (or lands) around this region not connected by a dock */
238 0 : PETRA.NavalManager.prototype.getUnconnectedSeas = function(gameState, region)
239 : {
240 0 : let seas = gameState.ai.accessibility.regionLinks[region].slice();
241 0 : this.docks.forEach(dock => {
242 0 : if (!dock.hasClass("Dock") || PETRA.getLandAccess(gameState, dock) != region)
243 0 : return;
244 0 : let i = seas.indexOf(PETRA.getSeaAccess(gameState, dock));
245 0 : if (i != -1)
246 0 : seas.splice(i--, 1);
247 : });
248 0 : return seas;
249 : };
250 :
251 0 : PETRA.NavalManager.prototype.checkEvents = function(gameState, queues, events)
252 : {
253 0 : for (let evt of events.Create)
254 : {
255 0 : if (!evt.entity)
256 0 : continue;
257 0 : let ent = gameState.getEntityById(evt.entity);
258 0 : if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && ent.hasClasses(["Dock", "Shipyard"]))
259 0 : PETRA.setSeaAccess(gameState, ent);
260 : }
261 :
262 0 : for (let evt of events.TrainingFinished)
263 : {
264 0 : if (!evt.entities)
265 0 : continue;
266 0 : for (let entId of evt.entities)
267 : {
268 0 : let ent = gameState.getEntityById(entId);
269 0 : if (!ent || !ent.hasClass("Ship") || !ent.isOwn(PlayerID))
270 0 : continue;
271 0 : PETRA.setSeaAccess(gameState, ent);
272 : }
273 : }
274 :
275 0 : for (let evt of events.Destroy)
276 : {
277 0 : if (!evt.entityObj || evt.entityObj.owner() !== PlayerID || !evt.metadata || !evt.metadata[PlayerID])
278 0 : continue;
279 0 : if (!evt.entityObj.hasClass("Ship") || !evt.metadata[PlayerID].transporter)
280 0 : continue;
281 0 : let plan = this.getPlan(evt.metadata[PlayerID].transporter);
282 0 : if (!plan)
283 0 : continue;
284 :
285 0 : let shipId = evt.entityObj.id();
286 0 : if (this.Config.debug > 1)
287 0 : API3.warn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + plan.state);
288 0 : if (plan.state === PETRA.TransportPlan.BOARDING)
289 : {
290 : // just reset the units onBoard metadata and wait for a new ship to be assigned to this plan
291 0 : plan.units.forEach(ent => {
292 0 : if (ent.getMetadata(PlayerID, "onBoard") == "onBoard" && ent.position() ||
293 : ent.getMetadata(PlayerID, "onBoard") == shipId)
294 0 : ent.setMetadata(PlayerID, "onBoard", undefined);
295 : });
296 0 : plan.needTransportShips = !plan.transportShips.hasEntities();
297 : }
298 0 : else if (plan.state === PETRA.TransportPlan.SAILING)
299 : {
300 0 : let endIndex = plan.endIndex;
301 0 : for (let ent of plan.units.values())
302 : {
303 0 : if (!ent.position()) // unit from another ship of this plan ... do nothing
304 0 : continue;
305 0 : let access = PETRA.getLandAccess(gameState, ent);
306 0 : let endPos = ent.getMetadata(PlayerID, "endPos");
307 0 : ent.setMetadata(PlayerID, "transport", undefined);
308 0 : ent.setMetadata(PlayerID, "onBoard", undefined);
309 0 : ent.setMetadata(PlayerID, "endPos", undefined);
310 : // nothing else to do if access = endIndex as already at destination
311 : // otherwise, we should require another transport
312 : // TODO if attacking and no more ships available, remove the units from the attack
313 : // to avoid delaying it too much
314 0 : if (access != endIndex)
315 0 : this.requireTransport(gameState, ent, access, endIndex, endPos);
316 : }
317 : }
318 : }
319 :
320 0 : for (let evt of events.OwnershipChanged) // capture events
321 : {
322 0 : if (evt.to !== PlayerID)
323 0 : continue;
324 0 : let ent = gameState.getEntityById(evt.entity);
325 0 : if (ent && ent.hasClasses(["Dock", "Shipyard"]))
326 0 : PETRA.setSeaAccess(gameState, ent);
327 : }
328 : };
329 :
330 :
331 0 : PETRA.NavalManager.prototype.getPlan = function(ID)
332 : {
333 0 : for (let plan of this.transportPlans)
334 0 : if (plan.ID === ID)
335 0 : return plan;
336 0 : return undefined;
337 : };
338 :
339 0 : PETRA.NavalManager.prototype.addPlan = function(plan)
340 : {
341 0 : this.transportPlans.push(plan);
342 : };
343 :
344 : /**
345 : * complete already existing plan or create a new one for this requirement
346 : * (many units can then call this separately and end up in the same plan)
347 : * TODO check garrison classes
348 : */
349 0 : PETRA.NavalManager.prototype.requireTransport = function(gameState, ent, startIndex, endIndex, endPos)
350 : {
351 0 : if (!ent.canGarrison())
352 0 : return false;
353 :
354 0 : if (ent.getMetadata(PlayerID, "transport") !== undefined)
355 : {
356 0 : if (this.Config.debug > 0)
357 0 : API3.warn("Petra naval manager error: unit " + ent.id() + " has already required a transport");
358 0 : return false;
359 : }
360 :
361 0 : let plans = [];
362 0 : for (let plan of this.transportPlans)
363 : {
364 0 : if (plan.startIndex != startIndex || plan.endIndex != endIndex || plan.state !== PETRA.TransportPlan.BOARDING)
365 0 : continue;
366 : // Limit the number of siege units per transport to avoid problems when ungarrisoning
367 0 : if (PETRA.isSiegeUnit(ent) && plan.units.filter(unit => PETRA.isSiegeUnit(unit)).length > 3)
368 0 : continue;
369 0 : plans.push(plan);
370 : }
371 :
372 0 : if (plans.length)
373 : {
374 0 : plans.sort(plan => plan.units.length);
375 0 : plans[0].addUnit(ent, endPos);
376 0 : return true;
377 : }
378 :
379 0 : let plan = new PETRA.TransportPlan(gameState, [ent], startIndex, endIndex, endPos);
380 0 : if (plan.failed)
381 : {
382 0 : if (this.Config.debug > 1)
383 0 : API3.warn(">>>> transport plan aborted <<<<");
384 0 : return false;
385 : }
386 0 : plan.init(gameState);
387 0 : this.transportPlans.push(plan);
388 0 : return true;
389 : };
390 :
391 : /** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */
392 0 : PETRA.NavalManager.prototype.splitTransport = function(gameState, plan)
393 : {
394 0 : if (this.Config.debug > 1)
395 0 : API3.warn(">>>> split of transport plan started <<<<");
396 0 : let newplan = new PETRA.TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos);
397 0 : if (newplan.failed)
398 : {
399 0 : if (this.Config.debug > 1)
400 0 : API3.warn(">>>> split of transport plan aborted <<<<");
401 0 : return false;
402 : }
403 0 : newplan.init(gameState);
404 :
405 0 : for (let ent of plan.needSplit)
406 : {
407 0 : if (ent.getMetadata(PlayerID, "onBoard")) // Should never happen.
408 0 : continue;
409 0 : newplan.addUnit(ent, ent.getMetadata(PlayerID, "endPos"));
410 0 : plan.units.updateEnt(ent);
411 : }
412 :
413 0 : if (newplan.units.length)
414 0 : this.transportPlans.push(newplan);
415 0 : return newplan.units.length != 0;
416 : };
417 :
418 : /**
419 : * create a transport from a garrisoned ship to a land location
420 : * needed at start game when starting with a garrisoned ship
421 : */
422 0 : PETRA.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess)
423 : {
424 0 : let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos);
425 0 : if (fromAccess !== 1)
426 0 : return;
427 0 : if (toAccess < 2)
428 0 : return;
429 :
430 0 : for (let ship of this.ships.values())
431 : {
432 0 : if (!ship.isGarrisonHolder() || !ship.garrisoned().length)
433 0 : continue;
434 0 : if (ship.getMetadata(PlayerID, "transporter") !== undefined)
435 0 : continue;
436 0 : let units = [];
437 0 : for (let entId of ship.garrisoned())
438 0 : units.push(gameState.getEntityById(entId));
439 : // TODO check that the garrisoned units have not another purpose
440 0 : let plan = new PETRA.TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship);
441 0 : if (plan.failed)
442 0 : continue;
443 0 : plan.init(gameState);
444 0 : this.transportPlans.push(plan);
445 : }
446 : };
447 :
448 : // set minimal number of needed ships when a new event (new base or new attack plan)
449 0 : PETRA.NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number)
450 : {
451 0 : if (!sea)
452 0 : return;
453 0 : if (this.wantedTransportShips[sea] < number)
454 0 : this.wantedTransportShips[sea] = number;
455 : };
456 :
457 : // bumps up the number of ships we want if we need more.
458 0 : PETRA.NavalManager.prototype.checkLevels = function(gameState, queues)
459 : {
460 0 : if (queues.ships.hasQueuedUnits())
461 0 : return;
462 :
463 0 : for (let sea = 0; sea < this.neededTransportShips.length; sea++)
464 0 : this.neededTransportShips[sea] = 0;
465 :
466 0 : for (let plan of this.transportPlans)
467 : {
468 0 : if (!plan.needTransportShips || plan.units.length < 2)
469 0 : continue;
470 0 : let sea = plan.sea;
471 0 : if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0 ||
472 : this.seaTransportShips[sea].length < this.wantedTransportShips[sea])
473 0 : continue;
474 0 : ++this.neededTransportShips[sea];
475 0 : if (this.wantedTransportShips[sea] === 0 || this.seaTransportShips[sea].length < plan.transportShips.length + 2)
476 : {
477 0 : ++this.wantedTransportShips[sea];
478 0 : return;
479 : }
480 : }
481 :
482 0 : for (let sea = 0; sea < this.neededTransportShips.length; sea++)
483 0 : if (this.neededTransportShips[sea] > 2)
484 0 : ++this.wantedTransportShips[sea];
485 : };
486 :
487 0 : PETRA.NavalManager.prototype.maintainFleet = function(gameState, queues)
488 : {
489 0 : if (queues.ships.hasQueuedUnits())
490 0 : return;
491 0 : if (!this.docks.filter(API3.Filters.isBuilt()).hasEntities())
492 0 : return;
493 : // check if we have enough transport ships per region.
494 0 : for (let sea = 0; sea < this.seaShips.length; ++sea)
495 : {
496 0 : if (this.seaShips[sea] === undefined)
497 0 : continue;
498 0 : if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0)
499 0 : continue;
500 :
501 0 : if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea])
502 : {
503 0 : let template = this.getBestShip(gameState, sea, "transport");
504 0 : if (template)
505 : {
506 0 : queues.ships.addPlan(new PETRA.TrainingPlan(gameState, template, { "sea": sea }, 1, 1));
507 0 : continue;
508 : }
509 : }
510 :
511 :
512 0 : if (this.seaFishShips[sea].length < this.wantedFishShips[sea])
513 : {
514 0 : let template = this.getBestShip(gameState, sea, "fishing");
515 0 : if (template)
516 : {
517 0 : queues.ships.addPlan(new PETRA.TrainingPlan(gameState, template, { "base": 0, "role": PETRA.Worker.ROLE_WORKER, "sea": sea }, 1, 1));
518 0 : continue;
519 : }
520 : }
521 : }
522 : };
523 :
524 : /** assigns free ships to plans that need some */
525 0 : PETRA.NavalManager.prototype.assignShipsToPlans = function(gameState)
526 : {
527 0 : for (let plan of this.transportPlans)
528 0 : if (plan.needTransportShips)
529 0 : plan.assignShip(gameState);
530 : };
531 :
532 : /** Return true if this ship is likeky (un)garrisoning units */
533 0 : PETRA.NavalManager.prototype.isShipBoarding = function(ship)
534 : {
535 0 : if (!ship.position())
536 0 : return false;
537 0 : let plan = this.getPlan(ship.getMetadata(PlayerID, "transporter"));
538 0 : if (!plan || !plan.boardingPos[ship.id()])
539 0 : return false;
540 0 : return API3.SquareVectorDistance(plan.boardingPos[ship.id()], ship.position()) < plan.boardingRange;
541 : };
542 :
543 : /** let blocking ships move apart from active ships (waiting for a better pathfinder)
544 : * TODO Ships entity collections are currently in two parts as the trader ships are dealt with
545 : * in the tradeManager. That should be modified to avoid dupplicating all the code here.
546 : */
547 0 : PETRA.NavalManager.prototype.moveApart = function(gameState)
548 : {
549 0 : let blockedShips = [];
550 0 : let blockedIds = [];
551 :
552 0 : for (let ship of this.ships.values())
553 : {
554 0 : let shipPosition = ship.position();
555 0 : if (!shipPosition)
556 0 : continue;
557 0 : if (ship.getMetadata(PlayerID, "transporter") !== undefined && this.isShipBoarding(ship))
558 0 : continue;
559 :
560 0 : let unitAIState = ship.unitAIState();
561 0 : if (ship.getMetadata(PlayerID, "transporter") !== undefined ||
562 : unitAIState == "INDIVIDUAL.GATHER.APPROACHING" ||
563 : unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING")
564 : {
565 0 : let previousPosition = ship.getMetadata(PlayerID, "previousPosition");
566 0 : if (!previousPosition || previousPosition[0] != shipPosition[0] ||
567 : previousPosition[1] != shipPosition[1])
568 : {
569 0 : ship.setMetadata(PlayerID, "previousPosition", shipPosition);
570 0 : ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn);
571 0 : continue;
572 : }
573 : // New transport ships receive boarding commands only on the following turn.
574 0 : if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2)
575 0 : continue;
576 0 : ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 35);
577 0 : blockedShips.push(ship);
578 0 : blockedIds.push(ship.id());
579 : }
580 0 : else if (ship.isIdle())
581 : {
582 0 : let previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition");
583 0 : if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] ||
584 : previousIdlePosition[1] != shipPosition[1])
585 : {
586 0 : ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition);
587 0 : ship.setMetadata(PlayerID, "stationnary", undefined);
588 0 : continue;
589 : }
590 0 : if (ship.getMetadata(PlayerID, "stationnary"))
591 0 : continue;
592 0 : ship.setMetadata(PlayerID, "stationnary", true);
593 : // Check if there are some treasure around
594 0 : if (PETRA.gatherTreasure(gameState, ship, true))
595 0 : continue;
596 : // Do not stay idle near a dock to not disturb other ships
597 0 : let sea = ship.getMetadata(PlayerID, "sea");
598 0 : for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values())
599 : {
600 0 : if (PETRA.getSeaAccess(gameState, dock) != sea)
601 0 : continue;
602 0 : if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900)
603 0 : continue;
604 0 : ship.moveToRange(dock.position()[0], dock.position()[1], 70, 75);
605 : }
606 :
607 : }
608 : }
609 :
610 0 : for (let ship of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values())
611 : {
612 0 : let shipPosition = ship.position();
613 0 : if (!shipPosition)
614 0 : continue;
615 0 : let role = ship.getMetadata(PlayerID, "role");
616 0 : if (role === undefined || role !== PETRA.Worker.ROLE_TRADER) // already accounted before
617 0 : continue;
618 :
619 0 : let unitAIState = ship.unitAIState();
620 0 : if (unitAIState == "INDIVIDUAL.TRADE.APPROACHINGMARKET")
621 : {
622 0 : let previousPosition = ship.getMetadata(PlayerID, "previousPosition");
623 0 : if (!previousPosition || previousPosition[0] != shipPosition[0] ||
624 : previousPosition[1] != shipPosition[1])
625 : {
626 0 : ship.setMetadata(PlayerID, "previousPosition", shipPosition);
627 0 : ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn);
628 0 : continue;
629 : }
630 : // New transport ships receives boarding commands only on the following turn.
631 0 : if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2)
632 0 : continue;
633 0 : ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 35);
634 0 : blockedShips.push(ship);
635 0 : blockedIds.push(ship.id());
636 : }
637 0 : else if (ship.isIdle())
638 : {
639 0 : let previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition");
640 0 : if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] ||
641 : previousIdlePosition[1] != shipPosition[1])
642 : {
643 0 : ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition);
644 0 : ship.setMetadata(PlayerID, "stationnary", undefined);
645 0 : continue;
646 : }
647 0 : if (ship.getMetadata(PlayerID, "stationnary"))
648 0 : continue;
649 0 : ship.setMetadata(PlayerID, "stationnary", true);
650 : // Check if there are some treasure around
651 0 : if (PETRA.gatherTreasure(gameState, ship, true))
652 0 : continue;
653 : // Do not stay idle near a dock to not disturb other ships
654 0 : let sea = ship.getMetadata(PlayerID, "sea");
655 0 : for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values())
656 : {
657 0 : if (PETRA.getSeaAccess(gameState, dock) != sea)
658 0 : continue;
659 0 : if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900)
660 0 : continue;
661 0 : ship.moveToRange(dock.position()[0], dock.position()[1], 70, 75);
662 : }
663 : }
664 : }
665 :
666 0 : for (let ship of blockedShips)
667 : {
668 0 : let shipPosition = ship.position();
669 0 : let sea = ship.getMetadata(PlayerID, "sea");
670 0 : for (let blockingShip of this.seaShips[sea].values())
671 : {
672 0 : if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position())
673 0 : continue;
674 0 : let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position());
675 0 : let unitAIState = blockingShip.unitAIState();
676 0 : if (blockingShip.getMetadata(PlayerID, "transporter") === undefined &&
677 : unitAIState != "INDIVIDUAL.GATHER.APPROACHING" &&
678 : unitAIState != "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING")
679 : {
680 0 : if (distSquare < 1600)
681 0 : blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45);
682 : }
683 0 : else if (distSquare < 900)
684 0 : blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35);
685 : }
686 :
687 0 : for (let blockingShip of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values())
688 : {
689 0 : if (blockingShip.getMetadata(PlayerID, "sea") != sea)
690 0 : continue;
691 0 : if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position())
692 0 : continue;
693 0 : let role = blockingShip.getMetadata(PlayerID, "role");
694 0 : if (role === undefined || role !== PETRA.Worker.ROLE_TRADER) // already accounted before
695 0 : continue;
696 0 : let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position());
697 0 : let unitAIState = blockingShip.unitAIState();
698 0 : if (unitAIState != "INDIVIDUAL.TRADE.APPROACHINGMARKET")
699 : {
700 0 : if (distSquare < 1600)
701 0 : blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45);
702 : }
703 0 : else if (distSquare < 900)
704 0 : blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35);
705 : }
706 : }
707 : };
708 :
709 0 : PETRA.NavalManager.prototype.buildNavalStructures = function(gameState, queues)
710 : {
711 0 : if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.hasPotentialBase())
712 0 : return;
713 :
714 0 : if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock)
715 : {
716 0 : if (queues.dock.countQueuedUnitsWithClass("Dock") === 0 &&
717 : !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("Dock"), API3.Filters.isFoundation())).hasEntities() &&
718 : gameState.ai.HQ.canBuild(gameState, "structures/{civ}/dock"))
719 : {
720 0 : let dockStarted = false;
721 0 : for (const base of gameState.ai.HQ.baseManagers())
722 : {
723 0 : if (dockStarted)
724 0 : break;
725 0 : if (!base.anchor || base.constructing)
726 0 : continue;
727 0 : let remaining = this.getUnconnectedSeas(gameState, base.accessIndex);
728 0 : for (let sea of remaining)
729 : {
730 0 : if (!gameState.ai.HQ.navalRegions[sea])
731 0 : continue;
732 0 : let wantedLand = {};
733 0 : wantedLand[base.accessIndex] = true;
734 0 : queues.dock.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/dock", { "land": wantedLand, "sea": sea }));
735 0 : dockStarted = true;
736 0 : break;
737 : }
738 : }
739 : }
740 : }
741 :
742 0 : if (gameState.currentPhase() < 2 || gameState.ai.HQ.getAccountedPopulation(gameState) < this.Config.Economy.popPhase2 + 15 ||
743 : queues.militaryBuilding.hasQueuedUnits())
744 0 : return;
745 0 : if (!this.docks.filter(API3.Filters.byClass("Dock")).hasEntities() ||
746 : this.docks.filter(API3.Filters.byClass("Shipyard")).hasEntities())
747 0 : return;
748 : // Use in priority resources to build a Market.
749 0 : if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() &&
750 : gameState.ai.HQ.canBuild(gameState, "structures/{civ}/market"))
751 0 : return;
752 : let template;
753 0 : if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/super_dock"))
754 0 : template = "structures/{civ}/super_dock";
755 0 : else if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/shipyard"))
756 0 : template = "structures/{civ}/shipyard";
757 : else
758 0 : return;
759 0 : let wantedLand = {};
760 0 : for (const base of gameState.ai.HQ.baseManagers())
761 0 : if (base.anchor)
762 0 : wantedLand[base.accessIndex] = true;
763 0 : let sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea");
764 0 : queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea }));
765 : };
766 :
767 : /** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */
768 0 : PETRA.NavalManager.prototype.getBestShip = function(gameState, sea, goal)
769 : {
770 0 : let civ = gameState.getPlayerCiv();
771 0 : let trainableShips = [];
772 0 : gameState.getOwnTrainingFacilities().filter(API3.Filters.byMetadata(PlayerID, "sea", sea)).forEach(function(ent) {
773 0 : let trainables = ent.trainableEntities(civ);
774 0 : for (let trainable of trainables)
775 : {
776 0 : if (gameState.isTemplateDisabled(trainable))
777 0 : continue;
778 0 : let template = gameState.getTemplate(trainable);
779 0 : if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1)
780 0 : trainableShips.push(trainable);
781 : }
782 : });
783 :
784 0 : let best = 0;
785 : let bestShip;
786 0 : let limits = gameState.getEntityLimits();
787 0 : let current = gameState.getEntityCounts();
788 0 : for (let trainable of trainableShips)
789 : {
790 0 : let template = gameState.getTemplate(trainable);
791 0 : if (!template.available(gameState))
792 0 : continue;
793 :
794 0 : let category = template.trainingCategory();
795 0 : if (category && limits[category] && current[category] >= limits[category])
796 0 : continue;
797 :
798 0 : let arrows = +(template.getDefaultArrow() || 0);
799 0 : if (goal === "attack") // choose the maximum default arrows
800 : {
801 0 : if (best > arrows)
802 0 : continue;
803 0 : best = arrows;
804 : }
805 0 : else if (goal === "transport") // choose the maximum capacity, with a bonus if arrows or if siege transport
806 : {
807 0 : let capacity = +(template.garrisonMax() || 0);
808 0 : if (capacity < 2)
809 0 : continue;
810 0 : capacity += 10*arrows;
811 0 : if (MatchesClassList(template.garrisonableClasses(), "Siege"))
812 0 : capacity += 50;
813 0 : if (best > capacity)
814 0 : continue;
815 0 : best = capacity;
816 : }
817 0 : else if (goal === "fishing")
818 0 : if (!template.hasClass("FishingBoat"))
819 0 : continue;
820 0 : bestShip = trainable;
821 : }
822 0 : return bestShip;
823 : };
824 :
825 0 : PETRA.NavalManager.prototype.update = function(gameState, queues, events)
826 : {
827 0 : Engine.ProfileStart("Naval Manager update");
828 :
829 : // close previous transport plans if finished
830 0 : for (let i = 0; i < this.transportPlans.length; ++i)
831 : {
832 0 : let remaining = this.transportPlans[i].update(gameState);
833 0 : if (remaining)
834 0 : continue;
835 0 : if (this.Config.debug > 1)
836 0 : API3.warn("no more units on transport plan " + this.transportPlans[i].ID);
837 0 : this.transportPlans[i].releaseAll();
838 0 : this.transportPlans.splice(i--, 1);
839 : }
840 : // assign free ships to plans which need them
841 0 : this.assignShipsToPlans(gameState);
842 :
843 : // and require for more ships/structures if needed
844 0 : if (gameState.ai.playedTurn % 3 === 0)
845 : {
846 0 : this.checkLevels(gameState, queues);
847 0 : this.maintainFleet(gameState, queues);
848 0 : this.buildNavalStructures(gameState, queues);
849 : }
850 : // let inactive ships move apart from active ones (waiting for a better pathfinder)
851 0 : this.moveApart(gameState);
852 :
853 0 : Engine.ProfileStop();
854 : };
855 :
856 0 : PETRA.NavalManager.prototype.Serialize = function()
857 : {
858 0 : let properties = {
859 : "wantedTransportShips": this.wantedTransportShips,
860 : "wantedWarShips": this.wantedWarShips,
861 : "wantedFishShips": this.wantedFishShips,
862 : "neededTransportShips": this.neededTransportShips,
863 : "neededWarShips": this.neededWarShips,
864 : "landingZones": this.landingZones
865 : };
866 :
867 0 : let transports = {};
868 0 : for (let plan in this.transportPlans)
869 0 : transports[plan] = this.transportPlans[plan].Serialize();
870 :
871 0 : return { "properties": properties, "transports": transports };
872 : };
873 :
874 0 : PETRA.NavalManager.prototype.Deserialize = function(gameState, data)
875 : {
876 0 : for (let key in data.properties)
877 0 : this[key] = data.properties[key];
878 :
879 0 : this.transportPlans = [];
880 0 : for (let i in data.transports)
881 : {
882 0 : let dataPlan = data.transports[i];
883 0 : let plan = new PETRA.TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos);
884 0 : plan.Deserialize(dataPlan);
885 0 : plan.init(gameState);
886 0 : this.transportPlans.push(plan);
887 : }
888 : };
|