Line data Source code
1 : /**
2 : * Manage the garrisonHolders
3 : * When a unit is ordered to garrison, it must be done through this.garrison() function so that
4 : * an object in this.holders is created. This object contains an array with the entities
5 : * in the process of being garrisoned. To have all garrisoned units, we must add those in holder.garrisoned().
6 : * Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...)
7 : */
8 :
9 0 : PETRA.GarrisonManager = function(Config)
10 : {
11 0 : this.Config = Config;
12 0 : this.holders = new Map();
13 0 : this.decayingStructures = new Map();
14 : };
15 :
16 0 : PETRA.GarrisonManager.TYPE_FORCE = "force";
17 0 : PETRA.GarrisonManager.TYPE_TRADE = "trade";
18 0 : PETRA.GarrisonManager.TYPE_PROTECTION = "protection";
19 0 : PETRA.GarrisonManager.TYPE_DECAY = "decay";
20 0 : PETRA.GarrisonManager.TYPE_EMERGENCY = "emergency";
21 :
22 0 : PETRA.GarrisonManager.prototype.update = function(gameState, events)
23 : {
24 : // First check for possible upgrade of a structure
25 0 : for (let evt of events.EntityRenamed)
26 : {
27 0 : for (let id of this.holders.keys())
28 : {
29 0 : if (id != evt.entity)
30 0 : continue;
31 0 : let data = this.holders.get(id);
32 0 : let newHolder = gameState.getEntityById(evt.newentity);
33 0 : if (newHolder && newHolder.isGarrisonHolder())
34 : {
35 0 : this.holders.delete(id);
36 0 : this.holders.set(evt.newentity, data);
37 : }
38 : else
39 : {
40 0 : for (let entId of data.list)
41 : {
42 0 : let ent = gameState.getEntityById(entId);
43 0 : if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id)
44 0 : continue;
45 0 : this.leaveGarrison(ent);
46 0 : ent.stopMoving();
47 : }
48 0 : this.holders.delete(id);
49 : }
50 : }
51 :
52 0 : for (let id of this.decayingStructures.keys())
53 : {
54 0 : if (id !== evt.entity)
55 0 : continue;
56 0 : this.decayingStructures.delete(id);
57 0 : if (this.decayingStructures.has(evt.newentity))
58 0 : continue;
59 0 : let ent = gameState.getEntityById(evt.newentity);
60 0 : if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate())
61 0 : continue;
62 0 : let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate());
63 0 : this.decayingStructures.set(evt.newentity, gmin);
64 : }
65 : }
66 :
67 0 : for (let [id, data] of this.holders.entries())
68 : {
69 0 : let list = data.list;
70 0 : let holder = gameState.getEntityById(id);
71 0 : if (!holder || !gameState.isPlayerAlly(holder.owner()))
72 : {
73 : // this holder was certainly destroyed or captured. Let's remove it
74 0 : for (let entId of list)
75 : {
76 0 : let ent = gameState.getEntityById(entId);
77 0 : if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id)
78 0 : continue;
79 0 : this.leaveGarrison(ent);
80 0 : ent.stopMoving();
81 : }
82 0 : this.holders.delete(id);
83 0 : continue;
84 : }
85 :
86 : // Update the list of garrisoned units
87 0 : for (let j = 0; j < list.length; ++j)
88 : {
89 0 : for (let evt of events.EntityRenamed)
90 0 : if (evt.entity === list[j])
91 0 : list[j] = evt.newentity;
92 :
93 0 : let ent = gameState.getEntityById(list[j]);
94 0 : if (!ent) // unit must have been killed while garrisoning
95 0 : list.splice(j--, 1);
96 0 : else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned
97 : {
98 0 : this.leaveGarrison(ent);
99 0 : list.splice(j--, 1);
100 : }
101 : else
102 : {
103 0 : if (ent.unitAIOrderData().some(order => order.target && order.target == id))
104 0 : continue;
105 0 : if (ent.getMetadata(PlayerID, "garrisonHolder") == id)
106 : {
107 : // The garrison order must have failed
108 0 : this.leaveGarrison(ent);
109 0 : list.splice(j--, 1);
110 : }
111 : else
112 : {
113 0 : if (gameState.ai.Config.debug > 0)
114 : {
115 0 : API3.warn("Petra garrison error: unit " + ent.id() + " (" + ent.genericName() +
116 : ") is expected to garrison in " + id + " (" + holder.genericName() +
117 : "), but has no such garrison order " + uneval(ent.unitAIOrderData()));
118 0 : PETRA.dumpEntity(ent);
119 : }
120 0 : list.splice(j--, 1);
121 : }
122 : }
123 :
124 : }
125 :
126 0 : if (!holder.position()) // could happen with siege unit inside a ship
127 0 : continue;
128 :
129 0 : if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3)
130 : {
131 0 : let range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80;
132 0 : let around = { "defenseStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false };
133 0 : for (let ent of gameState.getEnemyEntities().values())
134 : {
135 0 : if (ent.hasClass("Structure"))
136 : {
137 0 : if (!ent.attackRange("Ranged"))
138 0 : continue;
139 : }
140 0 : else if (ent.hasClass("Unit"))
141 : {
142 0 : if (ent.owner() == 0 && (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT"))
143 0 : continue;
144 : }
145 : else
146 0 : continue;
147 0 : if (!ent.position())
148 0 : continue;
149 0 : let dist = API3.SquareVectorDistance(ent.position(), holder.position());
150 0 : if (dist > range*range)
151 0 : continue;
152 0 : if (ent.hasClass("Structure"))
153 0 : around.defenseStructure = true;
154 0 : else if (PETRA.isSiegeUnit(ent))
155 : {
156 0 : if (ent.attackTypes().indexOf("Melee") !== -1)
157 0 : around.meleeSiege = true;
158 : else
159 0 : around.rangeSiege = true;
160 : }
161 : else
162 : {
163 0 : around.unit = true;
164 0 : break;
165 : }
166 : }
167 : // Keep defenseManager.garrisonUnitsInside in sync to avoid garrisoning-ungarrisoning some units
168 0 : data.allowMelee = around.defenseStructure || around.unit;
169 :
170 0 : for (let entId of holder.garrisoned())
171 : {
172 0 : let ent = gameState.getEntityById(entId);
173 0 : if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around))
174 0 : holder.unload(entId);
175 : }
176 0 : for (let j = 0; j < list.length; ++j)
177 : {
178 0 : let ent = gameState.getEntityById(list[j]);
179 0 : if (this.keepGarrisoned(ent, holder, around))
180 0 : continue;
181 0 : if (ent.getMetadata(PlayerID, "garrisonHolder") == id)
182 : {
183 0 : this.leaveGarrison(ent);
184 0 : ent.stopMoving();
185 : }
186 0 : list.splice(j--, 1);
187 : }
188 0 : if (this.numberOfGarrisonedSlots(holder) === 0)
189 0 : this.holders.delete(id);
190 : else
191 0 : holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime);
192 : }
193 : }
194 :
195 : // Warning new garrison orders (as in the following lines) should be done after having updated the holders
196 : // (or TODO we should add a test that the garrison order is from a previous turn when updating)
197 0 : for (let [id, gmin] of this.decayingStructures.entries())
198 : {
199 0 : let ent = gameState.getEntityById(id);
200 0 : if (!ent || ent.owner() !== PlayerID)
201 0 : this.decayingStructures.delete(id);
202 0 : else if (this.numberOfGarrisonedSlots(ent) < gmin)
203 0 : gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": PETRA.GarrisonManager.TYPE_DECAY });
204 : }
205 : };
206 :
207 : /** TODO should add the units garrisoned inside garrisoned units */
208 0 : PETRA.GarrisonManager.prototype.numberOfGarrisonedUnits = function(holder)
209 : {
210 0 : if (!this.holders.has(holder.id()))
211 0 : return holder.garrisoned().length;
212 :
213 0 : return holder.garrisoned().length + this.holders.get(holder.id()).list.length;
214 : };
215 :
216 : /** TODO should add the units garrisoned inside garrisoned units */
217 0 : PETRA.GarrisonManager.prototype.numberOfGarrisonedSlots = function(holder)
218 : {
219 0 : if (!this.holders.has(holder.id()))
220 0 : return holder.garrisonedSlots();
221 :
222 0 : return holder.garrisonedSlots() + this.holders.get(holder.id()).list.length;
223 : };
224 :
225 0 : PETRA.GarrisonManager.prototype.allowMelee = function(holder)
226 : {
227 0 : if (!this.holders.has(holder.id()))
228 0 : return undefined;
229 :
230 0 : return this.holders.get(holder.id()).allowMelee;
231 : };
232 :
233 : /** This is just a pre-garrison state, while the entity walk to the garrison holder */
234 0 : PETRA.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type)
235 : {
236 0 : if (this.numberOfGarrisonedSlots(holder) >= holder.garrisonMax() || !ent.canGarrison())
237 0 : return;
238 :
239 0 : this.registerHolder(gameState, holder);
240 0 : this.holders.get(holder.id()).list.push(ent.id());
241 :
242 0 : if (gameState.ai.Config.debug > 2)
243 : {
244 0 : warn("garrison unit " + ent.genericName() + " in " + holder.genericName() + " with type " + type);
245 0 : warn(" we try to garrison a unit with plan " + ent.getMetadata(PlayerID, "plan") + " and role " + ent.getMetadata(PlayerID, "role") +
246 : " and subrole " + ent.getMetadata(PlayerID, "subrole") + " and transport " + ent.getMetadata(PlayerID, "transport"));
247 : }
248 :
249 0 : if (ent.getMetadata(PlayerID, "plan") !== undefined)
250 0 : ent.setMetadata(PlayerID, "plan", -2);
251 : else
252 0 : ent.setMetadata(PlayerID, "plan", -3);
253 0 : ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_GARRISONING);
254 0 : ent.setMetadata(PlayerID, "garrisonHolder", holder.id());
255 0 : ent.setMetadata(PlayerID, "garrisonType", type);
256 0 : ent.garrison(holder);
257 : };
258 :
259 : /**
260 : This is the end of the pre-garrison state, either because the entity is really garrisoned
261 : or because it has changed its order (i.e. because the garrisonHolder was destroyed)
262 : This function is for internal use inside garrisonManager. From outside, you should also update
263 : the holder and then using cancelGarrison should be the preferred solution
264 : */
265 0 : PETRA.GarrisonManager.prototype.leaveGarrison = function(ent)
266 : {
267 0 : ent.setMetadata(PlayerID, "subrole", undefined);
268 0 : if (ent.getMetadata(PlayerID, "plan") === -2)
269 0 : ent.setMetadata(PlayerID, "plan", -1);
270 : else
271 0 : ent.setMetadata(PlayerID, "plan", undefined);
272 0 : ent.setMetadata(PlayerID, "garrisonHolder", undefined);
273 : };
274 :
275 : /** Cancel a pre-garrison state */
276 0 : PETRA.GarrisonManager.prototype.cancelGarrison = function(ent)
277 : {
278 0 : ent.stopMoving();
279 0 : this.leaveGarrison(ent);
280 0 : let holderId = ent.getMetadata(PlayerID, "garrisonHolder");
281 0 : if (!holderId || !this.holders.has(holderId))
282 0 : return;
283 0 : let list = this.holders.get(holderId).list;
284 0 : let index = list.indexOf(ent.id());
285 0 : if (index !== -1)
286 0 : list.splice(index, 1);
287 : };
288 :
289 0 : PETRA.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around)
290 : {
291 0 : switch (ent.getMetadata(PlayerID, "garrisonType"))
292 : {
293 : case PETRA.GarrisonManager.TYPE_FORCE: // force the ungarrisoning
294 0 : return false;
295 : case PETRA.GarrisonManager.TYPE_TRADE: // trader garrisoned in ship
296 0 : return true;
297 : case PETRA.GarrisonManager.TYPE_PROTECTION: // hurt unit for healing or infantry for defense
298 0 : if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high)
299 0 : return true;
300 0 : let capture = ent.capturePoints();
301 0 : if (capture && capture[PlayerID] / capture.reduce((a, b) => a + b) < 0.8)
302 0 : return true;
303 0 : if (ent.hasClasses(holder.getGarrisonArrowClasses()))
304 : {
305 0 : if (around.unit || around.defenseStructure)
306 0 : return true;
307 0 : if (around.meleeSiege || around.rangeSiege)
308 0 : return ent.attackTypes().indexOf("Melee") === -1 || ent.healthLevel() < this.Config.garrisonHealthLevel.low;
309 0 : return false;
310 : }
311 0 : if (ent.attackTypes() && ent.attackTypes().indexOf("Melee") !== -1)
312 0 : return false;
313 0 : if (around.unit)
314 0 : return ent.hasClass("Support") || PETRA.isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above
315 0 : if (PETRA.isSiegeUnit(ent))
316 0 : return around.meleeSiege;
317 0 : return holder.buffHeal() && ent.needsHeal();
318 : case PETRA.GarrisonManager.TYPE_DECAY:
319 0 : return ent.captureStrength() && this.decayingStructures.has(holder.id());
320 : case PETRA.GarrisonManager.TYPE_EMERGENCY: // f.e. hero in regicide mode
321 0 : if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high)
322 0 : return true;
323 0 : if (around.unit || around.defenseStructure || around.meleeSiege ||
324 : around.rangeSiege && ent.healthLevel() < this.Config.garrisonHealthLevel.high)
325 0 : return true;
326 0 : return holder.buffHeal() && ent.needsHeal();
327 : default:
328 0 : if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager
329 0 : return true;
330 0 : API3.warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") +
331 : " for " + ent.genericName() + " id " + ent.id() +
332 : " inside " + holder.genericName() + " id " + holder.id());
333 0 : ent.setMetadata(PlayerID, "garrisonType", PETRA.GarrisonManager.TYPE_PROTECTION);
334 0 : return true;
335 : }
336 : };
337 :
338 : /** Add this holder in the list managed by the garrisonManager */
339 0 : PETRA.GarrisonManager.prototype.registerHolder = function(gameState, holder)
340 : {
341 0 : if (this.holders.has(holder.id())) // already registered
342 0 : return;
343 0 : this.holders.set(holder.id(), { "list": [], "allowMelee": true });
344 0 : holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime);
345 : };
346 :
347 : /**
348 : * Garrison units in decaying structures to stop their decay
349 : * do it only for structures useful for defense, except if we are expanding (justCaptured=true)
350 : * in which case we also do it for structures useful for unit trainings (TODO only Barracks are done)
351 : */
352 0 : PETRA.GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, justCaptured)
353 : {
354 0 : if (this.decayingStructures.has(entId))
355 0 : return true;
356 0 : let ent = gameState.getEntityById(entId);
357 0 : if (!ent || !(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire())
358 0 : return false;
359 0 : if (!ent.territoryDecayRate() || !ent.garrisonRegenRate())
360 0 : return false;
361 0 : let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate());
362 0 : this.decayingStructures.set(entId, gmin);
363 0 : return true;
364 : };
365 :
366 0 : PETRA.GarrisonManager.prototype.removeDecayingStructure = function(entId)
367 : {
368 0 : if (!this.decayingStructures.has(entId))
369 0 : return;
370 0 : this.decayingStructures.delete(entId);
371 : };
372 :
373 0 : PETRA.GarrisonManager.prototype.Serialize = function()
374 : {
375 0 : return { "holders": this.holders, "decayingStructures": this.decayingStructures };
376 : };
377 :
378 0 : PETRA.GarrisonManager.prototype.Deserialize = function(data)
379 : {
380 0 : for (let key in data)
381 0 : this[key] = data[key];
382 : };
|