Line data Source code
1 : function GarrisonHolder() {}
2 :
3 2 : GarrisonHolder.prototype.Schema =
4 : "<element name='Max' a:help='Maximum number of entities which can be garrisoned in this holder'>" +
5 : "<data type='positiveInteger'/>" +
6 : "</element>" +
7 : "<element name='List' a:help='Classes of entities which are allowed to garrison in this holder (from Identity)'>" +
8 : "<attribute name='datatype'>" +
9 : "<value>tokens</value>" +
10 : "</attribute>" +
11 : "<text/>" +
12 : "</element>" +
13 : "<element name='EjectClassesOnDestroy' a:help='Classes of entities to be ejected on destroy. Others are killed'>" +
14 : "<attribute name='datatype'>" +
15 : "<value>tokens</value>" +
16 : "</attribute>" +
17 : "<text/>" +
18 : "</element>" +
19 : "<element name='BuffHeal' a:help='Number of hitpoints that will be restored to this holder's garrisoned units each second'>" +
20 : "<ref name='nonNegativeDecimal'/>" +
21 : "</element>" +
22 : "<element name='LoadingRange' a:help='The maximum distance from this holder at which entities are allowed to garrison. Should be about 2.0 for land entities and preferably greater for ships'>" +
23 : "<ref name='nonNegativeDecimal'/>" +
24 : "</element>" +
25 : "<optional>" +
26 : "<element name='EjectHealth' a:help='Percentage of maximum health below which this holder no longer allows garrisoning'>" +
27 : "<ref name='nonNegativeDecimal'/>" +
28 : "</element>" +
29 : "</optional>" +
30 : "<optional>" +
31 : "<element name='Pickup' a:help='This garrisonHolder will move to pick up units to be garrisoned'>" +
32 : "<data type='boolean'/>" +
33 : "</element>" +
34 : "</optional>";
35 :
36 : /**
37 : * Time between heals.
38 : */
39 2 : GarrisonHolder.prototype.HEAL_TIMEOUT = 1000;
40 :
41 : /**
42 : * Initialize GarrisonHolder Component
43 : * Garrisoning when loading a map is set in the script of the map, by setting initGarrison
44 : * which should contain the array of garrisoned entities.
45 : */
46 2 : GarrisonHolder.prototype.Init = function()
47 : {
48 4 : this.entities = [];
49 4 : this.allowedClasses = ApplyValueModificationsToEntity("GarrisonHolder/List/_string", this.template.List._string, this.entity);
50 : };
51 :
52 : /**
53 : * @param {number} entity - The entity to verify.
54 : * @return {boolean} - Whether the given entity is garrisoned in this GarrisonHolder.
55 : */
56 2 : GarrisonHolder.prototype.IsGarrisoned = function(entity)
57 : {
58 0 : return this.entities.indexOf(entity) != -1;
59 : };
60 :
61 : /**
62 : * @return {Object} max and min range at which entities can garrison the holder.
63 : */
64 2 : GarrisonHolder.prototype.LoadingRange = function()
65 : {
66 1 : return { "max": +this.template.LoadingRange, "min": 0 };
67 : };
68 :
69 2 : GarrisonHolder.prototype.CanPickup = function(ent)
70 : {
71 4 : if (!this.template.Pickup || this.IsFull())
72 4 : return false;
73 0 : let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership);
74 0 : return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent);
75 : };
76 :
77 2 : GarrisonHolder.prototype.GetEntities = function()
78 : {
79 16 : return this.entities;
80 : };
81 :
82 : /**
83 : * @return {Array} unit classes which can be garrisoned inside this
84 : * particular entity. Obtained from the entity's template.
85 : */
86 2 : GarrisonHolder.prototype.GetAllowedClasses = function()
87 : {
88 2 : return this.allowedClasses;
89 : };
90 :
91 2 : GarrisonHolder.prototype.GetCapacity = function()
92 : {
93 57 : return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity);
94 : };
95 :
96 2 : GarrisonHolder.prototype.IsFull = function()
97 : {
98 7 : return this.OccupiedSlots() >= this.GetCapacity();
99 : };
100 :
101 2 : GarrisonHolder.prototype.GetHealRate = function()
102 : {
103 5 : return ApplyValueModificationsToEntity("GarrisonHolder/BuffHeal", +this.template.BuffHeal, this.entity);
104 : };
105 :
106 : /**
107 : * Set this entity to allow or disallow garrisoning in the entity.
108 : * Every component calling this function should do it with its own ID, and as long as one
109 : * component doesn't allow this entity to garrison, it can't be garrisoned
110 : * When this entity already contains garrisoned soldiers,
111 : * these will not be able to ungarrison until the flag is set to true again.
112 : *
113 : * This more useful for modern-day features. For example you can't garrison or ungarrison
114 : * a driving vehicle or plane.
115 : * @param {boolean} allow - Whether the entity should be garrisonable.
116 : */
117 2 : GarrisonHolder.prototype.AllowGarrisoning = function(allow, callerID)
118 : {
119 3 : if (!this.allowGarrisoning)
120 1 : this.allowGarrisoning = new Map();
121 3 : this.allowGarrisoning.set(callerID, allow);
122 : };
123 :
124 : /**
125 : * @return {boolean} - Whether (un)garrisoning is allowed.
126 : */
127 2 : GarrisonHolder.prototype.IsGarrisoningAllowed = function()
128 : {
129 87 : return !this.allowGarrisoning ||
130 16 : Array.from(this.allowGarrisoning.values()).every(allow => allow);
131 : };
132 :
133 2 : GarrisonHolder.prototype.GetGarrisonedEntitiesCount = function()
134 : {
135 10 : let count = this.entities.length;
136 10 : for (let ent of this.entities)
137 : {
138 18 : let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
139 18 : if (cmpGarrisonHolder)
140 0 : count += cmpGarrisonHolder.GetGarrisonedEntitiesCount();
141 : }
142 10 : return count;
143 : };
144 :
145 2 : GarrisonHolder.prototype.OccupiedSlots = function()
146 : {
147 56 : let count = 0;
148 56 : for (let ent of this.entities)
149 : {
150 160 : let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
151 160 : if (cmpGarrisonable)
152 160 : count += cmpGarrisonable.TotalSize();
153 : }
154 56 : return count;
155 : };
156 :
157 2 : GarrisonHolder.prototype.IsAllowedToGarrison = function(entity)
158 : {
159 51 : if (!this.IsGarrisoningAllowed())
160 1 : return false;
161 :
162 50 : let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable);
163 50 : if (!cmpGarrisonable || this.OccupiedSlots() + cmpGarrisonable.TotalSize() > this.GetCapacity())
164 5 : return false;
165 :
166 45 : return this.IsAllowedToBeGarrisoned(entity);
167 : };
168 :
169 2 : GarrisonHolder.prototype.IsAllowedToBeGarrisoned = function(entity)
170 : {
171 46 : if (!IsOwnedByMutualAllyOfEntity(entity, this.entity))
172 3 : return false;
173 :
174 43 : let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
175 43 : return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), this.allowedClasses);
176 : };
177 :
178 : /**
179 : * @param {number} entity - The entityID to garrison.
180 : * @return {boolean} - Whether the entity was garrisoned.
181 : */
182 2 : GarrisonHolder.prototype.Garrison = function(entity)
183 : {
184 42 : if (!this.IsAllowedToGarrison(entity))
185 8 : return false;
186 :
187 34 : if (!this.HasEnoughHealth())
188 1 : return false;
189 :
190 33 : if (!this.timer && this.GetHealRate())
191 4 : this.StartTimer();
192 :
193 33 : this.entities.push(entity);
194 33 : this.UpdateGarrisonFlag();
195 :
196 33 : Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {
197 : "added": [entity],
198 : "removed": []
199 : });
200 :
201 33 : return true;
202 : };
203 :
204 : /**
205 : * @param {number} entity - The entity ID of the entity to eject.
206 : * @param {boolean} forced - Whether eject is forced (e.g. if building is destroyed).
207 : * @return {boolean} Whether the entity was ejected.
208 : */
209 2 : GarrisonHolder.prototype.Eject = function(entity, forced)
210 : {
211 34 : if (!this.IsGarrisoningAllowed() && !forced)
212 1 : return false;
213 :
214 33 : let entityIndex = this.entities.indexOf(entity);
215 : // Error: invalid entity ID, usually it's already been ejected, assume success.
216 33 : if (entityIndex == -1)
217 4 : return true;
218 :
219 29 : this.entities.splice(entityIndex, 1);
220 29 : this.UpdateGarrisonFlag();
221 29 : Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {
222 : "added": [],
223 : "removed": [entity]
224 : });
225 :
226 29 : return true;
227 : };
228 :
229 : /**
230 : * Tell unit to unload from this entity.
231 : * @param {number} entity - The entity to unload.
232 : * @return {boolean} Whether the command was successful.
233 : */
234 2 : GarrisonHolder.prototype.Unload = function(entity)
235 : {
236 31 : let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable);
237 31 : return cmpGarrisonable && cmpGarrisonable.UnGarrison();
238 : };
239 :
240 : /**
241 : * Tell units to unload from this entity.
242 : * @param {number[]} entities - The entities to unload.
243 : * @return {boolean} - Whether all unloads were successful.
244 : */
245 2 : GarrisonHolder.prototype.UnloadEntities = function(entities)
246 : {
247 7 : let success = true;
248 7 : for (let entity of entities)
249 21 : if (!this.Unload(entity))
250 0 : success = false;
251 7 : return success;
252 : };
253 :
254 : /**
255 : * Unload one or all units that match a template and owner from us.
256 : * @param {string} template - Type of units that should be ejected.
257 : * @param {number} owner - Id of the player whose units should be ejected.
258 : * @param {boolean} all - Whether all units should be ejected.
259 : * @return {boolean} Whether the unloading was successful.
260 : */
261 2 : GarrisonHolder.prototype.UnloadTemplate = function(template, owner, all)
262 : {
263 2 : let entities = [];
264 2 : let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
265 2 : for (let entity of this.entities)
266 : {
267 20 : let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
268 :
269 : // Units with multiple ranks are grouped together.
270 20 : let name = cmpIdentity.GetSelectionGroupName() || cmpTemplateManager.GetCurrentTemplateName(entity);
271 20 : if (name != template || owner != Engine.QueryInterface(entity, IID_Ownership).GetOwner())
272 18 : continue;
273 :
274 2 : entities.push(entity);
275 :
276 : // If 'all' is false, only ungarrison the first matched unit.
277 2 : if (!all)
278 2 : break;
279 : }
280 :
281 2 : return this.UnloadEntities(entities);
282 : };
283 :
284 : /**
285 : * Unload all units, that belong to certain player
286 : * and order all own units to move to the rally point.
287 : * @param {number} owner - Id of the player whose units should be ejected.
288 : * @return {boolean} Whether the unloading was successful.
289 : */
290 2 : GarrisonHolder.prototype.UnloadAllByOwner = function(owner)
291 : {
292 2 : let entities = this.entities.filter(ent => {
293 18 : let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
294 18 : return cmpOwnership && cmpOwnership.GetOwner() == owner;
295 : });
296 2 : return this.UnloadEntities(entities);
297 : };
298 :
299 : /**
300 : * Unload all units from the entity and order them to move to the rally point.
301 : * @return {boolean} Whether the unloading was successful.
302 : */
303 2 : GarrisonHolder.prototype.UnloadAll = function()
304 : {
305 3 : return this.UnloadEntities(this.entities.slice());
306 : };
307 :
308 : /**
309 : * Used to check if the garrisoning entity's health has fallen below
310 : * a certain limit after which all garrisoned units are unloaded.
311 : */
312 2 : GarrisonHolder.prototype.OnHealthChanged = function(msg)
313 : {
314 0 : if (!this.HasEnoughHealth() && this.entities.length)
315 0 : this.EjectOrKill(this.entities.slice());
316 : };
317 :
318 2 : GarrisonHolder.prototype.HasEnoughHealth = function()
319 : {
320 : // 0 is a valid value so explicitly check for undefined.
321 37 : if (this.template.EjectHealth === undefined)
322 13 : return true;
323 :
324 24 : let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
325 24 : return !cmpHealth || cmpHealth.GetHitpoints() > Math.floor(+this.template.EjectHealth * cmpHealth.GetMaxHitpoints());
326 : };
327 :
328 2 : GarrisonHolder.prototype.StartTimer = function()
329 : {
330 4 : if (this.timer)
331 0 : return;
332 4 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
333 4 : this.timer = cmpTimer.SetInterval(this.entity, IID_GarrisonHolder, "HealTimeout", this.HEAL_TIMEOUT, this.HEAL_TIMEOUT, null);
334 : };
335 :
336 2 : GarrisonHolder.prototype.StopTimer = function()
337 : {
338 0 : if (!this.timer)
339 0 : return;
340 0 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
341 0 : cmpTimer.CancelTimer(this.timer);
342 0 : delete this.timer;
343 : };
344 :
345 : /**
346 : * @params data and lateness are unused.
347 : */
348 2 : GarrisonHolder.prototype.HealTimeout = function(data, lateness)
349 : {
350 0 : let healRate = this.GetHealRate();
351 0 : if (!this.entities.length || !healRate)
352 : {
353 0 : this.StopTimer();
354 0 : return;
355 : }
356 :
357 0 : for (let entity of this.entities)
358 : {
359 0 : let cmpHealth = Engine.QueryInterface(entity, IID_Health);
360 0 : if (cmpHealth && !cmpHealth.IsUnhealable())
361 0 : cmpHealth.Increase(healRate);
362 : }
363 : };
364 :
365 : /**
366 : * Updates the garrison flag depending whether something is garrisoned in the entity.
367 : */
368 2 : GarrisonHolder.prototype.UpdateGarrisonFlag = function()
369 : {
370 65 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
371 65 : if (!cmpVisual)
372 65 : return;
373 :
374 0 : cmpVisual.SetVariant("garrison", this.entities.length ? "garrisoned" : "ungarrisoned");
375 : };
376 :
377 : /**
378 : * Cancel timer when destroyed.
379 : */
380 2 : GarrisonHolder.prototype.OnDestroy = function()
381 : {
382 0 : if (this.timer)
383 : {
384 0 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
385 0 : cmpTimer.CancelTimer(this.timer);
386 : }
387 : };
388 :
389 : /**
390 : * If a garrisoned entity is captured, or about to be killed (so its owner changes to '-1'),
391 : * remove it from the building so we only ever contain valid entities.
392 : */
393 2 : GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg)
394 : {
395 : // The ownership change may be on the garrisonholder
396 1 : if (this.entity == msg.entity)
397 : {
398 0 : let entities = this.entities.filter(ent => msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, ent));
399 :
400 0 : if (entities.length)
401 0 : this.EjectOrKill(entities);
402 :
403 0 : return;
404 : }
405 :
406 : // or on some of its garrisoned units
407 1 : let entityIndex = this.entities.indexOf(msg.entity);
408 1 : if (entityIndex != -1 && (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity)))
409 1 : this.EjectOrKill([msg.entity]);
410 : };
411 :
412 : /**
413 : * Update list of garrisoned entities when a game inits.
414 : */
415 2 : GarrisonHolder.prototype.OnGlobalSkirmishReplacerReplaced = function(msg)
416 : {
417 0 : if (!this.initGarrison)
418 0 : return;
419 :
420 0 : if (msg.entity == this.entity)
421 : {
422 0 : let cmpGarrisonHolder = Engine.QueryInterface(msg.newentity, IID_GarrisonHolder);
423 0 : if (cmpGarrisonHolder)
424 0 : cmpGarrisonHolder.initGarrison = this.initGarrison;
425 : }
426 : else
427 : {
428 0 : let entityIndex = this.initGarrison.indexOf(msg.entity);
429 0 : if (entityIndex != -1)
430 0 : this.initGarrison[entityIndex] = msg.newentity;
431 : }
432 : };
433 :
434 : /**
435 : * Eject all foreign garrisoned entities which are no more allied.
436 : */
437 2 : GarrisonHolder.prototype.OnDiplomacyChanged = function()
438 : {
439 4 : this.EjectOrKill(this.entities.filter(ent => !IsOwnedByMutualAllyOfEntity(this.entity, ent)));
440 : };
441 :
442 : /**
443 : * Eject or kill a garrisoned unit which can no more be garrisoned
444 : * (garrisonholder's health too small or ownership changed).
445 : */
446 2 : GarrisonHolder.prototype.EjectOrKill = function(entities)
447 : {
448 3 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
449 : // Eject the units which can be ejected (if not in world, it generally means this holder
450 : // is inside a holder which kills its entities, so do not eject)
451 3 : if (cmpPosition && cmpPosition.IsInWorld())
452 : {
453 0 : let ejectables = entities.filter(ent => this.IsEjectable(ent));
454 0 : if (ejectables.length)
455 0 : this.UnloadEntities(ejectables);
456 : }
457 :
458 : // And destroy all remaining entities
459 3 : let killedEntities = [];
460 3 : for (let entity of entities)
461 : {
462 3 : let entityIndex = this.entities.indexOf(entity);
463 3 : if (entityIndex == -1)
464 0 : continue;
465 3 : let cmpHealth = Engine.QueryInterface(entity, IID_Health);
466 3 : if (cmpHealth)
467 0 : cmpHealth.Kill();
468 : else
469 3 : Engine.DestroyEntity(entity);
470 3 : this.entities.splice(entityIndex, 1);
471 3 : killedEntities.push(entity);
472 : }
473 :
474 3 : if (killedEntities.length)
475 : {
476 3 : Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {
477 : "added": [],
478 : "removed": killedEntities
479 : });
480 3 : this.UpdateGarrisonFlag();
481 : }
482 : };
483 :
484 : /**
485 : * Whether an entity is ejectable.
486 : * @param {number} entity - The entity-ID to be tested.
487 : * @return {boolean} - Whether the entity is ejectable.
488 : */
489 2 : GarrisonHolder.prototype.IsEjectable = function(entity)
490 : {
491 10 : if (!this.entities.find(ent => ent == entity))
492 2 : return false;
493 :
494 2 : let ejectableClasses = this.template.EjectClassesOnDestroy._string;
495 2 : let entityClasses = Engine.QueryInterface(entity, IID_Identity).GetClassesList();
496 :
497 2 : return MatchesClassList(entityClasses, ejectableClasses);
498 : };
499 :
500 : /**
501 : * Sets the intitGarrison to the specified entities. Used by the mapreader.
502 : *
503 : * @param {number[]} entities - The entity IDs to garrison on init.
504 : */
505 2 : GarrisonHolder.prototype.SetInitGarrison = function(entities)
506 : {
507 1 : this.initGarrison = clone(entities);
508 : };
509 :
510 : /**
511 : * Initialise the garrisoned units.
512 : */
513 2 : GarrisonHolder.prototype.OnGlobalInitGame = function(msg)
514 : {
515 1 : if (!this.initGarrison)
516 0 : return;
517 :
518 1 : for (let ent of this.initGarrison)
519 : {
520 4 : let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
521 4 : if (cmpGarrisonable)
522 4 : cmpGarrisonable.Garrison(this.entity);
523 : }
524 1 : delete this.initGarrison;
525 : };
526 :
527 2 : GarrisonHolder.prototype.OnValueModification = function(msg)
528 : {
529 1 : if (msg.component != "GarrisonHolder")
530 0 : return;
531 :
532 1 : if (msg.valueNames.indexOf("GarrisonHolder/List/_string") !== -1)
533 : {
534 1 : this.allowedClasses = ApplyValueModificationsToEntity("GarrisonHolder/List/_string", this.template.List._string, this.entity);
535 1 : this.EjectOrKill(this.entities.filter(entity => !this.IsAllowedToBeGarrisoned(entity)));
536 : }
537 :
538 1 : if (msg.valueNames.indexOf("GarrisonHolder/BuffHeal") === -1)
539 1 : return;
540 :
541 0 : if (this.timer && !this.GetHealRate())
542 0 : this.StopTimer();
543 0 : else if (!this.timer && this.GetHealRate())
544 0 : this.StartTimer();
545 : };
546 :
547 2 : Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder);
|