Line data Source code
1 : function UnitAI() {}
2 :
3 1 : UnitAI.prototype.Schema =
4 : "<a:help>Controls the unit's movement, attacks, etc, in response to commands from the player.</a:help>" +
5 : "<a:example/>" +
6 : "<element name='DefaultStance'>" +
7 : "<choice>" +
8 : "<value>violent</value>" +
9 : "<value>aggressive</value>" +
10 : "<value>defensive</value>" +
11 : "<value>passive</value>" +
12 : "<value>standground</value>" +
13 : "<value>skittish</value>" +
14 : "<value>passive-defensive</value>" +
15 : "</choice>" +
16 : "</element>" +
17 : "<element name='FormationController'>" +
18 : "<data type='boolean'/>" +
19 : "</element>" +
20 : "<element name='FleeDistance'>" +
21 : "<ref name='positiveDecimal'/>" +
22 : "</element>" +
23 : "<optional>" +
24 : "<element name='Formations' a:help='Optional list of space-separated formations this unit is allowed to use. Choices include: Scatter, Box, ColumnClosed, LineClosed, ColumnOpen, LineOpen, Flank, Skirmish, Wedge, Testudo, Phalanx, Syntagma, BattleLine.'>" +
25 : "<attribute name='datatype'>" +
26 : "<value>tokens</value>" +
27 : "</attribute>" +
28 : "<text/>" +
29 : "</element>" +
30 : "</optional>" +
31 : "<element name='CanGuard'>" +
32 : "<data type='boolean'/>" +
33 : "</element>" +
34 : "<element name='CanPatrol'>" +
35 : "<data type='boolean'/>" +
36 : "</element>" +
37 : "<element name='PatrolWaitTime' a:help='Number of seconds to wait in between patrol waypoints.'>" +
38 : "<data type='nonNegativeInteger'/>" +
39 : "</element>" +
40 : "<optional>" +
41 : "<element name='CheeringTime'>" +
42 : "<data type='nonNegativeInteger'/>" +
43 : "</element>" +
44 : "</optional>" +
45 : "<optional>" +
46 : "<interleave>" +
47 : "<element name='RoamDistance'>" +
48 : "<ref name='positiveDecimal'/>" +
49 : "</element>" +
50 : "<element name='RoamTimeMin'>" +
51 : "<ref name='positiveDecimal'/>" +
52 : "</element>" +
53 : "<element name='RoamTimeMax'>" +
54 : "<ref name='positiveDecimal'/>" +
55 : "</element>" +
56 : "<element name='FeedTimeMin'>" +
57 : "<ref name='positiveDecimal'/>" +
58 : "</element>" +
59 : "<element name='FeedTimeMax'>" +
60 : "<ref name='positiveDecimal'/>" +
61 : "</element>"+
62 : "</interleave>" +
63 : "</optional>";
64 :
65 : // Unit stances.
66 : // There some targeting options:
67 : // targetVisibleEnemies: anything in vision range is a viable target
68 : // targetAttackersAlways: anything that hurts us is a viable target,
69 : // possibly overriding user orders!
70 : // There are some response options, triggered when targets are detected:
71 : // respondFlee: run away
72 : // respondFleeOnSight: run away when an enemy is sighted
73 : // respondChase: start chasing after the enemy
74 : // respondChaseBeyondVision: start chasing, and don't stop even if it's out
75 : // of this unit's vision range (though still visible to the player)
76 : // respondStandGround: attack enemy but don't move at all
77 : // respondHoldGround: attack enemy but don't move far from current position
78 : // TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts,
79 : // do worry around armies slaughtering the guy standing next to you), etc.
80 1 : var g_Stances = {
81 : "violent": {
82 : "targetVisibleEnemies": true,
83 : "targetAttackersAlways": true,
84 : "respondFlee": false,
85 : "respondFleeOnSight": false,
86 : "respondChase": true,
87 : "respondChaseBeyondVision": true,
88 : "respondStandGround": false,
89 : "respondHoldGround": false,
90 : "selectable": true
91 : },
92 : "aggressive": {
93 : "targetVisibleEnemies": true,
94 : "targetAttackersAlways": false,
95 : "respondFlee": false,
96 : "respondFleeOnSight": false,
97 : "respondChase": true,
98 : "respondChaseBeyondVision": false,
99 : "respondStandGround": false,
100 : "respondHoldGround": false,
101 : "selectable": true
102 : },
103 : "defensive": {
104 : "targetVisibleEnemies": true,
105 : "targetAttackersAlways": false,
106 : "respondFlee": false,
107 : "respondFleeOnSight": false,
108 : "respondChase": false,
109 : "respondChaseBeyondVision": false,
110 : "respondStandGround": false,
111 : "respondHoldGround": true,
112 : "selectable": true
113 : },
114 : "passive": {
115 : "targetVisibleEnemies": false,
116 : "targetAttackersAlways": false,
117 : "respondFlee": true,
118 : "respondFleeOnSight": false,
119 : "respondChase": false,
120 : "respondChaseBeyondVision": false,
121 : "respondStandGround": false,
122 : "respondHoldGround": false,
123 : "selectable": true
124 : },
125 : "standground": {
126 : "targetVisibleEnemies": true,
127 : "targetAttackersAlways": false,
128 : "respondFlee": false,
129 : "respondFleeOnSight": false,
130 : "respondChase": false,
131 : "respondChaseBeyondVision": false,
132 : "respondStandGround": true,
133 : "respondHoldGround": false,
134 : "selectable": true
135 : },
136 : "skittish": {
137 : "targetVisibleEnemies": false,
138 : "targetAttackersAlways": false,
139 : "respondFlee": true,
140 : "respondFleeOnSight": true,
141 : "respondChase": false,
142 : "respondChaseBeyondVision": false,
143 : "respondStandGround": false,
144 : "respondHoldGround": false,
145 : "selectable": false
146 : },
147 : "passive-defensive": {
148 : "targetVisibleEnemies": false,
149 : "targetAttackersAlways": false,
150 : "respondFlee": false,
151 : "respondFleeOnSight": false,
152 : "respondChase": false,
153 : "respondChaseBeyondVision": false,
154 : "respondStandGround": false,
155 : "respondHoldGround": true,
156 : "selectable": false
157 : },
158 : "none": {
159 : // Only to be used by AI or trigger scripts
160 : "targetVisibleEnemies": false,
161 : "targetAttackersAlways": false,
162 : "respondFlee": false,
163 : "respondFleeOnSight": false,
164 : "respondChase": false,
165 : "respondChaseBeyondVision": false,
166 : "respondStandGround": false,
167 : "respondHoldGround": false,
168 : "selectable": false
169 : }
170 : };
171 :
172 : // These orders always require a packed unit, so if a unit that is unpacking is given one of these orders,
173 : // it will immediately cancel unpacking.
174 1 : var g_OrdersCancelUnpacking = new Set([
175 : "FormationWalk",
176 : "Walk",
177 : "WalkAndFight",
178 : "WalkToTarget",
179 : "Patrol",
180 : "Garrison"
181 : ]);
182 :
183 : // When leaving a foundation, we want to be clear of it by this distance.
184 1 : var g_LeaveFoundationRange = 4;
185 :
186 1 : UnitAI.prototype.notifyToCheerInRange = 30;
187 :
188 1 : UnitAI.prototype.DEFAULT_CAPTURE = false;
189 :
190 : // To reject an order, use 'return this.FinishOrder();'
191 1 : const ACCEPT_ORDER = true;
192 :
193 : // See ../helpers/FSM.js for some documentation of this FSM specification syntax
194 1 : UnitAI.prototype.UnitFsmSpec = {
195 :
196 : // Default event handlers:
197 :
198 : "MovementUpdate": function(msg) {
199 : // ignore spurious movement messages
200 : // (these can happen when stopping moving at the same time
201 : // as switching states)
202 : },
203 :
204 : "ConstructionFinished": function(msg) {
205 : // ignore uninteresting construction messages
206 : },
207 :
208 : "LosRangeUpdate": function(msg) {
209 : // Ignore newly-seen units by default.
210 : },
211 :
212 : "LosHealRangeUpdate": function(msg) {
213 : // Ignore newly-seen injured units by default.
214 : },
215 :
216 : "LosAttackRangeUpdate": function(msg) {
217 : // Ignore newly-seen enemy units by default.
218 : },
219 :
220 : "Attacked": function(msg) {
221 : // ignore attacker
222 : },
223 :
224 : "PackFinished": function(msg) {
225 : // ignore
226 : },
227 :
228 : "PickupCanceled": function(msg) {
229 : // ignore
230 : },
231 :
232 : "TradingCanceled": function(msg) {
233 : // ignore
234 : },
235 :
236 : "GuardedAttacked": function(msg) {
237 : // ignore
238 : },
239 :
240 : "OrderTargetRenamed": function() {
241 : // By default, trigger an exit-reenter
242 : // so that state preconditions are checked against the new entity
243 : // (there is no reason to assume the target is still valid).
244 2 : this.SetNextState(this.GetCurrentState());
245 : },
246 :
247 : // Formation handlers:
248 :
249 : "FormationLeave": function(msg) {
250 : // Overloaded by FORMATIONMEMBER
251 : // We end up here if LeaveFormation was called when the entity
252 : // was executing an order in an individual state, so we must
253 : // discard the order now that it has been executed.
254 8 : if (this.order && this.order.type === "LeaveFormation")
255 0 : this.FinishOrder();
256 : },
257 :
258 : // Called when being told to walk as part of a formation
259 : "Order.FormationWalk": function(msg) {
260 11 : if (!this.IsFormationMember() || !this.AbleToMove())
261 0 : return this.FinishOrder();
262 :
263 11 : if (this.CanPack())
264 : {
265 : // If the controller is IDLE, this is just the regular reformation timer.
266 : // In that case we don't actually want to move, as that would unpack us.
267 0 : let cmpControllerAI = Engine.QueryInterface(this.GetFormationController(), IID_UnitAI);
268 0 : if (cmpControllerAI.IsIdle())
269 0 : return this.FinishOrder();
270 0 : this.PushOrderFront("Pack", { "force": true });
271 : }
272 : else
273 11 : this.SetNextState("FORMATIONMEMBER.WALKING");
274 11 : return ACCEPT_ORDER;
275 : },
276 :
277 : // Special orders:
278 : // (these will be overridden by various states)
279 :
280 : "Order.LeaveFoundation": function(msg) {
281 0 : if (!this.WillMoveFromFoundation(msg.data.target))
282 0 : return this.FinishOrder();
283 0 : msg.data.min = g_LeaveFoundationRange;
284 0 : this.SetNextState("INDIVIDUAL.WALKING");
285 0 : return ACCEPT_ORDER;
286 : },
287 :
288 : // Individual orders:
289 :
290 : "Order.LeaveFormation": function() {
291 0 : if (!this.IsFormationMember())
292 0 : return this.FinishOrder();
293 :
294 0 : let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
295 0 : if (cmpFormation)
296 : {
297 0 : cmpFormation.SetRearrange(false);
298 : // Triggers FormationLeave, which ultimately will FinishOrder,
299 : // discarding this order.
300 0 : cmpFormation.RemoveMembers([this.entity]);
301 0 : cmpFormation.SetRearrange(true);
302 : }
303 0 : return ACCEPT_ORDER;
304 : },
305 :
306 : "Order.Stop": function(msg) {
307 0 : this.FinishOrder();
308 0 : return ACCEPT_ORDER;
309 : },
310 :
311 : "Order.Walk": function(msg) {
312 0 : if (!this.AbleToMove())
313 0 : return this.FinishOrder();
314 :
315 0 : if (this.CanPack())
316 : {
317 0 : this.PushOrderFront("Pack", { "force": true });
318 0 : return ACCEPT_ORDER;
319 : }
320 :
321 0 : this.SetHeldPosition(msg.data.x, msg.data.z);
322 0 : msg.data.relaxed = true;
323 0 : this.SetNextState("INDIVIDUAL.WALKING");
324 0 : return ACCEPT_ORDER;
325 : },
326 :
327 : "Order.WalkAndFight": function(msg) {
328 0 : if (!this.AbleToMove())
329 0 : return this.FinishOrder();
330 :
331 0 : if (this.CanPack())
332 : {
333 0 : this.PushOrderFront("Pack", { "force": true });
334 0 : return ACCEPT_ORDER;
335 : }
336 :
337 0 : this.SetHeldPosition(msg.data.x, msg.data.z);
338 0 : msg.data.relaxed = true;
339 0 : this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING");
340 0 : return ACCEPT_ORDER;
341 : },
342 :
343 :
344 : "Order.WalkToTarget": function(msg) {
345 0 : if (!this.AbleToMove())
346 0 : return this.FinishOrder();
347 :
348 0 : if (this.CanPack())
349 : {
350 0 : this.PushOrderFront("Pack", { "force": true });
351 0 : return ACCEPT_ORDER;
352 : }
353 :
354 :
355 0 : if (this.CheckRange(msg.data))
356 0 : return this.FinishOrder();
357 :
358 0 : msg.data.relaxed = true;
359 0 : this.SetNextState("INDIVIDUAL.WALKING");
360 0 : return ACCEPT_ORDER;
361 : },
362 :
363 : "Order.PickupUnit": function(msg) {
364 0 : let cmpHolder = Engine.QueryInterface(this.entity, msg.data.iid);
365 0 : if (!cmpHolder || cmpHolder.IsFull())
366 0 : return this.FinishOrder();
367 :
368 0 : let range = cmpHolder.LoadingRange();
369 0 : msg.data.min = range.min;
370 0 : msg.data.max = range.max;
371 0 : if (this.CheckRange(msg.data))
372 0 : return this.FinishOrder();
373 :
374 : // Check if we need to move
375 : // If the target can reach us and we are reasonably close, don't move.
376 : // TODO: it would be slightly more optimal to check for real, not bird-flight distance.
377 0 : let cmpPassengerMotion = Engine.QueryInterface(msg.data.target, IID_UnitMotion);
378 0 : if (cmpPassengerMotion &&
379 : cmpPassengerMotion.IsTargetRangeReachable(this.entity, range.min, range.max) &&
380 : PositionHelper.DistanceBetweenEntities(this.entity, msg.data.target) < 200)
381 0 : this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
382 0 : else if (this.AbleToMove())
383 0 : this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
384 : else
385 0 : return this.FinishOrder();
386 0 : return ACCEPT_ORDER;
387 : },
388 :
389 : "Order.Guard": function(msg) {
390 0 : if (!this.AddGuard(msg.data.target))
391 0 : return this.FinishOrder();
392 :
393 0 : if (this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
394 0 : this.SetNextState("INDIVIDUAL.GUARD.GUARDING");
395 0 : else if (this.AbleToMove())
396 0 : this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
397 : else
398 0 : return this.FinishOrder();
399 0 : return ACCEPT_ORDER;
400 : },
401 :
402 : "Order.Flee": function(msg) {
403 1 : if (!this.AbleToMove())
404 0 : return this.FinishOrder();
405 1 : this.SetNextState("INDIVIDUAL.FLEEING");
406 1 : return ACCEPT_ORDER;
407 : },
408 :
409 : "Order.Attack": function(msg) {
410 17 : let type = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture);
411 17 : if (!type)
412 0 : return this.FinishOrder();
413 :
414 17 : msg.data.attackType = type;
415 :
416 17 : this.RememberTargetPosition();
417 17 : if (msg.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather")
418 0 : this.RememberTargetPosition(this.orderQueue[1].data);
419 :
420 17 : if (this.CheckTargetAttackRange(msg.data.target, msg.data.attackType))
421 : {
422 17 : if (this.CanUnpack())
423 : {
424 0 : this.PushOrderFront("Unpack", { "force": true });
425 0 : return ACCEPT_ORDER;
426 : }
427 :
428 : // Cancel any current packing order.
429 17 : if (this.EnsureCorrectPackStateForAttack(false))
430 17 : this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
431 :
432 17 : return ACCEPT_ORDER;
433 : }
434 :
435 : // If we're hunting, that's a special case where we should continue attacking our target.
436 0 : if (this.GetStance().respondStandGround && !msg.data.force && !msg.data.hunting || !this.AbleToMove())
437 0 : return this.FinishOrder();
438 :
439 0 : if (this.CanPack())
440 : {
441 0 : this.PushOrderFront("Pack", { "force": true });
442 0 : return ACCEPT_ORDER;
443 : }
444 :
445 : // If we're currently packing/unpacking, make sure we are packed, so we can move.
446 0 : if (this.EnsureCorrectPackStateForAttack(true))
447 0 : this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
448 0 : return ACCEPT_ORDER;
449 : },
450 :
451 : "Order.Patrol": function(msg) {
452 0 : if (!this.AbleToMove())
453 0 : return this.FinishOrder();
454 :
455 0 : if (this.CanPack())
456 : {
457 0 : this.PushOrderFront("Pack", { "force": true });
458 0 : return ACCEPT_ORDER;
459 : }
460 :
461 0 : msg.data.relaxed = true;
462 :
463 0 : this.SetNextState("INDIVIDUAL.PATROL.PATROLLING");
464 0 : return ACCEPT_ORDER;
465 : },
466 :
467 : "Order.Heal": function(msg) {
468 0 : if (!this.TargetIsAlive(msg.data.target))
469 0 : return this.FinishOrder();
470 :
471 : // Healers can't heal themselves.
472 0 : if (msg.data.target == this.entity)
473 0 : return this.FinishOrder();
474 :
475 0 : if (this.CheckTargetRange(msg.data.target, IID_Heal))
476 : {
477 0 : this.SetNextState("INDIVIDUAL.HEAL.HEALING");
478 0 : return ACCEPT_ORDER;
479 : }
480 0 : if (!this.AbleToMove())
481 0 : return this.FinishOrder();
482 :
483 0 : if (this.GetStance().respondStandGround && !msg.data.force)
484 0 : return this.FinishOrder();
485 :
486 0 : this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
487 0 : return ACCEPT_ORDER;
488 : },
489 :
490 : "Order.Gather": function(msg) {
491 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
492 0 : if (!cmpResourceGatherer)
493 0 : return this.FinishOrder();
494 :
495 : // We were given the order to gather while we were still gathering.
496 : // This is needed because we don't re-enter the GATHER-state.
497 0 : const taskedResourceType = cmpResourceGatherer.GetTaskedResourceType();
498 0 : if (taskedResourceType && msg.data.type.generic != taskedResourceType)
499 0 : this.UnitFsm.SwitchToNextState(this, "INDIVIDUAL.GATHER");
500 :
501 0 : if (!this.CanGather(msg.data.target))
502 : {
503 0 : this.SetNextState("INDIVIDUAL.GATHER.FINDINGNEWTARGET");
504 0 : return ACCEPT_ORDER;
505 : }
506 :
507 0 : if (this.MustKillGatherTarget(msg.data.target))
508 : {
509 0 : const bestAttack = Engine.QueryInterface(this.entity, IID_Attack)?.GetBestAttackAgainst(msg.data.target, false);
510 : // Make sure we can attack the target, else we'll get very stuck
511 0 : if (!bestAttack)
512 : {
513 : // Oops, we can't attack at all - give up
514 : // TODO: should do something so the player knows why this failed
515 0 : return this.FinishOrder();
516 : }
517 : // The target was visible when this order was issued,
518 : // but could now be invisible again.
519 0 : if (!this.CheckTargetVisible(msg.data.target))
520 : {
521 0 : if (msg.data.secondTry === undefined)
522 : {
523 0 : msg.data.secondTry = true;
524 0 : this.PushOrderFront("Walk", msg.data.lastPos);
525 : }
526 : // We couldn't move there, or the target moved away
527 0 : else if (!this.FinishOrder())
528 0 : this.PushOrderFront("GatherNearPosition", {
529 : "x": msg.data.lastPos.x,
530 : "z": msg.data.lastPos.z,
531 : "type": msg.data.type,
532 : "template": msg.data.template
533 : });
534 0 : return ACCEPT_ORDER;
535 : }
536 :
537 0 : if (!this.AbleToMove() && !this.CheckTargetRange(msg.data.target, IID_Attack, bestAttack))
538 0 : return this.FinishOrder();
539 :
540 0 : this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true });
541 0 : return ACCEPT_ORDER;
542 : }
543 :
544 : // If the unit is full go to the nearest dropsite instead of trying to gather.
545 0 : if (!cmpResourceGatherer.CanCarryMore(msg.data.type.generic))
546 : {
547 0 : this.SetNextState("INDIVIDUAL.GATHER.RETURNINGRESOURCE");
548 0 : return ACCEPT_ORDER;
549 : }
550 :
551 0 : this.RememberTargetPosition();
552 0 : if (!msg.data.initPos)
553 0 : msg.data.initPos = msg.data.lastPos;
554 :
555 0 : if (this.CheckTargetRange(msg.data.target, IID_ResourceGatherer))
556 0 : this.SetNextState("INDIVIDUAL.GATHER.GATHERING");
557 0 : else if (this.AbleToMove())
558 0 : this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
559 : else
560 0 : return this.FinishOrder();
561 0 : return ACCEPT_ORDER;
562 : },
563 :
564 : "Order.GatherNearPosition": function(msg) {
565 0 : if (!this.AbleToMove())
566 0 : return this.FinishOrder();
567 0 : this.SetNextState("INDIVIDUAL.GATHER.WALKING");
568 0 : msg.data.initPos = { 'x': msg.data.x, 'z': msg.data.z };
569 0 : msg.data.relaxed = true;
570 0 : return ACCEPT_ORDER;
571 : },
572 :
573 : "Order.DropAtNearestDropSite": function(msg) {
574 0 : const cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
575 0 : if (!cmpResourceGatherer)
576 0 : return this.FinishOrder();
577 0 : const nearby = this.FindNearestDropsite(cmpResourceGatherer.GetMainCarryingType());
578 0 : if (!nearby)
579 0 : return this.FinishOrder();
580 0 : this.ReturnResource(nearby, false, true);
581 0 : return ACCEPT_ORDER;
582 : },
583 :
584 : "Order.ReturnResource": function(msg) {
585 0 : if (this.CheckTargetRange(msg.data.target, IID_ResourceGatherer))
586 0 : this.SetNextState("INDIVIDUAL.RETURNRESOURCE.DROPPINGRESOURCES");
587 0 : else if (this.AbleToMove())
588 0 : this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
589 : else
590 0 : return this.FinishOrder();
591 0 : return ACCEPT_ORDER;
592 : },
593 :
594 : "Order.Trade": function(msg) {
595 0 : if (!this.AbleToMove())
596 0 : return this.FinishOrder();
597 : // We must check if this trader has both markets in case it was a back-to-work order.
598 0 : let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
599 0 : if (!cmpTrader || !cmpTrader.HasBothMarkets())
600 0 : return this.FinishOrder();
601 :
602 0 : this.waypoints = [];
603 0 : this.SetNextState("TRADE.APPROACHINGMARKET");
604 0 : return ACCEPT_ORDER;
605 : },
606 :
607 : "Order.Repair": function(msg) {
608 1 : if (this.CheckTargetRange(msg.data.target, IID_Builder))
609 1 : this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING");
610 0 : else if (this.AbleToMove())
611 0 : this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
612 : else
613 0 : return this.FinishOrder();
614 1 : return ACCEPT_ORDER;
615 : },
616 :
617 : "Order.Garrison": function(msg) {
618 1 : if (!this.AbleToMove())
619 0 : return this.FinishOrder();
620 :
621 : // Also pack when we are in range.
622 1 : if (this.CanPack())
623 : {
624 0 : this.PushOrderFront("Pack", { "force": true });
625 0 : return ACCEPT_ORDER;
626 : }
627 :
628 1 : if (this.CheckTargetRange(msg.data.target, msg.data.garrison ? IID_Garrisonable : IID_Turretable))
629 0 : this.SetNextState("INDIVIDUAL.GARRISON.GARRISONING");
630 : else
631 1 : this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
632 1 : return ACCEPT_ORDER;
633 : },
634 :
635 : "Order.Ungarrison": function(msg) {
636 : // Note that this order MUST succeed, or we break
637 : // the assumptions done in garrisonable/garrisonHolder,
638 : // especially in Unloading in the latter. (For user feedback.)
639 : // ToDo: This can be fixed by not making that assumption :)
640 0 : this.FinishOrder();
641 0 : return ACCEPT_ORDER;
642 : },
643 :
644 : "Order.Cheer": function(msg) {
645 0 : return this.FinishOrder();
646 : },
647 :
648 : "Order.Pack": function(msg) {
649 0 : if (!this.CanPack())
650 0 : return this.FinishOrder();
651 0 : this.SetNextState("INDIVIDUAL.PACKING");
652 0 : return ACCEPT_ORDER;
653 : },
654 :
655 : "Order.Unpack": function(msg) {
656 0 : if (!this.CanUnpack())
657 0 : return this.FinishOrder();
658 0 : this.SetNextState("INDIVIDUAL.UNPACKING");
659 0 : return ACCEPT_ORDER;
660 : },
661 :
662 : "Order.MoveToChasingPoint": function(msg) {
663 : // Overriden by the CHASING state.
664 : // Can however happen outside of it when renaming...
665 : // TODO: don't use an order for that behaviour.
666 0 : return this.FinishOrder();
667 : },
668 :
669 : "Order.CollectTreasure": function(msg) {
670 0 : if (this.CheckTargetRange(msg.data.target, IID_TreasureCollector))
671 0 : this.SetNextState("INDIVIDUAL.COLLECTTREASURE.COLLECTING");
672 0 : else if (this.AbleToMove())
673 0 : this.SetNextState("INDIVIDUAL.COLLECTTREASURE.APPROACHING");
674 : else
675 0 : return this.FinishOrder();
676 :
677 0 : return ACCEPT_ORDER;
678 : },
679 :
680 : "Order.CollectTreasureNearPosition": function(msg) {
681 0 : if (!this.AbleToMove())
682 0 : return this.FinishOrder();
683 0 : this.SetNextState("INDIVIDUAL.COLLECTTREASURE.WALKING");
684 0 : msg.data.initPos = { 'x': msg.data.x, 'z': msg.data.z };
685 0 : msg.data.relaxed = true;
686 0 : return ACCEPT_ORDER;
687 : },
688 :
689 : // States for the special entity representing a group of units moving in formation:
690 : "FORMATIONCONTROLLER": {
691 :
692 : "Order.Walk": function(msg) {
693 3 : if (!this.AbleToMove())
694 0 : return this.FinishOrder();
695 3 : this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
696 3 : this.SetNextState("WALKING");
697 3 : return ACCEPT_ORDER;
698 : },
699 :
700 : "Order.WalkAndFight": function(msg) {
701 0 : if (!this.AbleToMove())
702 0 : return this.FinishOrder();
703 0 : this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
704 0 : this.SetNextState("WALKINGANDFIGHTING");
705 0 : return ACCEPT_ORDER;
706 : },
707 :
708 : "Order.MoveIntoFormation": function(msg) {
709 1 : if (!this.AbleToMove())
710 0 : return this.FinishOrder();
711 1 : this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
712 1 : this.SetNextState("FORMING");
713 1 : return ACCEPT_ORDER;
714 : },
715 :
716 : // Only used by other orders to walk there in formation.
717 : "Order.WalkToTargetRange": function(msg) {
718 0 : if (this.CheckRange(msg.data))
719 0 : return this.FinishOrder();
720 0 : if (!this.AbleToMove())
721 0 : return this.FinishOrder();
722 0 : this.SetNextState("WALKING");
723 0 : return ACCEPT_ORDER;
724 : },
725 :
726 : "Order.WalkToTarget": function(msg) {
727 0 : if (this.CheckRange(msg.data))
728 0 : return this.FinishOrder();
729 0 : if (!this.AbleToMove())
730 0 : return this.FinishOrder();
731 0 : this.SetNextState("WALKING");
732 0 : return ACCEPT_ORDER;
733 : },
734 :
735 : "Order.WalkToPointRange": function(msg) {
736 0 : if (this.CheckRange(msg.data))
737 0 : return this.FinishOrder();
738 0 : if (!this.AbleToMove())
739 0 : return this.FinishOrder();
740 0 : this.SetNextState("WALKING");
741 0 : return ACCEPT_ORDER;
742 : },
743 :
744 : "Order.Patrol": function(msg) {
745 0 : if (!this.AbleToMove())
746 0 : return this.FinishOrder();
747 0 : this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
748 0 : this.SetNextState("PATROL.PATROLLING");
749 0 : return ACCEPT_ORDER;
750 : },
751 :
752 : "Order.Guard": function(msg) {
753 0 : this.CallMemberFunction("Guard", [msg.data.target, false]);
754 0 : Engine.QueryInterface(this.entity, IID_Formation).Disband();
755 0 : return ACCEPT_ORDER;
756 : },
757 :
758 : "Order.Stop": function(msg) {
759 0 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
760 0 : cmpFormation.ResetOrderVariant();
761 0 : if (!this.IsAttackingAsFormation())
762 0 : this.CallMemberFunction("Stop", [false]);
763 0 : this.FinishOrder();
764 0 : return ACCEPT_ORDER;
765 : // Don't move the members back into formation,
766 : // as the formation then resets and it looks odd when walk-stopping.
767 : // TODO: this should be improved in the formation reshaping code.
768 : },
769 :
770 : "Order.Attack": function(msg) {
771 2 : let target = msg.data.target;
772 2 : let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
773 2 : if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
774 0 : target = cmpTargetUnitAI.GetFormationController();
775 :
776 2 : if (!this.CheckFormationTargetAttackRange(target))
777 : {
778 0 : if (this.AbleToMove() && this.CheckTargetVisible(target))
779 : {
780 0 : this.SetNextState("COMBAT.APPROACHING");
781 0 : return ACCEPT_ORDER;
782 : }
783 0 : return this.FinishOrder();
784 : }
785 2 : this.CallMemberFunction("Attack", [target, msg.data.allowCapture, false]);
786 2 : let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
787 2 : if (cmpAttack && cmpAttack.CanAttackAsFormation())
788 0 : this.SetNextState("COMBAT.ATTACKING");
789 : else
790 2 : this.SetNextState("MEMBER");
791 2 : return ACCEPT_ORDER;
792 : },
793 :
794 : "Order.Garrison": function(msg) {
795 0 : if (!Engine.QueryInterface(msg.data.target,
796 : msg.data.garrison ? IID_GarrisonHolder : IID_TurretHolder))
797 0 : return this.FinishOrder();
798 0 : if (this.CheckTargetRange(msg.data.target, msg.data.garrison ? IID_Garrisonable : IID_Turretable))
799 : {
800 0 : if (!this.AbleToMove() || !this.CheckTargetVisible(msg.data.target))
801 0 : return this.FinishOrder();
802 :
803 0 : this.SetNextState("GARRISON.APPROACHING");
804 : }
805 : else
806 0 : this.SetNextState("GARRISON.GARRISONING");
807 0 : return ACCEPT_ORDER;
808 : },
809 :
810 : "Order.Gather": function(msg) {
811 0 : if (this.MustKillGatherTarget(msg.data.target))
812 : {
813 : // The target was visible when this order was given,
814 : // but could now be invisible.
815 0 : if (!this.CheckTargetVisible(msg.data.target))
816 : {
817 0 : if (msg.data.secondTry === undefined)
818 : {
819 0 : msg.data.secondTry = true;
820 0 : this.PushOrderFront("Walk", msg.data.lastPos);
821 : }
822 : // We couldn't move there, or the target moved away
823 : else
824 : {
825 0 : let data = msg.data;
826 0 : if (!this.FinishOrder())
827 0 : this.PushOrderFront("GatherNearPosition", {
828 : "x": data.lastPos.x,
829 : "z": data.lastPos.z,
830 : "type": data.type,
831 : "template": data.template
832 : });
833 : }
834 0 : return ACCEPT_ORDER;
835 : }
836 0 : this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true, "min": 0, "max": 10 });
837 0 : return ACCEPT_ORDER;
838 : }
839 :
840 : // TODO: on what should we base this range?
841 0 : if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
842 : {
843 0 : if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
844 0 : return this.FinishOrder();
845 : // TODO: Should we issue a gather-near-position order
846 : // if the target isn't gatherable/doesn't exist anymore?
847 0 : if (!msg.data.secondTry)
848 : {
849 0 : msg.data.secondTry = true;
850 0 : this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
851 0 : return ACCEPT_ORDER;
852 : }
853 0 : return this.FinishOrder();
854 : }
855 :
856 0 : this.CallMemberFunction("Gather", [msg.data.target, false]);
857 :
858 0 : this.SetNextState("MEMBER");
859 0 : return ACCEPT_ORDER;
860 : },
861 :
862 : "Order.GatherNearPosition": function(msg) {
863 : // TODO: on what should we base this range?
864 0 : if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20))
865 : {
866 : // Out of range; move there in formation
867 0 : this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 });
868 0 : return ACCEPT_ORDER;
869 : }
870 :
871 0 : this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
872 :
873 0 : this.SetNextState("MEMBER");
874 0 : return ACCEPT_ORDER;
875 : },
876 :
877 : "Order.Heal": function(msg) {
878 : // TODO: on what should we base this range?
879 0 : if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
880 : {
881 0 : if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
882 0 : return this.FinishOrder();
883 :
884 0 : if (!msg.data.secondTry)
885 : {
886 0 : msg.data.secondTry = true;
887 0 : this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
888 0 : return ACCEPT_ORDER;
889 : }
890 0 : return this.FinishOrder();
891 : }
892 :
893 0 : this.CallMemberFunction("Heal", [msg.data.target, false]);
894 :
895 0 : this.SetNextState("MEMBER");
896 0 : return ACCEPT_ORDER;
897 : },
898 :
899 : "Order.CollectTreasure": function(msg) {
900 : // TODO: on what should we base this range?
901 0 : if (this.CheckTargetRangeExplicit(msg.data.target, 0, 20))
902 : {
903 0 : this.CallMemberFunction("CollectTreasure", [msg.data.target, false, false]);
904 0 : this.SetNextState("MEMBER");
905 :
906 0 : return ACCEPT_ORDER;
907 : }
908 0 : if (msg.data.secondTry || !this.CheckTargetVisible(msg.data.target))
909 0 : return this.FinishOrder();
910 :
911 0 : msg.data.secondTry = true;
912 0 : this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 20 });
913 0 : return ACCEPT_ORDER;
914 : },
915 :
916 : "Order.CollectTreasureNearPosition": function(msg) {
917 : // TODO: on what should we base this range?
918 0 : if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20))
919 : {
920 0 : this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 });
921 0 : return ACCEPT_ORDER;
922 : }
923 :
924 0 : this.CallMemberFunction("CollectTreasureNearPosition", [msg.data.x, msg.data.z, false, false]);
925 0 : this.SetNextState("MEMBER");
926 0 : return ACCEPT_ORDER;
927 : },
928 :
929 : "Order.Repair": function(msg) {
930 : // TODO: on what should we base this range?
931 0 : if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
932 : {
933 0 : if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
934 0 : return this.FinishOrder();
935 :
936 0 : if (!msg.data.secondTry)
937 : {
938 0 : msg.data.secondTry = true;
939 0 : this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
940 0 : return ACCEPT_ORDER;
941 : }
942 0 : return this.FinishOrder();
943 : }
944 :
945 0 : this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]);
946 :
947 0 : this.SetNextState("MEMBER");
948 0 : return ACCEPT_ORDER;
949 : },
950 :
951 : "Order.ReturnResource": function(msg) {
952 : // TODO: on what should we base this range?
953 0 : if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
954 : {
955 0 : if (!this.CheckTargetVisible(msg.data.target))
956 0 : return this.FinishOrder();
957 :
958 0 : if (!msg.data.secondTry)
959 : {
960 0 : msg.data.secondTry = true;
961 0 : this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
962 0 : return ACCEPT_ORDER;
963 : }
964 0 : return this.FinishOrder();
965 : }
966 :
967 0 : this.CallMemberFunction("ReturnResource", [msg.data.target, false]);
968 :
969 0 : this.SetNextState("MEMBER");
970 0 : return ACCEPT_ORDER;
971 : },
972 :
973 : "Order.Pack": function(msg) {
974 0 : this.CallMemberFunction("Pack", [false]);
975 :
976 0 : this.SetNextState("MEMBER");
977 0 : return ACCEPT_ORDER;
978 : },
979 :
980 : "Order.Unpack": function(msg) {
981 0 : this.CallMemberFunction("Unpack", [false]);
982 :
983 0 : this.SetNextState("MEMBER");
984 0 : return ACCEPT_ORDER;
985 : },
986 :
987 : "Order.DropAtNearestDropSite": function(msg) {
988 0 : this.CallMemberFunction("DropAtNearestDropSite", [false, false]);
989 :
990 0 : this.SetNextState("MEMBER");
991 0 : return ACCEPT_ORDER;
992 : },
993 :
994 : "IDLE": {
995 : "enter": function(msg) {
996 : // Turn rearrange off. Otherwise, if the formation is idle
997 : // but individual units go off to fight,
998 : // any death will rearrange the formation, which looks odd.
999 : // Instead, move idle units in formation on a timer.
1000 4 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1001 4 : cmpFormation.SetRearrange(false);
1002 : // Start the timer on the next turn to catch up with potential stragglers.
1003 4 : this.StartTimer(100, 2000);
1004 4 : this.isIdle = true;
1005 4 : this.CallMemberFunction("ResetIdle");
1006 4 : return false;
1007 : },
1008 :
1009 : "leave": function() {
1010 4 : this.isIdle = false;
1011 4 : this.StopTimer();
1012 : },
1013 :
1014 : "Timer": function(msg) {
1015 0 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1016 0 : if (!cmpFormation)
1017 0 : return;
1018 :
1019 0 : if (this.TestAllMemberFunction("IsIdle"))
1020 0 : cmpFormation.MoveMembersIntoFormation(false, false);
1021 : },
1022 :
1023 : },
1024 :
1025 : "WALKING": {
1026 : "enter": function() {
1027 3 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1028 3 : cmpFormation.SetRearrange(true);
1029 3 : cmpFormation.MoveMembersIntoFormation(true, true);
1030 3 : if (!this.MoveTo(this.order.data))
1031 : {
1032 0 : this.FinishOrder();
1033 0 : return true;
1034 : }
1035 3 : return false;
1036 : },
1037 :
1038 : "leave": function() {
1039 3 : this.StopTimer();
1040 3 : this.StopMoving();
1041 : },
1042 :
1043 : "MovementUpdate": function(msg) {
1044 0 : if (msg.veryObstructed && !this.timer)
1045 : {
1046 : // It's possible that the controller (with large clearance)
1047 : // is stuck, but not the individual units.
1048 : // Ask them to move individually for a little while.
1049 0 : this.CallMemberFunction("MoveTo", [this.order.data]);
1050 0 : this.StartTimer(3000);
1051 0 : return;
1052 : }
1053 0 : else if (this.timer)
1054 0 : return;
1055 0 : if (msg.likelyFailure || this.CheckRange(this.order.data))
1056 0 : this.FinishOrder();
1057 : },
1058 :
1059 : "Timer": function() {
1060 : // Reenter to reset the pathfinder state.
1061 0 : this.SetNextState("WALKING");
1062 : }
1063 : },
1064 :
1065 : "WALKINGANDFIGHTING": {
1066 : "enter": function(msg) {
1067 0 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1068 0 : cmpFormation.SetRearrange(true);
1069 0 : cmpFormation.MoveMembersIntoFormation(true, true, "combat");
1070 0 : if (!this.MoveTo(this.order.data))
1071 : {
1072 0 : this.FinishOrder();
1073 0 : return true;
1074 : }
1075 0 : this.StartTimer(0, 1000);
1076 0 : this.order.data.returningState = "WALKINGANDFIGHTING";
1077 0 : return false;
1078 : },
1079 :
1080 : "leave": function() {
1081 0 : this.StopMoving();
1082 0 : this.StopTimer();
1083 : },
1084 :
1085 : "Timer": function(msg) {
1086 0 : Engine.ProfileStart("FindWalkAndFightTargets");
1087 0 : if (this.FindWalkAndFightTargets())
1088 0 : this.SetNextState("MEMBER");
1089 :
1090 0 : Engine.ProfileStop();
1091 : },
1092 :
1093 : "MovementUpdate": function(msg) {
1094 0 : if (msg.likelyFailure || this.CheckRange(this.order.data))
1095 0 : this.FinishOrder();
1096 : },
1097 : },
1098 :
1099 : "PATROL": {
1100 : "enter": function() {
1101 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
1102 0 : if (!cmpPosition || !cmpPosition.IsInWorld())
1103 : {
1104 0 : this.FinishOrder();
1105 0 : return true;
1106 : }
1107 : // Memorize the origin position in case that we want to go back.
1108 0 : if (!this.patrolStartPosOrder)
1109 : {
1110 0 : this.patrolStartPosOrder = cmpPosition.GetPosition();
1111 0 : this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses;
1112 0 : this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture;
1113 : }
1114 :
1115 0 : this.SetAnimationVariant("combat");
1116 :
1117 0 : return false;
1118 : },
1119 :
1120 : "leave": function() {
1121 0 : delete this.patrolStartPosOrder;
1122 0 : this.SetDefaultAnimationVariant();
1123 : },
1124 :
1125 : "PATROLLING": {
1126 : "enter": function() {
1127 0 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1128 0 : cmpFormation.SetRearrange(true);
1129 0 : cmpFormation.MoveMembersIntoFormation(true, true, "combat");
1130 :
1131 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
1132 0 : if (!cmpPosition || !cmpPosition.IsInWorld() ||
1133 : !this.MoveTo(this.order.data))
1134 : {
1135 0 : this.FinishOrder();
1136 0 : return true;
1137 : }
1138 :
1139 0 : this.StartTimer(0, 1000);
1140 0 : this.order.data.returningState = "PATROL.PATROLLING";
1141 0 : return false;
1142 : },
1143 :
1144 : "leave": function() {
1145 0 : this.StopMoving();
1146 0 : this.StopTimer();
1147 : },
1148 :
1149 : "Timer": function(msg) {
1150 0 : if (this.FindWalkAndFightTargets())
1151 0 : this.SetNextState("MEMBER");
1152 : },
1153 :
1154 : "MovementUpdate": function(msg) {
1155 0 : if (!msg.likelyFailure && !msg.likelySuccess && !this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange))
1156 0 : return;
1157 :
1158 0 : if (this.orderQueue.length == 1)
1159 0 : this.PushOrder("Patrol", this.patrolStartPosOrder);
1160 :
1161 0 : this.PushOrder(this.order.type, this.order.data);
1162 0 : this.SetNextState("CHECKINGWAYPOINT");
1163 : },
1164 : },
1165 :
1166 : "CHECKINGWAYPOINT": {
1167 : "enter": function() {
1168 0 : this.StartTimer(0, 1000);
1169 0 : this.stopSurveying = 0;
1170 : // TODO: pick a proper animation
1171 0 : return false;
1172 : },
1173 :
1174 : "leave": function() {
1175 0 : this.StopTimer();
1176 0 : delete this.stopSurveying;
1177 : },
1178 :
1179 : "Timer": function(msg) {
1180 0 : if (this.stopSurveying >= +this.template.PatrolWaitTime)
1181 : {
1182 0 : this.FinishOrder();
1183 0 : return;
1184 : }
1185 0 : if (this.FindWalkAndFightTargets())
1186 0 : this.SetNextState("MEMBER");
1187 : else
1188 0 : ++this.stopSurveying;
1189 : }
1190 : }
1191 : },
1192 :
1193 : "GARRISON": {
1194 : "APPROACHING": {
1195 : "enter": function() {
1196 0 : if (!this.MoveToTargetRange(this.order.data.target, this.order.data.garrison ? IID_Garrisonable : IID_Turretable))
1197 : {
1198 0 : this.FinishOrder();
1199 0 : return true;
1200 : }
1201 :
1202 0 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1203 0 : cmpFormation.SetRearrange(true);
1204 0 : cmpFormation.MoveMembersIntoFormation(true, true);
1205 :
1206 : // If the holder should pickup, warn it so it can take needed action.
1207 0 : let cmpHolder = Engine.QueryInterface(this.order.data.target, this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder);
1208 0 : if (cmpHolder && cmpHolder.CanPickup(this.entity))
1209 : {
1210 0 : this.pickup = this.order.data.target; // temporary, deleted in "leave"
1211 0 : Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity, "iid": this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder });
1212 : }
1213 0 : return false;
1214 : },
1215 :
1216 : "leave": function() {
1217 0 : this.StopMoving();
1218 0 : if (this.pickup)
1219 : {
1220 0 : Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
1221 0 : delete this.pickup;
1222 : }
1223 : },
1224 :
1225 : "MovementUpdate": function(msg) {
1226 0 : if (msg.likelyFailure || msg.likelySuccess)
1227 0 : this.SetNextState("GARRISONING");
1228 : },
1229 : },
1230 :
1231 : "GARRISONING": {
1232 : "enter": function() {
1233 0 : this.CallMemberFunction(this.order.data.garrison ? "Garrison" : "OccupyTurret", [this.order.data.target, false]);
1234 : // We might have been disbanded due to the lack of members.
1235 0 : if (Engine.QueryInterface(this.entity, IID_Formation).GetMemberCount())
1236 0 : this.SetNextState("MEMBER");
1237 0 : return true;
1238 : },
1239 : },
1240 : },
1241 :
1242 : "FORMING": {
1243 : "enter": function() {
1244 1 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1245 1 : cmpFormation.SetRearrange(true);
1246 1 : cmpFormation.MoveMembersIntoFormation(true, true);
1247 :
1248 1 : if (!this.MoveTo(this.order.data))
1249 : {
1250 1 : this.FinishOrder();
1251 1 : return true;
1252 : }
1253 0 : return false;
1254 : },
1255 :
1256 : "leave": function() {
1257 1 : this.StopMoving();
1258 : },
1259 :
1260 : "MovementUpdate": function(msg) {
1261 0 : if (!msg.likelyFailure && !this.CheckRange(this.order.data))
1262 0 : return;
1263 :
1264 0 : this.FinishOrder();
1265 : }
1266 : },
1267 :
1268 : "COMBAT": {
1269 : "APPROACHING": {
1270 : "enter": function() {
1271 0 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1272 0 : cmpFormation.SetRearrange(true);
1273 0 : cmpFormation.MoveMembersIntoFormation(true, true, "combat");
1274 :
1275 0 : if (!this.MoveFormationToTargetAttackRange(this.order.data.target))
1276 : {
1277 0 : this.FinishOrder();
1278 0 : return true;
1279 : }
1280 0 : return false;
1281 : },
1282 :
1283 : "leave": function() {
1284 0 : this.StopMoving();
1285 : },
1286 :
1287 : "MovementUpdate": function(msg) {
1288 0 : let target = this.order.data.target;
1289 0 : let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
1290 0 : if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
1291 0 : target = cmpTargetUnitAI.GetFormationController();
1292 0 : let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
1293 0 : this.CallMemberFunction("Attack", [target, this.order.data.allowCapture, false]);
1294 0 : if (cmpAttack.CanAttackAsFormation())
1295 0 : this.SetNextState("COMBAT.ATTACKING");
1296 : else
1297 0 : this.SetNextState("MEMBER");
1298 : },
1299 : },
1300 :
1301 : "ATTACKING": {
1302 : // Wait for individual members to finish
1303 : "enter": function(msg) {
1304 0 : const target = this.order.data.target;
1305 0 : if (!this.CheckFormationTargetAttackRange(target))
1306 : {
1307 0 : if (this.CanAttack(target) && this.CheckTargetVisible(target))
1308 : {
1309 0 : this.SetNextState("COMBAT.APPROACHING");
1310 0 : return true;
1311 : }
1312 0 : this.FinishOrder();
1313 0 : return true;
1314 : }
1315 :
1316 0 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1317 : // TODO fix the rearranging while attacking as formation
1318 0 : cmpFormation.SetRearrange(!this.IsAttackingAsFormation());
1319 0 : cmpFormation.MoveMembersIntoFormation(false, false, "combat");
1320 0 : this.StartTimer(200, 200);
1321 0 : return false;
1322 : },
1323 :
1324 : "Timer": function(msg) {
1325 0 : const target = this.order.data.target;
1326 0 : if (!this.CheckFormationTargetAttackRange(target))
1327 : {
1328 0 : if (this.CanAttack(target) && this.CheckTargetVisible(target))
1329 : {
1330 0 : this.SetNextState("COMBAT.APPROACHING");
1331 0 : return;
1332 : }
1333 0 : this.FinishOrder();
1334 0 : return;
1335 : }
1336 : },
1337 :
1338 : "leave": function(msg) {
1339 0 : this.StopTimer();
1340 0 : var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1341 0 : if (cmpFormation)
1342 0 : cmpFormation.SetRearrange(true);
1343 : },
1344 : },
1345 : },
1346 :
1347 : // Wait for individual members to finish
1348 : "MEMBER": {
1349 : "OrderTargetRenamed": function(msg) {
1350 : // In general, don't react - we don't want to send spurious messages to members.
1351 : // This looks odd for hunting however because we wait for all
1352 : // entities to have clumped around the dead resource before proceeding
1353 : // so explicitly handle this case.
1354 0 : if (this.order && this.order.data && this.order.data.hunting &&
1355 : this.order.data.target == msg.data.newentity &&
1356 : this.orderQueue.length > 1)
1357 0 : this.FinishOrder();
1358 : },
1359 :
1360 : "enter": function(msg) {
1361 : // Don't rearrange the formation, as that forces all units to stop
1362 : // what they're doing.
1363 2 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1364 2 : if (cmpFormation)
1365 2 : cmpFormation.SetRearrange(false);
1366 : // While waiting on members, the formation is more like
1367 : // a group of unit and does not have a well-defined position,
1368 : // so move the controller out of the world to enforce that.
1369 2 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
1370 2 : if (cmpPosition && cmpPosition.IsInWorld())
1371 2 : cmpPosition.MoveOutOfWorld();
1372 :
1373 2 : this.StartTimer(1000, 1000);
1374 2 : return false;
1375 : },
1376 :
1377 : "Timer": function(msg) {
1378 0 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1379 0 : if (cmpFormation && !cmpFormation.AreAllMembersFinished())
1380 0 : return;
1381 :
1382 0 : if (this.order?.data?.returningState)
1383 0 : this.SetNextState(this.order.data.returningState);
1384 : else
1385 0 : this.FinishOrder();
1386 : },
1387 :
1388 : "leave": function(msg) {
1389 2 : this.StopTimer();
1390 : // Reform entirely as members might be all over the place now.
1391 2 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
1392 2 : if (cmpFormation && (cmpFormation.AreAllMembersIdle() || this.orderQueue.length))
1393 2 : cmpFormation.MoveMembersIntoFormation(true);
1394 :
1395 : // Update the held position so entities respond to orders.
1396 2 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
1397 2 : if (cmpPosition && cmpPosition.IsInWorld())
1398 : {
1399 2 : let pos = cmpPosition.GetPosition2D();
1400 2 : this.CallMemberFunction("SetHeldPosition", [pos.x, pos.y]);
1401 : }
1402 : },
1403 : },
1404 : },
1405 :
1406 :
1407 : // States for entities moving as part of a formation:
1408 : "FORMATIONMEMBER": {
1409 : "FormationLeave": function(msg) {
1410 : // Stop moving as soon as the formation disbands
1411 : // Keep current rotation
1412 3 : let facePointAfterMove = this.GetFacePointAfterMove();
1413 3 : this.SetFacePointAfterMove(false);
1414 3 : this.StopMoving();
1415 3 : this.SetFacePointAfterMove(facePointAfterMove);
1416 :
1417 : // If the controller handled an order but some members rejected it,
1418 : // they will have no orders and be in the FORMATIONMEMBER.IDLE state.
1419 3 : if (this.orderQueue.length)
1420 : {
1421 : // We're leaving the formation, so stop our FormationWalk order
1422 3 : if (this.FinishOrder())
1423 0 : return;
1424 : }
1425 :
1426 3 : this.formationAnimationVariant = undefined;
1427 3 : this.SetNextState("INDIVIDUAL.IDLE");
1428 : },
1429 :
1430 : // Override the LeaveFoundation order since we're not doing
1431 : // anything more important (and we might be stuck in the WALKING
1432 : // state forever and need to get out of foundations in that case)
1433 : "Order.LeaveFoundation": function(msg) {
1434 0 : if (!this.WillMoveFromFoundation(msg.data.target))
1435 0 : return this.FinishOrder();
1436 0 : msg.data.min = g_LeaveFoundationRange;
1437 0 : this.SetNextState("WALKINGTOPOINT");
1438 0 : return ACCEPT_ORDER;
1439 : },
1440 :
1441 : "enter": function() {
1442 11 : let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
1443 11 : if (cmpFormation)
1444 : {
1445 11 : this.formationAnimationVariant = cmpFormation.GetFormationAnimationVariant(this.entity);
1446 11 : if (this.formationAnimationVariant)
1447 0 : this.SetAnimationVariant(this.formationAnimationVariant);
1448 : else
1449 11 : this.SetDefaultAnimationVariant();
1450 : }
1451 11 : return false;
1452 : },
1453 :
1454 : "leave": function() {
1455 11 : this.SetDefaultAnimationVariant();
1456 11 : this.formationAnimationVariant = undefined;
1457 : },
1458 :
1459 : "IDLE": "INDIVIDUAL.IDLE",
1460 :
1461 : "CHEERING": "INDIVIDUAL.CHEERING",
1462 :
1463 : "WALKING": {
1464 : "enter": function() {
1465 11 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
1466 11 : cmpUnitMotion.MoveToFormationOffset(this.order.data.target, this.order.data.x, this.order.data.z);
1467 11 : if (this.order.data.offsetsChanged)
1468 : {
1469 11 : let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
1470 11 : if (cmpFormation)
1471 11 : this.formationAnimationVariant = cmpFormation.GetFormationAnimationVariant(this.entity);
1472 : }
1473 11 : if (this.formationAnimationVariant)
1474 0 : this.SetAnimationVariant(this.formationAnimationVariant);
1475 11 : else if (this.order.data.variant)
1476 0 : this.SetAnimationVariant(this.order.data.variant);
1477 : else
1478 11 : this.SetDefaultAnimationVariant();
1479 11 : return false;
1480 : },
1481 :
1482 : "leave": function() {
1483 : // Don't use the logic from unitMotion, as SetInPosition
1484 : // has already given us a custom rotation
1485 : // (or we failed to move and thus don't care.)
1486 11 : let facePointAfterMove = this.GetFacePointAfterMove();
1487 11 : this.SetFacePointAfterMove(false);
1488 11 : this.StopMoving();
1489 11 : this.SetFacePointAfterMove(facePointAfterMove);
1490 : },
1491 :
1492 : // Occurs when the unit has reached its destination and the controller
1493 : // is done moving. The controller is notified.
1494 : "MovementUpdate": function(msg) {
1495 : // When walking in formation, we'll only get notified in case of failure
1496 : // if the formation controller has stopped walking.
1497 : // Formations can start lagging a lot if many entities request short path
1498 : // so prefer to finish order early than retry pathing.
1499 : // (see https://code.wildfiregames.com/rP23806)
1500 : // (if the message is likelyFailure of likelySuccess, we also want to stop).
1501 0 : this.FinishOrder();
1502 : },
1503 : },
1504 :
1505 : // Special case used by Order.LeaveFoundation
1506 : "WALKINGTOPOINT": {
1507 : "enter": function() {
1508 0 : if (!this.MoveTo(this.order.data))
1509 : {
1510 0 : this.FinishOrder();
1511 0 : return true;
1512 : }
1513 0 : return false;
1514 : },
1515 :
1516 : "leave": function() {
1517 0 : this.StopMoving();
1518 : },
1519 :
1520 : "MovementUpdate": function() {
1521 0 : if (!this.CheckRange(this.order.data))
1522 0 : return;
1523 0 : this.FinishOrder();
1524 : },
1525 : },
1526 : },
1527 :
1528 :
1529 : // States for entities not part of a formation:
1530 : "INDIVIDUAL": {
1531 : "Attacked": function(msg) {
1532 0 : if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)
1533 0 : this.RespondToTargetedEntities([msg.data.attacker]);
1534 : },
1535 :
1536 : "GuardedAttacked": function(msg) {
1537 : // do nothing if we have a forced order in queue before the guard order
1538 0 : for (var i = 0; i < this.orderQueue.length; ++i)
1539 : {
1540 0 : if (this.orderQueue[i].type == "Guard")
1541 0 : break;
1542 0 : if (this.orderQueue[i].data && this.orderQueue[i].data.force)
1543 0 : return;
1544 : }
1545 : // if we already are targeting another unit still alive, finish with it first
1546 0 : if (this.order && (this.order.type == "WalkAndFight" || this.order.type == "Attack"))
1547 0 : if (this.order.data.target != msg.data.attacker && this.CanAttack(msg.data.attacker))
1548 0 : return;
1549 :
1550 0 : var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
1551 0 : var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
1552 0 : if (cmpIdentity && cmpIdentity.HasClass("Support") &&
1553 : cmpHealth && cmpHealth.IsInjured())
1554 : {
1555 0 : if (this.CanHeal(this.isGuardOf))
1556 0 : this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
1557 0 : else if (this.CanRepair(this.isGuardOf))
1558 0 : this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
1559 0 : return;
1560 : }
1561 :
1562 0 : var cmpBuildingAI = Engine.QueryInterface(msg.data.attacker, IID_BuildingAI);
1563 0 : if (cmpBuildingAI && this.CanRepair(this.isGuardOf))
1564 : {
1565 0 : this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
1566 0 : return;
1567 : }
1568 :
1569 0 : if (this.CheckTargetVisible(msg.data.attacker))
1570 0 : this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false });
1571 : else
1572 : {
1573 0 : var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
1574 0 : if (!cmpPosition || !cmpPosition.IsInWorld())
1575 0 : return;
1576 0 : var pos = cmpPosition.GetPosition();
1577 0 : this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false });
1578 : // if we already had a WalkAndFight, keep only the most recent one in case the target has moved
1579 0 : if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight")
1580 : {
1581 0 : this.orderQueue.splice(1, 1);
1582 0 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
1583 : }
1584 : }
1585 : },
1586 :
1587 : "IDLE": {
1588 : "Order.Cheer": function() {
1589 : // Do not cheer if there is no cheering time and we are not idle yet.
1590 0 : if (!this.cheeringTime || !this.isIdle)
1591 0 : return this.FinishOrder();
1592 :
1593 0 : this.SetNextState("CHEERING");
1594 0 : return ACCEPT_ORDER;
1595 : },
1596 :
1597 : "enter": function() {
1598 : // Switch back to idle animation to guarantee we won't
1599 : // get stuck with an incorrect animation
1600 19 : this.SelectAnimation("idle");
1601 :
1602 : // Idle is the default state. If units try, from the IDLE.enter sub-state, to
1603 : // begin another order, and that order fails (calling FinishOrder), they might
1604 : // end up in an infinite loop. To avoid this, all methods that could put the unit in
1605 : // a new state are done on the next turn.
1606 : // This wastes a turn but avoids infinite loops.
1607 : // Further, the GUI and AI want to know when a unit is idle,
1608 : // but sending this info in Idle.enter will send spurious messages.
1609 : // Pick 100 to execute on the next turn in SP and MP.
1610 19 : this.StartTimer(100);
1611 19 : return false;
1612 : },
1613 :
1614 : "leave": function() {
1615 15 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1616 15 : if (this.losRangeQuery)
1617 0 : cmpRangeManager.DisableActiveQuery(this.losRangeQuery);
1618 15 : if (this.losHealRangeQuery)
1619 0 : cmpRangeManager.DisableActiveQuery(this.losHealRangeQuery);
1620 15 : if (this.losAttackRangeQuery)
1621 12 : cmpRangeManager.DisableActiveQuery(this.losAttackRangeQuery);
1622 :
1623 15 : this.StopTimer();
1624 :
1625 15 : if (this.isIdle)
1626 : {
1627 14 : if (this.IsFormationMember())
1628 11 : Engine.QueryInterface(this.formationController, IID_Formation).UnsetIdleEntity(this.entity);
1629 14 : this.isIdle = false;
1630 14 : Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
1631 : }
1632 : },
1633 :
1634 : "Attacked": function(msg) {
1635 0 : if (this.isIdle && (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force))
1636 0 : this.RespondToTargetedEntities([msg.data.attacker]);
1637 : },
1638 :
1639 : // On the range updates:
1640 : // We check for idleness to prevent an entity to react only to newly seen entities
1641 : // when receiving a Los*RangeUpdate on the same turn as the entity becomes idle
1642 : // since this.FindNew*Targets is called in the timer.
1643 :
1644 : "LosRangeUpdate": function(msg) {
1645 0 : if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length)
1646 0 : this.RespondToSightedEntities(msg.data.added);
1647 : },
1648 :
1649 : "LosHealRangeUpdate": function(msg) {
1650 0 : if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length)
1651 0 : this.RespondToHealableEntities(msg.data.added);
1652 : },
1653 :
1654 : "LosAttackRangeUpdate": function(msg) {
1655 0 : if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length && this.GetStance().targetVisibleEnemies)
1656 0 : this.AttackEntitiesByPreference(msg.data.added);
1657 : },
1658 :
1659 : "Timer": function(msg) {
1660 3 : if (this.isGuardOf)
1661 : {
1662 0 : this.Guard(this.isGuardOf, false);
1663 0 : return;
1664 : }
1665 :
1666 : // If a unit can heal and attack we first want to heal wounded units,
1667 : // so check if we are a healer and find whether there's anybody nearby to heal.
1668 : // (If anyone approaches later it'll be handled via LosHealRangeUpdate.)
1669 : // If anyone in sight gets hurt that will be handled via LosHealRangeUpdate.
1670 3 : if (this.IsHealer() && this.FindNewHealTargets())
1671 0 : return;
1672 :
1673 : // If we entered the idle state we must have nothing better to do,
1674 : // so immediately check whether there's anybody nearby to attack.
1675 : // (If anyone approaches later, it'll be handled via LosAttackRangeUpdate.)
1676 3 : if (this.FindNewTargets())
1677 1 : return;
1678 :
1679 2 : if (this.FindSightedEnemies())
1680 0 : return;
1681 :
1682 2 : if (!this.isIdle)
1683 : {
1684 : // Move back to the held position if we drifted away.
1685 : // (only if not a formation member).
1686 2 : if (!this.IsFormationMember() &&
1687 : this.GetStance().respondHoldGround && this.heldPosition &&
1688 : !this.CheckPointRangeExplicit(this.heldPosition.x, this.heldPosition.z, 0, 10) &&
1689 : this.WalkToHeldPosition())
1690 0 : return;
1691 :
1692 2 : if (this.IsFormationMember())
1693 : {
1694 0 : let cmpFormationAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
1695 0 : if (!cmpFormationAI || !cmpFormationAI.IsIdle())
1696 0 : return;
1697 0 : Engine.QueryInterface(this.formationController, IID_Formation).SetIdleEntity(this.entity);
1698 : }
1699 :
1700 2 : this.isIdle = true;
1701 2 : Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
1702 : }
1703 :
1704 : // Go linger first to prevent all roaming entities
1705 : // to move all at the same time on map init.
1706 2 : if (this.template.RoamDistance)
1707 0 : this.SetNextState("LINGERING");
1708 : },
1709 :
1710 : "ROAMING": {
1711 : "enter": function() {
1712 0 : this.SetFacePointAfterMove(false);
1713 0 : this.MoveRandomly(+this.template.RoamDistance);
1714 0 : this.StartTimer(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
1715 0 : return false;
1716 : },
1717 :
1718 : "leave": function() {
1719 0 : this.StopMoving();
1720 0 : this.StopTimer();
1721 0 : this.SetFacePointAfterMove(true);
1722 : },
1723 :
1724 : "Timer": function(msg) {
1725 0 : this.SetNextState("LINGERING");
1726 : },
1727 :
1728 : "MovementUpdate": function() {
1729 0 : this.MoveRandomly(+this.template.RoamDistance);
1730 : },
1731 : },
1732 :
1733 : "LINGERING": {
1734 : "enter": function() {
1735 : // ToDo: rename animations?
1736 0 : this.SelectAnimation("feeding");
1737 0 : this.StartTimer(randIntInclusive(+this.template.FeedTimeMin, +this.template.FeedTimeMax));
1738 0 : return false;
1739 : },
1740 :
1741 : "leave": function() {
1742 0 : this.ResetAnimation();
1743 0 : this.StopTimer();
1744 : },
1745 :
1746 : "Timer": function(msg) {
1747 0 : this.SetNextState("ROAMING");
1748 : },
1749 : },
1750 : },
1751 :
1752 : "WALKING": {
1753 : "enter": function() {
1754 0 : if (!this.MoveTo(this.order.data))
1755 : {
1756 0 : this.FinishOrder();
1757 0 : return true;
1758 : }
1759 0 : return false;
1760 : },
1761 :
1762 : "leave": function() {
1763 0 : this.StopMoving();
1764 : },
1765 :
1766 : "MovementUpdate": function(msg) {
1767 : // If it looks like the path is failing, and we are close enough stop anyways.
1768 : // This avoids pathing for an unreachable goal and reduces lag considerably.
1769 0 : if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
1770 : this.CheckRange(this.order.data))
1771 0 : this.FinishOrder();
1772 : },
1773 : },
1774 :
1775 : "WALKINGANDFIGHTING": {
1776 : "enter": function() {
1777 0 : if (!this.MoveTo(this.order.data))
1778 : {
1779 0 : this.FinishOrder();
1780 0 : return true;
1781 : }
1782 : // Show weapons rather than carried resources.
1783 0 : this.SetAnimationVariant("combat");
1784 :
1785 0 : this.StartTimer(0, 1000);
1786 0 : return false;
1787 : },
1788 :
1789 : "Timer": function(msg) {
1790 0 : this.FindWalkAndFightTargets();
1791 : },
1792 :
1793 : "leave": function(msg) {
1794 0 : this.StopMoving();
1795 0 : this.StopTimer();
1796 0 : this.SetDefaultAnimationVariant();
1797 : },
1798 :
1799 : "MovementUpdate": function(msg) {
1800 : // If it looks like the path is failing, and we are close enough stop anyways.
1801 : // This avoids pathing for an unreachable goal and reduces lag considerably.
1802 0 : if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
1803 : this.CheckRange(this.order.data))
1804 0 : this.FinishOrder();
1805 : },
1806 : },
1807 :
1808 : "PATROL": {
1809 : "enter": function() {
1810 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
1811 0 : if (!cmpPosition || !cmpPosition.IsInWorld())
1812 : {
1813 0 : this.FinishOrder();
1814 0 : return true;
1815 : }
1816 :
1817 : // Memorize the origin position in case that we want to go back.
1818 0 : if (!this.patrolStartPosOrder)
1819 : {
1820 0 : this.patrolStartPosOrder = cmpPosition.GetPosition();
1821 0 : this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses;
1822 0 : this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture;
1823 : }
1824 :
1825 0 : this.SetAnimationVariant("combat");
1826 :
1827 0 : return false;
1828 : },
1829 :
1830 : "leave": function() {
1831 0 : delete this.patrolStartPosOrder;
1832 0 : this.SetDefaultAnimationVariant();
1833 : },
1834 :
1835 : "PATROLLING": {
1836 : "enter": function() {
1837 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
1838 0 : if (!cmpPosition || !cmpPosition.IsInWorld() ||
1839 : !this.MoveTo(this.order.data))
1840 : {
1841 0 : this.FinishOrder();
1842 0 : return true;
1843 : }
1844 0 : this.StartTimer(0, 1000);
1845 0 : return false;
1846 : },
1847 :
1848 : "leave": function() {
1849 0 : this.StopMoving();
1850 0 : this.StopTimer();
1851 : },
1852 :
1853 : "Timer": function(msg) {
1854 0 : this.FindWalkAndFightTargets();
1855 : },
1856 :
1857 : "MovementUpdate": function(msg) {
1858 0 : if (!msg.likelyFailure && !msg.likelySuccess && !this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange))
1859 0 : return;
1860 :
1861 0 : if (this.orderQueue.length == 1)
1862 0 : this.PushOrder("Patrol", this.patrolStartPosOrder);
1863 :
1864 0 : this.PushOrder(this.order.type, this.order.data);
1865 0 : this.SetNextState("CHECKINGWAYPOINT");
1866 : },
1867 : },
1868 :
1869 : "CHECKINGWAYPOINT": {
1870 : "enter": function() {
1871 0 : this.StartTimer(0, 1000);
1872 0 : this.stopSurveying = 0;
1873 : // TODO: pick a proper animation
1874 0 : return false;
1875 : },
1876 :
1877 : "leave": function() {
1878 0 : this.StopTimer();
1879 0 : delete this.stopSurveying;
1880 : },
1881 :
1882 : "Timer": function(msg) {
1883 0 : if (this.stopSurveying >= +this.template.PatrolWaitTime)
1884 : {
1885 0 : this.FinishOrder();
1886 0 : return;
1887 : }
1888 0 : if (!this.FindWalkAndFightTargets())
1889 0 : ++this.stopSurveying;
1890 : }
1891 : }
1892 : },
1893 :
1894 : "GUARD": {
1895 : "RemoveGuard": function() {
1896 0 : this.FinishOrder();
1897 : },
1898 :
1899 : "ESCORTING": {
1900 : "enter": function() {
1901 0 : if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
1902 : {
1903 0 : this.FinishOrder();
1904 0 : return true;
1905 : }
1906 :
1907 : // Show weapons rather than carried resources.
1908 0 : this.SetAnimationVariant("combat");
1909 :
1910 0 : this.StartTimer(0, 1000);
1911 0 : this.SetHeldPositionOnEntity(this.isGuardOf);
1912 0 : return false;
1913 : },
1914 :
1915 : "Timer": function(msg) {
1916 0 : if (!this.ShouldGuard(this.isGuardOf))
1917 : {
1918 0 : this.FinishOrder();
1919 0 : return;
1920 : }
1921 :
1922 0 : let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
1923 0 : if (cmpObstructionManager.IsInTargetRange(this.entity, this.isGuardOf, 0, 3 * this.guardRange, false))
1924 0 : this.TryMatchTargetSpeed(this.isGuardOf, false);
1925 :
1926 0 : this.SetHeldPositionOnEntity(this.isGuardOf);
1927 : },
1928 :
1929 : "leave": function(msg) {
1930 0 : this.StopMoving();
1931 0 : this.ResetSpeedMultiplier();
1932 0 : this.StopTimer();
1933 0 : this.SetDefaultAnimationVariant();
1934 : },
1935 :
1936 : "MovementUpdate": function(msg) {
1937 0 : if (msg.likelyFailure || this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
1938 0 : this.SetNextState("GUARDING");
1939 : },
1940 : },
1941 :
1942 : "GUARDING": {
1943 : "enter": function() {
1944 0 : this.StartTimer(1000, 1000);
1945 0 : this.SetHeldPositionOnEntity(this.entity);
1946 0 : this.SetAnimationVariant("combat");
1947 0 : this.FaceTowardsTarget(this.order.data.target);
1948 0 : return false;
1949 : },
1950 :
1951 : "LosAttackRangeUpdate": function(msg) {
1952 0 : if (this.GetStance().targetVisibleEnemies)
1953 0 : this.AttackEntitiesByPreference(msg.data.added);
1954 : },
1955 :
1956 : "Timer": function(msg) {
1957 0 : if (!this.ShouldGuard(this.isGuardOf))
1958 : {
1959 0 : this.FinishOrder();
1960 0 : return;
1961 : }
1962 : // TODO: find out what to do if we cannot move.
1963 0 : if (!this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange) &&
1964 : this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
1965 0 : this.SetNextState("ESCORTING");
1966 : else
1967 : {
1968 0 : this.FaceTowardsTarget(this.order.data.target);
1969 0 : var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
1970 0 : if (cmpHealth && cmpHealth.IsInjured())
1971 : {
1972 0 : if (this.CanHeal(this.isGuardOf))
1973 0 : this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
1974 0 : else if (this.CanRepair(this.isGuardOf))
1975 0 : this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
1976 : }
1977 : }
1978 : },
1979 :
1980 : "leave": function(msg) {
1981 0 : this.StopTimer();
1982 0 : this.SetDefaultAnimationVariant();
1983 : },
1984 : },
1985 : },
1986 :
1987 : "FLEEING": {
1988 : "enter": function() {
1989 : // We use the distance between the entities to account for ranged attacks
1990 1 : this.order.data.distanceToFlee = PositionHelper.DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
1991 1 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
1992 : // Use unit motion directly to ignore the visibility check. TODO: change this if we add LOS to fauna.
1993 1 : if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) ||
1994 : !cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1))
1995 : {
1996 0 : this.FinishOrder();
1997 0 : return true;
1998 : }
1999 :
2000 1 : this.PlaySound("panic");
2001 :
2002 1 : this.SetSpeedMultiplier(this.GetRunMultiplier());
2003 1 : return false;
2004 : },
2005 :
2006 : "OrderTargetRenamed": function(msg) {
2007 : // To avoid replaying the panic sound, handle this explicitly.
2008 1 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
2009 1 : if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) ||
2010 : !cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1))
2011 0 : this.FinishOrder();
2012 : },
2013 :
2014 : "Attacked": function(msg) {
2015 0 : if (msg.data.attacker == this.order.data.target)
2016 0 : return;
2017 :
2018 0 : let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
2019 0 : if (cmpObstructionManager.DistanceToTarget(this.entity, msg.data.target) > cmpObstructionManager.DistanceToTarget(this.entity, this.order.data.target))
2020 0 : return;
2021 :
2022 0 : if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)
2023 0 : this.RespondToTargetedEntities([msg.data.attacker]);
2024 : },
2025 :
2026 : "leave": function() {
2027 0 : this.ResetSpeedMultiplier();
2028 0 : this.StopMoving();
2029 : },
2030 :
2031 : "MovementUpdate": function(msg) {
2032 0 : if (msg.likelyFailure || this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1))
2033 0 : this.FinishOrder();
2034 : },
2035 : },
2036 :
2037 : "COMBAT": {
2038 : "Order.LeaveFoundation": function(msg) {
2039 : // Ignore the order as we're busy.
2040 0 : return this.FinishOrder();
2041 : },
2042 :
2043 : "Attacked": function(msg) {
2044 : // If we're already in combat mode, ignore anyone else who's attacking us
2045 : // unless it's a melee attack since they may be blocking our way to the target
2046 0 : if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force))
2047 0 : this.RespondToTargetedEntities([msg.data.attacker]);
2048 : },
2049 :
2050 : "leave": function() {
2051 8 : if (!this.formationAnimationVariant)
2052 8 : this.SetDefaultAnimationVariant();
2053 : },
2054 :
2055 : "APPROACHING": {
2056 : "enter": function() {
2057 0 : if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
2058 : {
2059 0 : this.FinishOrder();
2060 0 : return true;
2061 : }
2062 :
2063 0 : if (!this.formationAnimationVariant)
2064 0 : this.SetAnimationVariant("combat");
2065 :
2066 0 : this.StartTimer(1000, 1000);
2067 0 : return false;
2068 : },
2069 :
2070 : "leave": function() {
2071 0 : this.StopMoving();
2072 0 : this.StopTimer();
2073 : },
2074 :
2075 : "Timer": function(msg) {
2076 0 : if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
2077 : {
2078 0 : this.FinishOrder();
2079 :
2080 0 : if (this.GetStance().respondHoldGround)
2081 0 : this.WalkToHeldPosition();
2082 : }
2083 : else
2084 : {
2085 0 : this.RememberTargetPosition();
2086 0 : if (this.order.data.hunting && this.orderQueue.length > 1 &&
2087 : this.orderQueue[1].type === "Gather")
2088 0 : this.RememberTargetPosition(this.orderQueue[1].data);
2089 : }
2090 : },
2091 :
2092 : "MovementUpdate": function(msg) {
2093 0 : if (msg.likelyFailure)
2094 : {
2095 : // This also handles hunting.
2096 0 : if (this.orderQueue.length > 1)
2097 : {
2098 0 : this.FinishOrder();
2099 0 : return;
2100 : }
2101 0 : else if (!this.order.data.force || !this.order.data.lastPos)
2102 : {
2103 0 : this.SetNextState("COMBAT.FINDINGNEWTARGET");
2104 0 : return;
2105 : }
2106 : // If the order was forced, try moving to the target position,
2107 : // under the assumption that this is desirable if the target
2108 : // was somewhat far away - we'll likely end up closer to where
2109 : // the player hoped we would.
2110 0 : let lastPos = this.order.data.lastPos;
2111 0 : this.PushOrder("WalkAndFight", {
2112 : "x": lastPos.x, "z": lastPos.z,
2113 : "force": false,
2114 : });
2115 0 : return;
2116 : }
2117 :
2118 0 : if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
2119 : {
2120 0 : if (this.CanUnpack())
2121 : {
2122 0 : this.PushOrderFront("Unpack", { "force": true });
2123 0 : return;
2124 : }
2125 0 : this.SetNextState("ATTACKING");
2126 : }
2127 0 : else if (msg.likelySuccess)
2128 : // Try moving again,
2129 : // attack range uses a height-related formula and our actual max range might have changed.
2130 0 : if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
2131 0 : this.FinishOrder();
2132 : },
2133 : },
2134 :
2135 : "ATTACKING": {
2136 : "enter": function() {
2137 17 : let target = this.order.data.target;
2138 17 : let cmpFormation = Engine.QueryInterface(target, IID_Formation);
2139 17 : if (cmpFormation)
2140 : {
2141 0 : this.order.data.formationTarget = target;
2142 0 : target = cmpFormation.GetClosestMember(this.entity);
2143 0 : this.order.data.target = target;
2144 : }
2145 :
2146 17 : this.shouldCheer = false;
2147 :
2148 17 : let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
2149 17 : if (!cmpAttack)
2150 : {
2151 0 : this.FinishOrder();
2152 0 : return true;
2153 : }
2154 :
2155 17 : if (!this.CheckTargetAttackRange(target, this.order.data.attackType))
2156 : {
2157 0 : if (this.CanPack())
2158 : {
2159 0 : this.PushOrderFront("Pack", { "force": true });
2160 0 : return true;
2161 : }
2162 :
2163 0 : this.ProcessMessage("OutOfRange");
2164 0 : return true;
2165 : }
2166 :
2167 17 : if (!this.formationAnimationVariant)
2168 17 : this.SetAnimationVariant("combat");
2169 :
2170 17 : this.FaceTowardsTarget(this.order.data.target);
2171 :
2172 17 : this.RememberTargetPosition();
2173 17 : if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather")
2174 0 : this.RememberTargetPosition(this.orderQueue[1].data);
2175 :
2176 17 : if (!cmpAttack.StartAttacking(this.order.data.target, this.order.data.attackType, IID_UnitAI))
2177 : {
2178 0 : this.ProcessMessage("TargetInvalidated");
2179 0 : return true;
2180 : }
2181 :
2182 17 : let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
2183 17 : if (cmpBuildingAI)
2184 : {
2185 0 : cmpBuildingAI.SetUnitAITarget(this.order.data.target);
2186 0 : return false;
2187 : }
2188 :
2189 17 : let cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
2190 :
2191 : // Units with no cheering time do not cheer.
2192 17 : this.shouldCheer = cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()) && this.cheeringTime > 0;
2193 :
2194 17 : return false;
2195 : },
2196 :
2197 : "leave": function() {
2198 8 : let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
2199 8 : if (cmpBuildingAI)
2200 0 : cmpBuildingAI.SetUnitAITarget(0);
2201 8 : let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
2202 8 : if (cmpAttack)
2203 8 : cmpAttack.StopAttacking();
2204 : },
2205 :
2206 : "OutOfRange": function() {
2207 0 : if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
2208 : {
2209 0 : if (this.CanPack())
2210 : {
2211 0 : this.PushOrderFront("Pack", { "force": true });
2212 0 : return;
2213 : }
2214 0 : this.SetNextState("CHASING");
2215 0 : return;
2216 : }
2217 0 : this.SetNextState("FINDINGNEWTARGET");
2218 : },
2219 :
2220 : "TargetInvalidated": function() {
2221 0 : this.SetNextState("FINDINGNEWTARGET");
2222 : },
2223 :
2224 : "Attacked": function(msg) {
2225 0 : if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force) &&
2226 : this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture")
2227 0 : this.RespondToTargetedEntities([msg.data.attacker]);
2228 : },
2229 : },
2230 :
2231 : "FINDINGNEWTARGET": {
2232 : "Order.Cheer": function() {
2233 0 : if (!this.cheeringTime)
2234 0 : return this.FinishOrder();
2235 :
2236 0 : this.SetNextState("CHEERING");
2237 0 : return ACCEPT_ORDER;
2238 : },
2239 :
2240 : "enter": function() {
2241 : // Try to find the formation the target was a part of.
2242 0 : let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation);
2243 0 : if (!cmpFormation)
2244 0 : cmpFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation);
2245 :
2246 : // If the target is a formation, pick closest member.
2247 0 : if (cmpFormation)
2248 : {
2249 0 : let filter = (t) => this.CanAttack(t);
2250 0 : this.order.data.formationTarget = this.order.data.target;
2251 0 : let target = cmpFormation.GetClosestMember(this.entity, filter);
2252 0 : this.order.data.target = target;
2253 0 : this.SetNextState("COMBAT.ATTACKING");
2254 0 : return true;
2255 : }
2256 :
2257 : // Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up
2258 : // except if in WalkAndFight mode where we look for more enemies around before moving again.
2259 0 : if (this.FinishOrder())
2260 : {
2261 0 : if (this.IsWalkingAndFighting())
2262 : {
2263 0 : Engine.ProfileStart("FindWalkAndFightTargets");
2264 0 : this.FindWalkAndFightTargets();
2265 0 : Engine.ProfileStop();
2266 : }
2267 0 : return true;
2268 : }
2269 :
2270 0 : if (this.FindNewTargets())
2271 0 : return true;
2272 :
2273 0 : if (this.GetStance().respondHoldGround)
2274 0 : this.WalkToHeldPosition();
2275 :
2276 0 : if (this.shouldCheer)
2277 : {
2278 0 : this.Cheer();
2279 0 : this.CallPlayerOwnedEntitiesFunctionInRange("Cheer", [], this.notifyToCheerInRange);
2280 : }
2281 :
2282 0 : return true;
2283 : },
2284 : },
2285 :
2286 : "CHASING": {
2287 : "Order.MoveToChasingPoint": function(msg) {
2288 0 : if (this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, msg.data.max) || !this.AbleToMove())
2289 0 : return this.FinishOrder();
2290 0 : msg.data.relaxed = true;
2291 0 : this.StopTimer();
2292 0 : this.SetNextState("MOVINGTOPOINT");
2293 0 : return ACCEPT_ORDER;
2294 : },
2295 : "enter": function() {
2296 0 : if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
2297 : {
2298 0 : this.FinishOrder();
2299 0 : return true;
2300 : }
2301 :
2302 0 : if (!this.formationAnimationVariant)
2303 0 : this.SetAnimationVariant("combat");
2304 :
2305 0 : var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
2306 0 : if (cmpUnitAI && cmpUnitAI.IsFleeing())
2307 0 : this.SetSpeedMultiplier(this.GetRunMultiplier());
2308 :
2309 0 : this.StartTimer(1000, 1000);
2310 0 : return false;
2311 : },
2312 :
2313 : "leave": function() {
2314 0 : this.ResetSpeedMultiplier();
2315 0 : this.StopMoving();
2316 0 : this.StopTimer();
2317 : },
2318 :
2319 : "Timer": function(msg) {
2320 0 : if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
2321 : {
2322 0 : this.FinishOrder();
2323 :
2324 0 : if (this.GetStance().respondHoldGround)
2325 0 : this.WalkToHeldPosition();
2326 : }
2327 : else
2328 : {
2329 0 : this.RememberTargetPosition();
2330 0 : if (this.order.data.hunting && this.orderQueue.length > 1 &&
2331 : this.orderQueue[1].type === "Gather")
2332 0 : this.RememberTargetPosition(this.orderQueue[1].data);
2333 : }
2334 : },
2335 :
2336 : "MovementUpdate": function(msg) {
2337 0 : if (msg.likelyFailure)
2338 : {
2339 : // This also handles hunting.
2340 0 : if (this.orderQueue.length > 1)
2341 : {
2342 0 : this.FinishOrder();
2343 0 : return;
2344 : }
2345 0 : else if (!this.order.data.force)
2346 : {
2347 0 : this.SetNextState("COMBAT.FINDINGNEWTARGET");
2348 0 : return;
2349 : }
2350 0 : else if (this.order.data.lastPos)
2351 : {
2352 0 : let lastPos = this.order.data.lastPos;
2353 0 : let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
2354 0 : this.PushOrder("MoveToChasingPoint", {
2355 : "x": lastPos.x,
2356 : "z": lastPos.z,
2357 : "max": cmpAttack.GetRange(this.order.data.attackType).max,
2358 : "force": true
2359 : });
2360 0 : return;
2361 : }
2362 : }
2363 0 : if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
2364 : {
2365 0 : if (this.CanUnpack())
2366 : {
2367 0 : this.PushOrderFront("Unpack", { "force": true });
2368 0 : return;
2369 : }
2370 0 : this.SetNextState("ATTACKING");
2371 : }
2372 0 : else if (msg.likelySuccess)
2373 : // Try moving again,
2374 : // attack range uses a height-related formula and our actual max range might have changed.
2375 0 : if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
2376 0 : this.FinishOrder();
2377 : },
2378 : "MOVINGTOPOINT": {
2379 : "enter": function() {
2380 0 : if (!this.MoveTo(this.order.data))
2381 : {
2382 0 : this.FinishOrder();
2383 0 : return true;
2384 : }
2385 0 : return false;
2386 : },
2387 : "leave": function() {
2388 0 : this.StopMoving();
2389 : },
2390 : "MovementUpdate": function(msg) {
2391 : // If it looks like the path is failing, and we are close enough from wanted range
2392 : // stop anyways. This avoids pathing for an unreachable goal and reduces lag considerably.
2393 0 : if (msg.likelyFailure ||
2394 : msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.order.data.max + this.DefaultRelaxedMaxRange) ||
2395 : !msg.obstructed && this.CheckRange(this.order.data))
2396 0 : this.FinishOrder();
2397 : },
2398 : },
2399 : },
2400 : },
2401 :
2402 : "GATHER": {
2403 : "enter": function() {
2404 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
2405 0 : if (cmpResourceGatherer)
2406 0 : cmpResourceGatherer.AddToPlayerCounter(this.order.data.type.generic);
2407 0 : return false;
2408 : },
2409 :
2410 : "leave": function() {
2411 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
2412 0 : if (cmpResourceGatherer)
2413 0 : cmpResourceGatherer.RemoveFromPlayerCounter();
2414 :
2415 : // Show the carried resource, if we've gathered anything.
2416 0 : this.SetDefaultAnimationVariant();
2417 : },
2418 :
2419 : "APPROACHING": {
2420 : "enter": function() {
2421 0 : this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave".
2422 0 : if (this.CheckRange(this.order.data, IID_ResourceGatherer))
2423 : {
2424 0 : this.SetNextState("GATHERING");
2425 0 : return true;
2426 : }
2427 :
2428 : // If we can't move, assume we'll fail any subsequent order
2429 : // and finish the order entirely to avoid an infinite loop.
2430 0 : if (!this.AbleToMove())
2431 : {
2432 0 : this.FinishOrder();
2433 0 : return true;
2434 : }
2435 :
2436 0 : let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
2437 0 : let cmpMirage = Engine.QueryInterface(this.gatheringTarget, IID_Mirage);
2438 0 : if ((!cmpMirage || !cmpMirage.Mirages(IID_ResourceSupply)) &&
2439 : (!cmpSupply || !cmpSupply.AddGatherer(this.entity)) ||
2440 : !this.MoveTo(this.order.data, IID_ResourceGatherer))
2441 : {
2442 : // If the target's last known position is in FOW, try going there
2443 : // and hope that we might find it then.
2444 0 : let lastPos = this.order.data.lastPos;
2445 0 : if (this.gatheringTarget != INVALID_ENTITY &&
2446 : lastPos && !this.CheckPositionVisible(lastPos.x, lastPos.z))
2447 : {
2448 0 : this.PushOrderFront("Walk", {
2449 : "x": lastPos.x, "z": lastPos.z,
2450 : "force": this.order.data.force
2451 : });
2452 0 : return true;
2453 : }
2454 0 : this.SetNextState("FINDINGNEWTARGET");
2455 0 : return true;
2456 : }
2457 0 : this.SetAnimationVariant("approach_" + this.order.data.type.specific);
2458 0 : return false;
2459 : },
2460 :
2461 : "MovementUpdate": function(msg) {
2462 : // The GATHERING timer will handle finding a valid resource.
2463 0 : if (msg.likelyFailure)
2464 0 : this.SetNextState("FINDINGNEWTARGET");
2465 0 : else if (this.CheckRange(this.order.data, IID_ResourceGatherer))
2466 0 : this.SetNextState("GATHERING");
2467 : },
2468 :
2469 : "leave": function() {
2470 0 : this.StopMoving();
2471 0 : this.SetDefaultAnimationVariant();
2472 :
2473 0 : if (!this.gatheringTarget)
2474 0 : return;
2475 :
2476 0 : let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
2477 0 : if (cmpSupply)
2478 0 : cmpSupply.RemoveGatherer(this.entity);
2479 :
2480 0 : delete this.gatheringTarget;
2481 : },
2482 : },
2483 :
2484 : // Walking to a good place to gather resources near, used by GatherNearPosition
2485 : "WALKING": {
2486 : "enter": function() {
2487 0 : if (!this.MoveTo(this.order.data))
2488 : {
2489 0 : this.FinishOrder();
2490 0 : return true;
2491 : }
2492 0 : this.SetAnimationVariant("approach_" + this.order.data.type.specific);
2493 0 : return false;
2494 : },
2495 :
2496 : "leave": function() {
2497 0 : this.StopMoving();
2498 0 : this.SetDefaultAnimationVariant();
2499 : },
2500 :
2501 : "MovementUpdate": function(msg) {
2502 0 : if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
2503 : this.CheckRange(this.order.data))
2504 0 : this.SetNextState("FINDINGNEWTARGET");
2505 : },
2506 : },
2507 :
2508 : "GATHERING": {
2509 : "enter": function() {
2510 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
2511 0 : if (!cmpResourceGatherer)
2512 : {
2513 0 : this.FinishOrder();
2514 0 : return true;
2515 : }
2516 :
2517 0 : if (!this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
2518 : {
2519 0 : this.ProcessMessage("OutOfRange");
2520 0 : return true;
2521 : }
2522 :
2523 : // If this order was forced, the player probably gave it, but now we've reached the target
2524 : // switch to an unforced order (can be interrupted by attacks)
2525 0 : this.order.data.force = false;
2526 0 : this.order.data.autoharvest = true;
2527 :
2528 0 : this.FaceTowardsTarget(this.order.data.target);
2529 0 : if (!cmpResourceGatherer.StartGathering(this.order.data.target, IID_UnitAI))
2530 : {
2531 0 : this.ProcessMessage("TargetInvalidated");
2532 0 : return true;
2533 : }
2534 :
2535 0 : return false;
2536 : },
2537 :
2538 : "leave": function() {
2539 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
2540 0 : if (cmpResourceGatherer)
2541 0 : cmpResourceGatherer.StopGathering();
2542 : },
2543 :
2544 : "InventoryFilled": function(msg) {
2545 0 : this.SetNextState("RETURNINGRESOURCE");
2546 : },
2547 :
2548 : "OutOfRange": function(msg) {
2549 0 : if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
2550 0 : this.SetNextState("APPROACHING");
2551 : // Our target is no longer visible - go to its last known position first
2552 : // and then hopefully it will become visible.
2553 0 : else if (!this.CheckTargetVisible(this.order.data.target) && this.order.data.lastPos)
2554 0 : this.PushOrderFront("Walk", {
2555 : "x": this.order.data.lastPos.x,
2556 : "z": this.order.data.lastPos.z,
2557 : "force": this.order.data.force
2558 : });
2559 : else
2560 0 : this.SetNextState("FINDINGNEWTARGET");
2561 : },
2562 :
2563 : "TargetInvalidated": function(msg) {
2564 0 : this.SetNextState("FINDINGNEWTARGET");
2565 : },
2566 : },
2567 :
2568 : "FINDINGNEWTARGET": {
2569 : "enter": function() {
2570 0 : const previousForced = this.order.data.force;
2571 0 : let previousTarget = this.order.data.target;
2572 0 : let resourceTemplate = this.order.data.template;
2573 0 : let resourceType = this.order.data.type;
2574 :
2575 : // Give up on this order and try our next queued order
2576 : // but first check what is our next order and, if needed, insert a returnResource order
2577 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
2578 0 : if (cmpResourceGatherer.IsCarrying(resourceType.generic) &&
2579 : this.orderQueue.length > 1 && this.orderQueue[1] !== "ReturnResource" &&
2580 : (this.orderQueue[1].type !== "Gather" || this.orderQueue[1].data.type.generic !== resourceType.generic))
2581 : {
2582 0 : let nearestDropsite = this.FindNearestDropsite(resourceType.generic);
2583 0 : if (nearestDropsite)
2584 0 : this.orderQueue.splice(1, 0, { "type": "ReturnResource", "data": { "target": nearestDropsite, "force": false } });
2585 : }
2586 :
2587 : // Must go before FinishOrder or this.order will be undefined.
2588 0 : let initPos = this.order.data.initPos;
2589 :
2590 0 : if (this.FinishOrder())
2591 0 : return true;
2592 :
2593 : // No remaining orders - pick a useful default behaviour
2594 :
2595 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
2596 0 : if (!cmpPosition || !cmpPosition.IsInWorld())
2597 0 : return true;
2598 :
2599 0 : let filter = (ent, type, template) => {
2600 0 : if (previousTarget == ent)
2601 0 : return false;
2602 :
2603 : // Don't switch to a different type of huntable animal.
2604 0 : return type.specific == resourceType.specific &&
2605 : (type.specific != "meat" || resourceTemplate == template);
2606 : };
2607 :
2608 : // Current position is often next to a dropsite.
2609 : // But don't use that on forced orders, as the order may want us to go
2610 : // to the other side of the map on purpose.
2611 0 : let pos = cmpPosition.GetPosition();
2612 : let nearbyResource;
2613 0 : if (!previousForced)
2614 0 : nearbyResource = this.FindNearbyResource(Vector2D.from3D(pos), filter);
2615 :
2616 : // If there is an initPos, search there as well when we haven't found anything.
2617 : // Otherwise set initPos to our current pos.
2618 0 : if (!initPos)
2619 0 : initPos = { 'x': pos.X, 'z': pos.Z };
2620 0 : else if (!nearbyResource || previousForced)
2621 0 : nearbyResource = this.FindNearbyResource(new Vector2D(initPos.x, initPos.z), filter);
2622 :
2623 0 : if (nearbyResource)
2624 : {
2625 0 : this.PerformGather(nearbyResource, false, false);
2626 0 : return true;
2627 : }
2628 :
2629 : // Failing that, try to move there and se if we are more lucky: maybe there are resources in FOW.
2630 : // Only move if we are some distance away (TODO: pick the distance better?).
2631 : // Using the default relaxed range check since that is used in the WALKING-state.
2632 0 : if (!this.CheckPointRangeExplicit(initPos.x, initPos.z, 0, this.DefaultRelaxedMaxRange))
2633 : {
2634 0 : this.GatherNearPosition(initPos.x, initPos.z, resourceType, resourceTemplate);
2635 0 : return true;
2636 : }
2637 :
2638 : // Nothing else to gather - if we're carrying anything then we should
2639 : // drop it off, and if not then we might as well head to the dropsite
2640 : // anyway because that's a nice enough place to congregate and idle
2641 :
2642 0 : let nearestDropsite = this.FindNearestDropsite(resourceType.generic);
2643 0 : if (nearestDropsite)
2644 : {
2645 0 : this.PushOrderFront("ReturnResource", { "target": nearestDropsite, "force": false });
2646 0 : return true;
2647 : }
2648 : // No dropsites - just give up.
2649 0 : return true;
2650 : },
2651 : },
2652 :
2653 : "RETURNINGRESOURCE": {
2654 : "enter": function() {
2655 0 : let nearestDropsite = this.FindNearestDropsite(this.order.data.type.generic);
2656 0 : if (!nearestDropsite)
2657 : {
2658 : // The player expects the unit to move upon failure.
2659 0 : let formerTarget = this.order.data.target;
2660 0 : if (!this.FinishOrder())
2661 0 : this.WalkToTarget(formerTarget);
2662 0 : return true;
2663 : }
2664 0 : this.order.data.formerTarget = this.order.data.target;
2665 0 : this.order.data.target = nearestDropsite;
2666 0 : if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
2667 : {
2668 0 : this.SetNextState("DROPPINGRESOURCES");
2669 0 : return true;
2670 : }
2671 0 : this.SetNextState("APPROACHING");
2672 0 : return true;
2673 : },
2674 :
2675 : "leave": function() {
2676 : },
2677 :
2678 : "APPROACHING": "INDIVIDUAL.RETURNRESOURCE.APPROACHING",
2679 :
2680 : "DROPPINGRESOURCES": {
2681 : "enter": function() {
2682 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
2683 0 : if (this.CanReturnResource(this.order.data.target, true, cmpResourceGatherer) &&
2684 : cmpResourceGatherer.IsTargetInRange(this.order.data.target))
2685 : {
2686 0 : cmpResourceGatherer.CommitResources(this.order.data.target);
2687 : // Stop showing the carried resource animation.
2688 0 : this.SetDefaultAnimationVariant();
2689 0 : this.SetNextState("GATHER.APPROACHING");
2690 : }
2691 : else
2692 0 : this.SetNextState("RETURNINGRESOURCE");
2693 0 : this.order.data.target = this.order.data.formerTarget;
2694 :
2695 0 : return true;
2696 : },
2697 :
2698 : "leave": function() {
2699 : },
2700 : },
2701 : },
2702 : },
2703 :
2704 : "HEAL": {
2705 : "APPROACHING": {
2706 : "enter": function() {
2707 0 : if (this.CheckRange(this.order.data, IID_Heal))
2708 : {
2709 0 : this.SetNextState("HEALING");
2710 0 : return true;
2711 : }
2712 :
2713 0 : if (!this.MoveTo(this.order.data, IID_Heal))
2714 : {
2715 0 : this.FinishOrder();
2716 0 : return true;
2717 : }
2718 :
2719 0 : this.StartTimer(1000, 1000);
2720 0 : return false;
2721 : },
2722 :
2723 : "leave": function() {
2724 0 : this.StopMoving();
2725 0 : this.StopTimer();
2726 : },
2727 :
2728 : "Timer": function(msg) {
2729 0 : if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null))
2730 0 : this.SetNextState("FINDINGNEWTARGET");
2731 : },
2732 :
2733 : "MovementUpdate": function(msg) {
2734 0 : if (msg.likelyFailure || this.CheckRange(this.order.data, IID_Heal))
2735 0 : this.SetNextState("HEALING");
2736 : },
2737 : },
2738 :
2739 : "HEALING": {
2740 : "enter": function() {
2741 0 : let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
2742 0 : if (!cmpHeal)
2743 : {
2744 0 : this.FinishOrder();
2745 0 : return true;
2746 : }
2747 :
2748 0 : if (!this.CheckRange(this.order.data, IID_Heal))
2749 : {
2750 0 : this.ProcessMessage("OutOfRange");
2751 0 : return true;
2752 : }
2753 :
2754 0 : if (!cmpHeal.StartHealing(this.order.data.target, IID_UnitAI))
2755 : {
2756 0 : this.ProcessMessage("TargetInvalidated");
2757 0 : return true;
2758 : }
2759 :
2760 0 : this.FaceTowardsTarget(this.order.data.target);
2761 0 : return false;
2762 : },
2763 :
2764 : "leave": function() {
2765 0 : let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
2766 0 : if (cmpHeal)
2767 0 : cmpHeal.StopHealing();
2768 : },
2769 :
2770 : "OutOfRange": function(msg) {
2771 0 : if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
2772 : {
2773 0 : if (this.CanPack())
2774 0 : this.PushOrderFront("Pack", { "force": true });
2775 : else
2776 0 : this.SetNextState("APPROACHING");
2777 : }
2778 : else
2779 0 : this.SetNextState("FINDINGNEWTARGET");
2780 : },
2781 :
2782 : "TargetInvalidated": function(msg) {
2783 0 : this.SetNextState("FINDINGNEWTARGET");
2784 : },
2785 : },
2786 :
2787 : "FINDINGNEWTARGET": {
2788 : "enter": function() {
2789 : // If we have another order, do that instead.
2790 0 : if (this.FinishOrder())
2791 0 : return true;
2792 :
2793 0 : if (this.FindNewHealTargets())
2794 0 : return true;
2795 :
2796 0 : if (this.GetStance().respondHoldGround)
2797 0 : this.WalkToHeldPosition();
2798 :
2799 : // We quit this state right away.
2800 0 : return true;
2801 : },
2802 : },
2803 : },
2804 :
2805 : // Returning to dropsite
2806 : "RETURNRESOURCE": {
2807 : "APPROACHING": {
2808 : "enter": function() {
2809 0 : if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
2810 : {
2811 0 : this.SetNextState("DROPPINGRESOURCES");
2812 0 : return true;
2813 : }
2814 :
2815 0 : if (!this.MoveTo(this.order.data, IID_ResourceGatherer))
2816 : {
2817 0 : this.FinishOrder();
2818 0 : return true;
2819 : }
2820 :
2821 0 : this.SetDefaultAnimationVariant();
2822 0 : return false;
2823 : },
2824 :
2825 : "leave": function() {
2826 0 : this.StopMoving();
2827 : },
2828 :
2829 : "MovementUpdate": function(msg) {
2830 0 : if (msg.likelyFailure || this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
2831 0 : this.SetNextState("DROPPINGRESOURCES");
2832 : },
2833 : },
2834 :
2835 : "DROPPINGRESOURCES": {
2836 : "enter": function() {
2837 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
2838 0 : if (this.CanReturnResource(this.order.data.target, true, cmpResourceGatherer) &&
2839 : cmpResourceGatherer.IsTargetInRange(this.order.data.target))
2840 : {
2841 0 : cmpResourceGatherer.CommitResources(this.order.data.target);
2842 :
2843 : // Stop showing the carried resource animation.
2844 0 : this.SetDefaultAnimationVariant();
2845 :
2846 0 : this.FinishOrder();
2847 0 : return true;
2848 : }
2849 0 : let nearby = this.FindNearestDropsite(cmpResourceGatherer.GetMainCarryingType());
2850 0 : this.FinishOrder();
2851 0 : if (nearby)
2852 0 : this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
2853 :
2854 0 : return true;
2855 : },
2856 :
2857 : "leave": function() {
2858 : },
2859 : },
2860 : },
2861 :
2862 : "COLLECTTREASURE": {
2863 : "leave": function() {
2864 : },
2865 :
2866 : "APPROACHING": {
2867 : "enter": function() {
2868 : // If we can't move, assume we'll fail any subsequent order
2869 : // and finish the order entirely to avoid an infinite loop.
2870 0 : if (!this.AbleToMove())
2871 : {
2872 0 : this.FinishOrder();
2873 0 : return true;
2874 : }
2875 0 : if (!this.MoveToTargetRange(this.order.data.target, IID_TreasureCollector))
2876 : {
2877 0 : this.SetNextState("FINDINGNEWTARGET");
2878 0 : return true;
2879 : }
2880 0 : return false;
2881 : },
2882 :
2883 : "leave": function() {
2884 0 : this.StopMoving();
2885 : },
2886 :
2887 : "MovementUpdate": function(msg) {
2888 0 : if (this.CheckTargetRange(this.order.data.target, IID_TreasureCollector))
2889 0 : this.SetNextState("COLLECTING");
2890 0 : else if (msg.likelyFailure)
2891 0 : this.SetNextState("FINDINGNEWTARGET");
2892 : },
2893 : },
2894 :
2895 : "COLLECTING": {
2896 : "enter": function() {
2897 0 : let cmpTreasureCollector = Engine.QueryInterface(this.entity, IID_TreasureCollector);
2898 0 : if (!cmpTreasureCollector.StartCollecting(this.order.data.target, IID_UnitAI))
2899 : {
2900 0 : this.ProcessMessage("TargetInvalidated");
2901 0 : return true;
2902 : }
2903 0 : this.FaceTowardsTarget(this.order.data.target);
2904 0 : return false;
2905 : },
2906 :
2907 : "leave": function() {
2908 0 : let cmpTreasureCollector = Engine.QueryInterface(this.entity, IID_TreasureCollector);
2909 0 : if (cmpTreasureCollector)
2910 0 : cmpTreasureCollector.StopCollecting();
2911 : },
2912 :
2913 : "OutOfRange": function(msg) {
2914 0 : this.SetNextState("APPROACHING");
2915 : },
2916 :
2917 : "TargetInvalidated": function(msg) {
2918 0 : this.SetNextState("FINDINGNEWTARGET");
2919 : },
2920 : },
2921 :
2922 : "FINDINGNEWTARGET": {
2923 : "enter": function() {
2924 0 : let oldTarget = this.order.data.target || INVALID_ENTITY;
2925 :
2926 : // Switch to the next order (if any).
2927 0 : if (this.FinishOrder())
2928 0 : return true;
2929 :
2930 0 : let nearbyTreasure = this.FindNearbyTreasure(this.TargetPosOrEntPos(oldTarget));
2931 0 : if (nearbyTreasure)
2932 0 : this.CollectTreasure(nearbyTreasure, true);
2933 :
2934 0 : return true;
2935 : },
2936 : },
2937 :
2938 : // Walking to a good place to collect treasures near, used by CollectTreasureNearPosition.
2939 : "WALKING": {
2940 : "enter": function() {
2941 0 : if (!this.MoveTo(this.order.data))
2942 : {
2943 0 : this.FinishOrder();
2944 0 : return true;
2945 : }
2946 0 : return false;
2947 : },
2948 :
2949 : "leave": function() {
2950 0 : this.StopMoving();
2951 : },
2952 :
2953 : "MovementUpdate": function(msg) {
2954 0 : if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
2955 : this.CheckRange(this.order.data))
2956 0 : this.SetNextState("FINDINGNEWTARGET");
2957 : },
2958 : },
2959 : },
2960 :
2961 : "TRADE": {
2962 : "Attacked": function(msg) {
2963 : // Ignore attack
2964 : // TODO: Inform player
2965 : },
2966 :
2967 : "leave": function() {
2968 : },
2969 :
2970 : "APPROACHINGMARKET": {
2971 : "enter": function() {
2972 0 : if (!this.MoveToMarket(this.order.data.target))
2973 : {
2974 0 : this.FinishOrder();
2975 0 : return true;
2976 : }
2977 0 : return false;
2978 : },
2979 :
2980 : "leave": function() {
2981 0 : this.StopMoving();
2982 : },
2983 :
2984 : "MovementUpdate": function(msg) {
2985 0 : if (!msg.likelyFailure && !this.CheckRange(this.order.data.nextTarget, IID_Trader))
2986 0 : return;
2987 0 : if (this.waypoints && this.waypoints.length)
2988 : {
2989 0 : if (!this.MoveToMarket(this.order.data.target))
2990 0 : this.FinishOrder();
2991 : }
2992 : else
2993 0 : this.SetNextState("TRADING");
2994 : },
2995 : },
2996 :
2997 : "TRADING": {
2998 : "enter": function() {
2999 0 : if (!this.CanTrade(this.order.data.target))
3000 : {
3001 0 : this.FinishOrder();
3002 0 : return true;
3003 : }
3004 :
3005 0 : if (!this.CheckTargetRange(this.order.data.target, IID_Trader))
3006 : {
3007 0 : this.SetNextState("APPROACHINGMARKET");
3008 0 : return true;
3009 : }
3010 :
3011 0 : let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
3012 0 : let nextMarket = cmpTrader.PerformTrade(this.order.data.target);
3013 0 : let amount = cmpTrader.GetGoods().amount;
3014 0 : if (!nextMarket || !amount || !amount.traderGain)
3015 : {
3016 0 : this.FinishOrder();
3017 0 : return true;
3018 : }
3019 :
3020 0 : this.order.data.target = nextMarket;
3021 :
3022 0 : if (this.order.data.route && this.order.data.route.length)
3023 : {
3024 0 : this.waypoints = this.order.data.route.slice();
3025 0 : if (this.order.data.target == cmpTrader.GetSecondMarket())
3026 0 : this.waypoints.reverse();
3027 : }
3028 :
3029 0 : this.SetNextState("APPROACHINGMARKET");
3030 0 : return true;
3031 : },
3032 :
3033 : "leave": function() {
3034 : },
3035 : },
3036 :
3037 : "TradingCanceled": function(msg) {
3038 0 : if (msg.market != this.order.data.target)
3039 0 : return;
3040 0 : let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
3041 0 : let otherMarket = cmpTrader && cmpTrader.GetFirstMarket();
3042 0 : if (otherMarket)
3043 0 : this.WalkToTarget(otherMarket);
3044 : else
3045 0 : this.FinishOrder();
3046 : },
3047 : },
3048 :
3049 : "REPAIR": {
3050 : "APPROACHING": {
3051 : "enter": function() {
3052 0 : if (!this.MoveTo(this.order.data, IID_Builder))
3053 : {
3054 0 : this.FinishOrder();
3055 0 : return true;
3056 : }
3057 0 : return false;
3058 : },
3059 :
3060 : "leave": function() {
3061 0 : this.StopMoving();
3062 : },
3063 :
3064 : "MovementUpdate": function(msg) {
3065 0 : if (msg.likelyFailure || msg.likelySuccess)
3066 0 : this.SetNextState("REPAIRING");
3067 : },
3068 : },
3069 :
3070 : "REPAIRING": {
3071 : "enter": function() {
3072 2 : let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
3073 2 : if (!cmpBuilder)
3074 : {
3075 0 : this.FinishOrder();
3076 0 : return true;
3077 : }
3078 :
3079 : // If this order was forced, the player probably gave it, but now we've reached the target
3080 : // switch to an unforced order (can be interrupted by attacks)
3081 2 : if (this.order.data.force)
3082 1 : this.order.data.autoharvest = true;
3083 :
3084 2 : this.order.data.force = false;
3085 :
3086 2 : if (!this.CheckTargetRange(this.order.data.target, IID_Builder))
3087 : {
3088 0 : this.ProcessMessage("OutOfRange");
3089 0 : return true;
3090 : }
3091 :
3092 2 : let cmpHealth = Engine.QueryInterface(this.order.data.target, IID_Health);
3093 2 : if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints())
3094 : {
3095 : // The building was already finished/fully repaired before we arrived;
3096 : // let the ConstructionFinished handler handle this.
3097 0 : this.ConstructionFinished({ "entity": this.order.data.target, "newentity": this.order.data.target });
3098 0 : return true;
3099 : }
3100 :
3101 2 : if (!cmpBuilder.StartRepairing(this.order.data.target, IID_UnitAI))
3102 : {
3103 0 : this.ProcessMessage("TargetInvalidated");
3104 0 : return true;
3105 : }
3106 :
3107 2 : this.FaceTowardsTarget(this.order.data.target);
3108 2 : return false;
3109 : },
3110 :
3111 : "leave": function() {
3112 1 : let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
3113 1 : if (cmpBuilder)
3114 1 : cmpBuilder.StopRepairing();
3115 : },
3116 :
3117 : "OutOfRange": function(msg) {
3118 0 : this.SetNextState("APPROACHING");
3119 : },
3120 :
3121 : "TargetInvalidated": function(msg) {
3122 0 : this.FinishOrder();
3123 : },
3124 : },
3125 :
3126 : "ConstructionFinished": function(msg) {
3127 0 : if (msg.data.entity != this.order.data.target)
3128 0 : return; // ignore other buildings
3129 :
3130 0 : let oldData = this.order.data;
3131 :
3132 : // Save the current state so we can continue walking if necessary
3133 : // FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation.
3134 : // Idle animation while moving towards finished construction looks weird (ghosty).
3135 0 : let oldState = this.GetCurrentState();
3136 :
3137 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
3138 0 : let canReturnResources = this.CanReturnResource(msg.data.newentity, true, cmpResourceGatherer);
3139 0 : if (this.CheckTargetRange(msg.data.newentity, IID_Builder) && canReturnResources)
3140 : {
3141 0 : cmpResourceGatherer.CommitResources(msg.data.newentity);
3142 0 : this.SetDefaultAnimationVariant();
3143 : }
3144 :
3145 : // Switch to the next order (if any)
3146 0 : if (this.FinishOrder())
3147 : {
3148 0 : if (canReturnResources)
3149 : {
3150 : // We aren't in range, but we can still return resources there: always do so.
3151 0 : this.SetDefaultAnimationVariant();
3152 0 : this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false });
3153 : }
3154 0 : return;
3155 : }
3156 :
3157 0 : if (canReturnResources)
3158 : {
3159 : // We aren't in range, but we can still return resources there: always do so.
3160 0 : this.SetDefaultAnimationVariant();
3161 0 : this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false });
3162 : }
3163 :
3164 : // No remaining orders - pick a useful default behaviour
3165 :
3166 : // If autocontinue explicitly disabled (e.g. by AI) then
3167 : // do nothing automatically
3168 0 : if (!oldData.autocontinue)
3169 0 : return;
3170 :
3171 : // If this building was e.g. a farm of ours, the entities that received
3172 : // the build command should start gathering from it
3173 0 : if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity))
3174 : {
3175 0 : this.PerformGather(msg.data.newentity, true, false);
3176 0 : return;
3177 : }
3178 :
3179 : // If this building was e.g. a farmstead of ours, entities that received
3180 : // the build command should look for nearby resources to gather
3181 0 : if ((oldData.force || oldData.autoharvest) &&
3182 : this.CanReturnResource(msg.data.newentity, false, cmpResourceGatherer))
3183 : {
3184 0 : let cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite);
3185 0 : let types = cmpResourceDropsite.GetTypes();
3186 : // TODO: Slightly undefined behavior here, we don't know what type of resource will be collected,
3187 : // may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that!
3188 0 : let nearby = this.FindNearbyResource(this.TargetPosOrEntPos(msg.data.newentity),
3189 0 : (ent, type, template) => types.indexOf(type.generic) != -1);
3190 :
3191 0 : if (nearby)
3192 : {
3193 0 : this.PerformGather(nearby, true, false);
3194 0 : return;
3195 : }
3196 : }
3197 :
3198 0 : let nearbyFoundation = this.FindNearbyFoundation(this.TargetPosOrEntPos(msg.data.newentity));
3199 0 : if (nearbyFoundation)
3200 : {
3201 0 : this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldData.autocontinue, "force": false }, true);
3202 0 : return;
3203 : }
3204 :
3205 : // Unit was approaching and there's nothing to do now, so switch to walking
3206 0 : if (oldState.endsWith("REPAIR.APPROACHING"))
3207 : // We're already walking to the given point, so add this as a order.
3208 0 : this.WalkToTarget(msg.data.newentity, true);
3209 : },
3210 : },
3211 :
3212 : "GARRISON": {
3213 : "APPROACHING": {
3214 : "enter": function() {
3215 2 : if (this.order.data.garrison ? !this.CanGarrison(this.order.data.target) :
3216 : !this.CanOccupyTurret(this.order.data.target))
3217 : {
3218 1 : this.FinishOrder();
3219 1 : return true;
3220 : }
3221 :
3222 1 : if (!this.MoveToTargetRange(this.order.data.target, this.order.data.garrison ? IID_Garrisonable : IID_Turretable))
3223 : {
3224 0 : this.FinishOrder();
3225 0 : return true;
3226 : }
3227 :
3228 1 : if (this.pickup)
3229 0 : Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
3230 :
3231 1 : let cmpHolder = Engine.QueryInterface(this.order.data.target, this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder);
3232 1 : if (cmpHolder && cmpHolder.CanPickup(this.entity))
3233 : {
3234 0 : this.pickup = this.order.data.target;
3235 0 : Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity, "iid": this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder });
3236 : }
3237 1 : return false;
3238 : },
3239 :
3240 : "leave": function() {
3241 2 : if (this.pickup)
3242 : {
3243 0 : Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
3244 0 : delete this.pickup;
3245 : }
3246 2 : this.StopMoving();
3247 : },
3248 :
3249 : "MovementUpdate": function(msg) {
3250 0 : if (!msg.likelyFailure && !msg.likelySuccess)
3251 0 : return;
3252 :
3253 0 : if (this.CheckTargetRange(this.order.data.target, this.order.data.garrison ? IID_Garrisonable : IID_Turretable))
3254 0 : this.SetNextState("GARRISONING");
3255 : else
3256 : {
3257 : // Unable to reach the target, try again (or follow if it is a moving target)
3258 : // except if the target does not exist anymore or its orders have changed.
3259 0 : if (this.pickup)
3260 : {
3261 0 : let cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI);
3262 0 : if (!cmpUnitAI || (!cmpUnitAI.HasPickupOrder(this.entity) && !cmpUnitAI.IsIdle()))
3263 0 : this.FinishOrder();
3264 : }
3265 : }
3266 : },
3267 : },
3268 :
3269 : "GARRISONING": {
3270 : "enter": function() {
3271 0 : let target = this.order.data.target;
3272 0 : if (this.order.data.garrison)
3273 : {
3274 0 : let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable);
3275 0 : if (!cmpGarrisonable || !cmpGarrisonable.Garrison(target))
3276 : {
3277 0 : this.FinishOrder();
3278 0 : return true;
3279 : }
3280 : }
3281 : else
3282 : {
3283 0 : let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
3284 0 : if (!cmpTurretable || !cmpTurretable.OccupyTurret(target))
3285 : {
3286 0 : this.FinishOrder();
3287 0 : return true;
3288 : }
3289 : }
3290 :
3291 0 : if (this.formationController)
3292 : {
3293 0 : let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
3294 0 : if (cmpFormation)
3295 : {
3296 0 : let rearrange = cmpFormation.rearrange;
3297 0 : cmpFormation.SetRearrange(false);
3298 0 : cmpFormation.RemoveMembers([this.entity]);
3299 0 : cmpFormation.SetRearrange(rearrange);
3300 : }
3301 : }
3302 :
3303 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
3304 0 : if (this.CanReturnResource(target, true, cmpResourceGatherer))
3305 : {
3306 0 : cmpResourceGatherer.CommitResources(target);
3307 0 : this.SetDefaultAnimationVariant();
3308 : }
3309 :
3310 0 : this.FinishOrder();
3311 0 : return true;
3312 : },
3313 :
3314 : "leave": function() {
3315 : },
3316 : },
3317 : },
3318 :
3319 : "CHEERING": {
3320 : "enter": function() {
3321 0 : this.SelectAnimation("promotion");
3322 0 : this.StartTimer(this.cheeringTime);
3323 0 : return false;
3324 : },
3325 :
3326 : "leave": function() {
3327 : // PushOrderFront preserves the cheering order,
3328 : // which can lead to very bad behaviour, so make
3329 : // sure to delete any queued ones.
3330 0 : for (let i = 1; i < this.orderQueue.length; ++i)
3331 0 : if (this.orderQueue[i].type == "Cheer")
3332 0 : this.orderQueue.splice(i--, 1);
3333 0 : this.StopTimer();
3334 0 : this.ResetAnimation();
3335 : },
3336 :
3337 : "LosRangeUpdate": function(msg) {
3338 0 : if (msg && msg.data && msg.data.added && msg.data.added.length)
3339 0 : this.RespondToSightedEntities(msg.data.added);
3340 : },
3341 :
3342 : "LosHealRangeUpdate": function(msg) {
3343 0 : if (msg && msg.data && msg.data.added && msg.data.added.length)
3344 0 : this.RespondToHealableEntities(msg.data.added);
3345 : },
3346 :
3347 : "LosAttackRangeUpdate": function(msg) {
3348 0 : if (msg && msg.data && msg.data.added && msg.data.added.length && this.GetStance().targetVisibleEnemies)
3349 0 : this.AttackEntitiesByPreference(msg.data.added);
3350 : },
3351 :
3352 : "Timer": function(msg) {
3353 0 : this.FinishOrder();
3354 : },
3355 : },
3356 :
3357 : "PACKING": {
3358 : "enter": function() {
3359 0 : let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
3360 0 : cmpPack.Pack();
3361 0 : return false;
3362 : },
3363 :
3364 : "Order.CancelPack": function(msg) {
3365 0 : this.FinishOrder();
3366 0 : return ACCEPT_ORDER;
3367 : },
3368 :
3369 : "PackFinished": function(msg) {
3370 0 : this.FinishOrder();
3371 : },
3372 :
3373 : "leave": function() {
3374 0 : let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
3375 0 : cmpPack.CancelPack();
3376 : },
3377 :
3378 : "Attacked": function(msg) {
3379 : // Ignore attacks while packing
3380 : },
3381 : },
3382 :
3383 : "UNPACKING": {
3384 : "enter": function() {
3385 0 : let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
3386 0 : cmpPack.Unpack();
3387 0 : return false;
3388 : },
3389 :
3390 : "Order.CancelUnpack": function(msg) {
3391 0 : this.FinishOrder();
3392 0 : return ACCEPT_ORDER;
3393 : },
3394 :
3395 : "PackFinished": function(msg) {
3396 0 : this.FinishOrder();
3397 : },
3398 :
3399 : "leave": function() {
3400 0 : let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
3401 0 : cmpPack.CancelPack();
3402 : },
3403 :
3404 : "Attacked": function(msg) {
3405 : // Ignore attacks while unpacking
3406 : },
3407 : },
3408 :
3409 : "PICKUP": {
3410 : "APPROACHING": {
3411 : "enter": function() {
3412 0 : if (!this.MoveTo(this.order.data))
3413 : {
3414 0 : this.FinishOrder();
3415 0 : return true;
3416 : }
3417 0 : return false;
3418 : },
3419 :
3420 : "leave": function() {
3421 0 : this.StopMoving();
3422 : },
3423 :
3424 : "MovementUpdate": function(msg) {
3425 0 : if (msg.likelyFailure || msg.likelySuccess)
3426 0 : this.SetNextState("LOADING");
3427 : },
3428 :
3429 : "PickupCanceled": function() {
3430 0 : this.FinishOrder();
3431 : },
3432 : },
3433 :
3434 : "LOADING": {
3435 : "enter": function() {
3436 0 : let cmpHolder = Engine.QueryInterface(this.entity, this.order.data.iid);
3437 0 : if (!cmpHolder || cmpHolder.IsFull())
3438 : {
3439 0 : this.FinishOrder();
3440 0 : return true;
3441 : }
3442 0 : return false;
3443 : },
3444 :
3445 : "PickupCanceled": function() {
3446 0 : this.FinishOrder();
3447 : },
3448 : },
3449 : },
3450 : },
3451 : };
3452 :
3453 1 : UnitAI.prototype.Init = function()
3454 : {
3455 19 : this.orderQueue = []; // current order is at the front of the list
3456 19 : this.order = undefined; // always == this.orderQueue[0]
3457 19 : this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
3458 19 : this.isIdle = false;
3459 :
3460 19 : this.heldPosition = undefined;
3461 :
3462 : // Queue of remembered works
3463 19 : this.workOrders = [];
3464 :
3465 19 : this.isGuardOf = undefined;
3466 :
3467 19 : this.formationAnimationVariant = undefined;
3468 19 : this.cheeringTime = +(this.template.CheeringTime || 0);
3469 19 : this.SetStance(this.template.DefaultStance);
3470 : };
3471 :
3472 : /**
3473 : * @param {cmpTurretable} cmpTurretable - Optionally the component to save a query here.
3474 : * @return {boolean} - Whether we are occupying a turret point.
3475 : */
3476 1 : UnitAI.prototype.IsTurret = function(cmpTurretable)
3477 : {
3478 1 : if (!cmpTurretable)
3479 1 : cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
3480 1 : return cmpTurretable && cmpTurretable.HolderID() != INVALID_ENTITY;
3481 : };
3482 :
3483 1 : UnitAI.prototype.IsFormationController = function()
3484 : {
3485 71 : return (this.template.FormationController == "true");
3486 : };
3487 :
3488 1 : UnitAI.prototype.IsFormationMember = function()
3489 : {
3490 148 : return (this.formationController != INVALID_ENTITY);
3491 : };
3492 :
3493 1 : UnitAI.prototype.GetFormationsList = function()
3494 : {
3495 0 : return this.template.Formations?._string?.split(/\s+/) || [];
3496 : };
3497 :
3498 1 : UnitAI.prototype.CanUseFormation = function(formation)
3499 : {
3500 0 : return this.GetFormationsList().includes(formation);
3501 : };
3502 :
3503 : /**
3504 : * For now, entities with a RoamDistance are animals.
3505 : */
3506 1 : UnitAI.prototype.IsAnimal = function()
3507 : {
3508 0 : return !!this.template.RoamDistance;
3509 : };
3510 :
3511 : /**
3512 : * ToDo: Make this not needed by fixing gaia
3513 : * range queries in BuildingAI and UnitAI regarding
3514 : * animals and other gaia entities.
3515 : */
3516 1 : UnitAI.prototype.IsDangerousAnimal = function()
3517 : {
3518 0 : return this.IsAnimal() && this.GetStance().targetVisibleEnemies && !!Engine.QueryInterface(this.entity, IID_Attack);
3519 : };
3520 :
3521 1 : UnitAI.prototype.IsHealer = function()
3522 : {
3523 5 : return Engine.QueryInterface(this.entity, IID_Heal);
3524 : };
3525 :
3526 1 : UnitAI.prototype.IsIdle = function()
3527 : {
3528 0 : return this.isIdle;
3529 : };
3530 :
3531 : /**
3532 : * Used by formation controllers to toggle the idleness of their members.
3533 : */
3534 1 : UnitAI.prototype.ResetIdle = function()
3535 : {
3536 0 : let shouldBeIdle = this.GetCurrentState().endsWith(".IDLE");
3537 0 : if (this.isIdle == shouldBeIdle)
3538 0 : return;
3539 0 : this.isIdle = shouldBeIdle;
3540 0 : Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
3541 : };
3542 :
3543 1 : UnitAI.prototype.SetGarrisoned = function()
3544 : {
3545 : // UnitAI caches its own garrisoned state for performance.
3546 0 : this.isGarrisoned = true;
3547 0 : this.SetImmobile();
3548 : };
3549 :
3550 1 : UnitAI.prototype.UnsetGarrisoned = function()
3551 : {
3552 0 : delete this.isGarrisoned;
3553 0 : this.SetMobile();
3554 : };
3555 :
3556 1 : UnitAI.prototype.ShouldRespondToEndOfAlert = function()
3557 : {
3558 0 : return !this.orderQueue.length || this.orderQueue[0].type == "Garrison";
3559 : };
3560 :
3561 1 : UnitAI.prototype.SetImmobile = function()
3562 : {
3563 0 : if (this.isImmobile)
3564 0 : return;
3565 :
3566 0 : this.isImmobile = true;
3567 0 : Engine.PostMessage(this.entity, MT_UnitAbleToMoveChanged, {
3568 : "entity": this.entity,
3569 : "ableToMove": this.AbleToMove()
3570 : });
3571 : };
3572 :
3573 1 : UnitAI.prototype.SetMobile = function()
3574 : {
3575 0 : if (!this.isImmobile)
3576 0 : return;
3577 :
3578 0 : delete this.isImmobile;
3579 0 : Engine.PostMessage(this.entity, MT_UnitAbleToMoveChanged, {
3580 : "entity": this.entity,
3581 : "ableToMove": this.AbleToMove()
3582 : });
3583 : };
3584 :
3585 : /**
3586 : * @param cmpUnitMotion - optionally pass unitMotion to avoid querying it here
3587 : * @returns true if the entity can move, i.e. has UnitMotion and isn't immobile.
3588 : */
3589 1 : UnitAI.prototype.AbleToMove = function(cmpUnitMotion)
3590 : {
3591 20 : if (this.isImmobile)
3592 0 : return false;
3593 :
3594 20 : if (!cmpUnitMotion)
3595 16 : cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
3596 :
3597 20 : return !!cmpUnitMotion;
3598 : };
3599 :
3600 1 : UnitAI.prototype.IsFleeing = function()
3601 : {
3602 0 : var state = this.GetCurrentState().split(".").pop();
3603 0 : return (state == "FLEEING");
3604 : };
3605 :
3606 1 : UnitAI.prototype.IsWalking = function()
3607 : {
3608 0 : var state = this.GetCurrentState().split(".").pop();
3609 0 : return (state == "WALKING");
3610 : };
3611 :
3612 : /**
3613 : * Return true if the current order is WalkAndFight or Patrol.
3614 : */
3615 1 : UnitAI.prototype.IsWalkingAndFighting = function()
3616 : {
3617 0 : if (this.IsFormationMember())
3618 0 : return Engine.QueryInterface(this.formationController, IID_UnitAI).IsWalkingAndFighting();
3619 :
3620 0 : return this.orderQueue.length > 0 && (this.orderQueue[0].type == "WalkAndFight" || this.orderQueue[0].type == "Patrol");
3621 : };
3622 :
3623 1 : UnitAI.prototype.OnCreate = function()
3624 : {
3625 19 : if (this.IsFormationController())
3626 4 : this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
3627 : else
3628 15 : this.UnitFsm.Init(this, "INDIVIDUAL.IDLE");
3629 19 : this.isIdle = true;
3630 : };
3631 :
3632 1 : UnitAI.prototype.OnDiplomacyChanged = function(msg)
3633 : {
3634 0 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
3635 0 : if (cmpOwnership && cmpOwnership.GetOwner() == msg.player)
3636 0 : this.SetupRangeQueries();
3637 :
3638 0 : if (this.isGuardOf && !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf))
3639 0 : this.RemoveGuard();
3640 : };
3641 :
3642 1 : UnitAI.prototype.OnOwnershipChanged = function(msg)
3643 : {
3644 0 : this.SetupRangeQueries();
3645 :
3646 0 : if (this.isGuardOf && (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf)))
3647 0 : this.RemoveGuard();
3648 :
3649 : // If the unit isn't being created or dying, reset stance and clear orders
3650 0 : if (msg.to != INVALID_PLAYER && msg.from != INVALID_PLAYER)
3651 : {
3652 : // Switch to a virgin state to let states execute their leave handlers.
3653 : // Except if (un)packing, in which case we only clear the order queue.
3654 0 : if (this.IsPacking())
3655 : {
3656 0 : this.orderQueue.length = Math.min(this.orderQueue.length, 1);
3657 0 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
3658 : }
3659 : else
3660 : {
3661 0 : const state = this.GetCurrentState();
3662 : // Special "will be destroyed soon" mode - do nothing.
3663 0 : if (state === "")
3664 0 : return;
3665 0 : const index = state.indexOf(".");
3666 0 : if (index != -1)
3667 0 : this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0, index));
3668 0 : this.Stop(false);
3669 : }
3670 :
3671 0 : this.workOrders = [];
3672 0 : let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
3673 0 : if (cmpTrader)
3674 0 : cmpTrader.StopTrading();
3675 :
3676 0 : this.SetStance(this.template.DefaultStance);
3677 0 : if (this.IsTurret())
3678 0 : this.SetTurretStance();
3679 : }
3680 : };
3681 :
3682 1 : UnitAI.prototype.OnDestroy = function()
3683 : {
3684 : // Switch to an empty state to let states execute their leave handlers.
3685 0 : this.UnitFsm.SwitchToNextState(this, "");
3686 :
3687 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
3688 0 : if (this.losRangeQuery)
3689 0 : cmpRangeManager.DestroyActiveQuery(this.losRangeQuery);
3690 0 : if (this.losHealRangeQuery)
3691 0 : cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery);
3692 0 : if (this.losAttackRangeQuery)
3693 0 : cmpRangeManager.DestroyActiveQuery(this.losAttackRangeQuery);
3694 : };
3695 :
3696 1 : UnitAI.prototype.OnVisionRangeChanged = function(msg)
3697 : {
3698 0 : if (this.entity == msg.entity)
3699 0 : this.SetupRangeQueries();
3700 : };
3701 :
3702 1 : UnitAI.prototype.HasPickupOrder = function(entity)
3703 : {
3704 0 : return this.orderQueue.some(order => order.type == "PickupUnit" && order.data.target == entity);
3705 : };
3706 :
3707 1 : UnitAI.prototype.OnPickupRequested = function(msg)
3708 : {
3709 0 : if (this.HasPickupOrder(msg.entity))
3710 0 : return;
3711 0 : this.PushOrderAfterForced("PickupUnit", { "target": msg.entity, "iid": msg.iid });
3712 : };
3713 :
3714 1 : UnitAI.prototype.OnPickupCanceled = function(msg)
3715 : {
3716 0 : for (let i = 0; i < this.orderQueue.length; ++i)
3717 : {
3718 0 : if (this.orderQueue[i].type != "PickupUnit" || this.orderQueue[i].data.target != msg.entity)
3719 0 : continue;
3720 0 : if (i == 0)
3721 0 : this.UnitFsm.ProcessMessage(this, { "type": "PickupCanceled", "data": msg });
3722 : else
3723 0 : this.orderQueue.splice(i, 1);
3724 0 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
3725 0 : break;
3726 : }
3727 : };
3728 :
3729 : /**
3730 : * Wrapper function that sets up the LOS, healer and attack range queries.
3731 : * This should be called whenever our ownership changes.
3732 : */
3733 1 : UnitAI.prototype.SetupRangeQueries = function()
3734 : {
3735 0 : if (this.GetStance().respondFleeOnSight)
3736 0 : this.SetupLOSRangeQuery();
3737 :
3738 0 : if (this.IsHealer())
3739 0 : this.SetupHealRangeQuery();
3740 :
3741 0 : if (Engine.QueryInterface(this.entity, IID_Attack))
3742 0 : this.SetupAttackRangeQuery();
3743 : };
3744 :
3745 1 : UnitAI.prototype.UpdateRangeQueries = function()
3746 : {
3747 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
3748 0 : if (this.losRangeQuery)
3749 0 : this.SetupLOSRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losRangeQuery));
3750 :
3751 0 : if (this.losHealRangeQuery)
3752 0 : this.SetupHealRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losHealRangeQuery));
3753 :
3754 0 : if (this.losAttackRangeQuery)
3755 0 : this.SetupAttackRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losAttackRangeQuery));
3756 : };
3757 :
3758 : /**
3759 : * Set up a range query for all enemy units within LOS range.
3760 : * @param {boolean} enable - Optional parameter whether to enable the query.
3761 : */
3762 1 : UnitAI.prototype.SetupLOSRangeQuery = function(enable = true)
3763 : {
3764 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
3765 0 : if (this.losRangeQuery)
3766 : {
3767 0 : cmpRangeManager.DestroyActiveQuery(this.losRangeQuery);
3768 0 : this.losRangeQuery = undefined;
3769 : }
3770 :
3771 0 : let cmpPlayer = QueryOwnerInterface(this.entity);
3772 : // If we are being destructed (owner == -1), creating a range query is pointless.
3773 0 : if (!cmpPlayer)
3774 0 : return;
3775 :
3776 0 : let players = cmpPlayer.GetEnemies();
3777 0 : if (!players.length)
3778 0 : return;
3779 :
3780 0 : let range = this.GetQueryRange(IID_Vision);
3781 : // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
3782 0 : this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
3783 : range.min, range.max, players, IID_Identity,
3784 : cmpRangeManager.GetEntityFlagMask("normal"), false);
3785 :
3786 0 : if (enable)
3787 0 : cmpRangeManager.EnableActiveQuery(this.losRangeQuery);
3788 : };
3789 :
3790 : /**
3791 : * Set up a range query for all own or ally units within LOS range
3792 : * which can be healed.
3793 : * @param {boolean} enable - Optional parameter whether to enable the query.
3794 : */
3795 1 : UnitAI.prototype.SetupHealRangeQuery = function(enable = true)
3796 : {
3797 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
3798 :
3799 0 : if (this.losHealRangeQuery)
3800 : {
3801 0 : cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery);
3802 0 : this.losHealRangeQuery = undefined;
3803 : }
3804 :
3805 0 : let cmpPlayer = QueryOwnerInterface(this.entity);
3806 : // If we are being destructed (owner == -1), creating a range query is pointless.
3807 0 : if (!cmpPlayer)
3808 0 : return;
3809 :
3810 0 : let players = cmpPlayer.GetAllies();
3811 0 : let range = this.GetQueryRange(IID_Heal);
3812 :
3813 : // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
3814 0 : this.losHealRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
3815 : range.min, range.max, players, IID_Health,
3816 : cmpRangeManager.GetEntityFlagMask("injured"), false);
3817 :
3818 0 : if (enable)
3819 0 : cmpRangeManager.EnableActiveQuery(this.losHealRangeQuery);
3820 : };
3821 :
3822 : /**
3823 : * Set up a range query for all enemy and gaia units within range
3824 : * which can be attacked.
3825 : * @param {boolean} enable - Optional parameter whether to enable the query.
3826 : */
3827 1 : UnitAI.prototype.SetupAttackRangeQuery = function(enable = true)
3828 : {
3829 11 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
3830 :
3831 11 : if (this.losAttackRangeQuery)
3832 : {
3833 0 : cmpRangeManager.DestroyActiveQuery(this.losAttackRangeQuery);
3834 0 : this.losAttackRangeQuery = undefined;
3835 : }
3836 :
3837 11 : let cmpPlayer = QueryOwnerInterface(this.entity);
3838 : // If we are being destructed (owner == -1), creating a range query is pointless.
3839 11 : if (!cmpPlayer)
3840 0 : return;
3841 :
3842 : // TODO: How to handle neutral players - Special query to attack military only?
3843 11 : let players = cmpPlayer.GetEnemies();
3844 11 : if (!players.length)
3845 0 : return;
3846 :
3847 11 : let range = this.GetQueryRange(IID_Attack);
3848 : // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
3849 11 : this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
3850 : range.min, range.max, players, IID_Resistance,
3851 : cmpRangeManager.GetEntityFlagMask("normal"), false);
3852 :
3853 11 : if (enable)
3854 11 : cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery);
3855 : };
3856 :
3857 :
3858 : // FSM linkage functions
3859 :
3860 : // Setting the next state to the current state will leave/re-enter the top-most substate.
3861 : // Must be called from inside the FSM.
3862 1 : UnitAI.prototype.SetNextState = function(state)
3863 : {
3864 46 : this.UnitFsm.SetNextState(this, state);
3865 : };
3866 :
3867 : // Must be called from inside the FSM.
3868 1 : UnitAI.prototype.DeferMessage = function(msg)
3869 : {
3870 0 : this.UnitFsm.DeferMessage(this, msg);
3871 : };
3872 :
3873 1 : UnitAI.prototype.GetCurrentState = function()
3874 : {
3875 8 : return this.UnitFsm.GetCurrentState(this);
3876 : };
3877 :
3878 1 : UnitAI.prototype.FsmStateNameChanged = function(state)
3879 : {
3880 66 : Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state });
3881 : };
3882 :
3883 : /**
3884 : * Call when the current order has been completed (or failed).
3885 : * Removes the current order from the queue, and processes the
3886 : * next one (if any). Returns false and defaults to IDLE
3887 : * if there are no remaining orders or if the unit is not
3888 : * inWorld and not garrisoned (thus usually waiting to be destroyed).
3889 : * Must be called from inside the FSM.
3890 : */
3891 1 : UnitAI.prototype.FinishOrder = function()
3892 : {
3893 5 : if (!this.orderQueue.length)
3894 : {
3895 0 : let stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line
3896 0 : let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
3897 0 : let template = cmpTemplateManager.GetCurrentTemplateName(this.entity);
3898 0 : error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
3899 : }
3900 :
3901 5 : this.orderQueue.shift();
3902 5 : this.order = this.orderQueue[0];
3903 :
3904 5 : if (this.orderQueue.length && (this.isGarrisoned || this.IsFormationController() ||
3905 : Engine.QueryInterface(this.entity, IID_Position)?.IsInWorld()))
3906 : {
3907 1 : let ret = this.UnitFsm.ProcessMessage(this, {
3908 : "type": "Order."+this.order.type,
3909 : "data": this.order.data
3910 : });
3911 :
3912 1 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
3913 :
3914 1 : return ret;
3915 : }
3916 :
3917 4 : this.orderQueue = [];
3918 4 : this.order = undefined;
3919 :
3920 : // Switch to IDLE as a default state.
3921 4 : this.SetNextState("IDLE");
3922 :
3923 4 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
3924 :
3925 : // Check if there are queued formation orders
3926 4 : if (this.IsFormationMember())
3927 : {
3928 0 : this.SetNextState("FORMATIONMEMBER.IDLE");
3929 0 : let cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
3930 0 : if (cmpUnitAI)
3931 : {
3932 : // Inform the formation controller that we finished this task
3933 0 : Engine.QueryInterface(this.formationController, IID_Formation).
3934 : SetFinishedEntity(this.entity);
3935 : // We don't want to carry out the default order
3936 : // if there are still queued formation orders left
3937 0 : if (cmpUnitAI.GetOrders().length > 1)
3938 0 : return true;
3939 : }
3940 : }
3941 4 : return false;
3942 : };
3943 :
3944 : /**
3945 : * Add an order onto the back of the queue,
3946 : * and execute it if we didn't already have an order.
3947 : */
3948 1 : UnitAI.prototype.PushOrder = function(type, data)
3949 : {
3950 15 : var order = { "type": type, "data": data };
3951 15 : this.orderQueue.push(order);
3952 :
3953 15 : if (this.orderQueue.length == 1)
3954 : {
3955 7 : this.order = order;
3956 7 : this.UnitFsm.ProcessMessage(this, {
3957 : "type": "Order."+this.order.type,
3958 : "data": this.order.data
3959 : });
3960 : }
3961 :
3962 15 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
3963 : };
3964 :
3965 : /**
3966 : * Add an order onto the front of the queue,
3967 : * and execute it immediately.
3968 : */
3969 1 : UnitAI.prototype.PushOrderFront = function(type, data, ignorePacking = false)
3970 : {
3971 29 : var order = { "type": type, "data": data };
3972 : // If current order is packing/unpacking then add new order after it.
3973 29 : if (!ignorePacking && this.order && this.IsPacking())
3974 : {
3975 0 : var packingOrder = this.orderQueue.shift();
3976 0 : this.orderQueue.unshift(packingOrder, order);
3977 : }
3978 : else
3979 : {
3980 29 : this.orderQueue.unshift(order);
3981 29 : this.order = order;
3982 29 : this.UnitFsm.ProcessMessage(this, {
3983 : "type": "Order."+this.order.type,
3984 : "data": this.order.data
3985 : });
3986 : }
3987 :
3988 29 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
3989 :
3990 : };
3991 :
3992 : /**
3993 : * Insert an order after the last forced order onto the queue
3994 : * and after the other orders of the same type
3995 : */
3996 1 : UnitAI.prototype.PushOrderAfterForced = function(type, data)
3997 : {
3998 0 : if (!this.order || ((!this.order.data || !this.order.data.force) && this.order.type != type))
3999 0 : this.PushOrderFront(type, data);
4000 : else
4001 : {
4002 0 : for (let i = 1; i < this.orderQueue.length; ++i)
4003 : {
4004 0 : if (this.orderQueue[i].data && this.orderQueue[i].data.force)
4005 0 : continue;
4006 0 : if (this.orderQueue[i].type == type)
4007 0 : continue;
4008 0 : this.orderQueue.splice(i, 0, { "type": type, "data": data });
4009 0 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
4010 0 : return;
4011 : }
4012 0 : this.PushOrder(type, data);
4013 : }
4014 :
4015 0 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
4016 : };
4017 :
4018 : /**
4019 : * For a unit that is packing and trying to attack something,
4020 : * either cancel packing or continue with packing, as appropriate.
4021 : * Precondition: if the unit is packing/unpacking, then orderQueue
4022 : * should have the Attack order at index 0,
4023 : * and the Pack/Unpack order at index 1.
4024 : * This precondition holds because if we are packing while processing "Order.Attack",
4025 : * then we must have come from ReplaceOrder, which guarantees it.
4026 : *
4027 : * @param {boolean} requirePacked - true if the unit needs to be packed to continue attacking,
4028 : * false if it needs to be unpacked.
4029 : * @return {boolean} true if the unit can attack now, false if it must continue packing (or unpacking) first.
4030 : */
4031 1 : UnitAI.prototype.EnsureCorrectPackStateForAttack = function(requirePacked)
4032 : {
4033 17 : let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
4034 17 : if (!cmpPack ||
4035 : !cmpPack.IsPacking() ||
4036 : this.orderQueue.length != 2 ||
4037 : this.orderQueue[0].type != "Attack" ||
4038 : this.orderQueue[1].type != "Pack" &&
4039 : this.orderQueue[1].type != "Unpack")
4040 17 : return true;
4041 :
4042 0 : if (cmpPack.IsPacked() == requirePacked)
4043 : {
4044 : // The unit is already in the packed/unpacked state we want.
4045 : // Delete the packing order.
4046 0 : this.orderQueue.splice(1, 1);
4047 0 : cmpPack.CancelPack();
4048 0 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
4049 : // Continue with the attack order.
4050 0 : return true;
4051 : }
4052 : // Move the attack order behind the unpacking order, to continue unpacking.
4053 0 : let tmp = this.orderQueue[0];
4054 0 : this.orderQueue[0] = this.orderQueue[1];
4055 0 : this.orderQueue[1] = tmp;
4056 0 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
4057 0 : return false;
4058 : };
4059 :
4060 1 : UnitAI.prototype.WillMoveFromFoundation = function(target, checkPacking = true)
4061 : {
4062 0 : let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
4063 0 : if (!IsOwnedByAllyOfEntity(this.entity, target) && cmpUnitAI && !cmpUnitAI.IsAnimal() &&
4064 : !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() ||
4065 : checkPacking && this.IsPacking() || this.CanPack() || !this.AbleToMove())
4066 0 : return false;
4067 :
4068 0 : return !this.CheckTargetRangeExplicit(target, g_LeaveFoundationRange, -1);
4069 : };
4070 :
4071 1 : UnitAI.prototype.ReplaceOrder = function(type, data)
4072 : {
4073 : // Remember the previous work orders to be able to go back to them later if required
4074 34 : if (data && data.force)
4075 : {
4076 22 : if (this.IsFormationController())
4077 4 : this.CallMemberFunction("UpdateWorkOrders", [type]);
4078 : else
4079 18 : this.UpdateWorkOrders(type);
4080 : }
4081 :
4082 : // Do not replace packing/unpacking unless it is cancel order.
4083 : // TODO: maybe a better way of doing this would be to use priority levels
4084 34 : if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack" && type != "Stop")
4085 : {
4086 0 : var order = { "type": type, "data": data };
4087 0 : var packingOrder = this.orderQueue.shift();
4088 0 : if (type == "Attack")
4089 : {
4090 : // The Attack order is able to handle a packing unit, while other orders can't.
4091 0 : this.orderQueue = [packingOrder];
4092 0 : this.PushOrderFront(type, data, true);
4093 : }
4094 0 : else if (packingOrder.type == "Unpack" && g_OrdersCancelUnpacking.has(type))
4095 : {
4096 : // Immediately cancel unpacking before processing an order that demands a packed unit.
4097 0 : let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
4098 0 : cmpPack.CancelPack();
4099 0 : this.orderQueue = [];
4100 0 : this.PushOrder(type, data);
4101 : }
4102 : else
4103 0 : this.orderQueue = [packingOrder, order];
4104 : }
4105 34 : else if (this.IsFormationMember())
4106 : {
4107 : // Don't replace orders after a LeaveFormation order
4108 : // (this is needed to support queued no-formation orders).
4109 27 : let idx = this.orderQueue.findIndex(o => o.type == "LeaveFormation");
4110 27 : if (idx === -1)
4111 : {
4112 27 : this.orderQueue = [];
4113 27 : this.order = undefined;
4114 : }
4115 : else
4116 0 : this.orderQueue.splice(0, idx);
4117 27 : this.PushOrderFront(type, data);
4118 : }
4119 : else
4120 : {
4121 7 : this.orderQueue = [];
4122 7 : this.PushOrder(type, data);
4123 : }
4124 :
4125 34 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
4126 : };
4127 :
4128 1 : UnitAI.prototype.GetOrders = function()
4129 : {
4130 0 : return this.orderQueue.slice();
4131 : };
4132 :
4133 1 : UnitAI.prototype.AddOrders = function(orders)
4134 : {
4135 0 : orders.forEach(order => this.PushOrder(order.type, order.data));
4136 : };
4137 :
4138 1 : UnitAI.prototype.GetOrderData = function()
4139 : {
4140 86 : var orders = [];
4141 86 : for (let order of this.orderQueue)
4142 89 : if (order.data)
4143 89 : orders.push(clone(order.data));
4144 :
4145 86 : return orders;
4146 : };
4147 :
4148 1 : UnitAI.prototype.UpdateWorkOrders = function(type)
4149 : {
4150 86 : var isWorkType = type => type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource";
4151 40 : if (isWorkType(type))
4152 : {
4153 1 : this.workOrders = [];
4154 1 : return;
4155 : }
4156 :
4157 39 : if (this.workOrders.length)
4158 0 : return;
4159 :
4160 39 : if (this.IsFormationMember())
4161 : {
4162 38 : var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
4163 38 : if (cmpUnitAI)
4164 : {
4165 38 : for (var i = 0; i < cmpUnitAI.orderQueue.length; ++i)
4166 : {
4167 27 : if (isWorkType(cmpUnitAI.orderQueue[i].type))
4168 : {
4169 0 : this.workOrders = cmpUnitAI.orderQueue.slice(i);
4170 0 : return;
4171 : }
4172 : }
4173 : }
4174 : }
4175 :
4176 : // If nothing found, take the unit orders
4177 39 : for (var i = 0; i < this.orderQueue.length; ++i)
4178 : {
4179 19 : if (isWorkType(this.orderQueue[i].type))
4180 : {
4181 0 : this.workOrders = this.orderQueue.slice(i);
4182 0 : return;
4183 : }
4184 : }
4185 : };
4186 :
4187 1 : UnitAI.prototype.BackToWork = function()
4188 : {
4189 0 : if (this.workOrders.length == 0)
4190 0 : return false;
4191 :
4192 0 : if (this.isGarrisoned && !Engine.QueryInterface(this.entity, IID_Garrisonable)?.UnGarrison(false))
4193 0 : return false;
4194 :
4195 0 : const cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
4196 0 : if (this.IsTurret(cmpTurretable) && !cmpTurretable.LeaveTurret())
4197 0 : return false;
4198 :
4199 0 : this.orderQueue = [];
4200 :
4201 0 : this.AddOrders(this.workOrders);
4202 0 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
4203 :
4204 0 : if (this.IsFormationMember())
4205 : {
4206 0 : var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
4207 0 : if (cmpFormation)
4208 0 : cmpFormation.RemoveMembers([this.entity]);
4209 : }
4210 :
4211 0 : this.workOrders = [];
4212 0 : return true;
4213 : };
4214 :
4215 1 : UnitAI.prototype.HasWorkOrders = function()
4216 : {
4217 0 : return this.workOrders.length > 0;
4218 : };
4219 :
4220 1 : UnitAI.prototype.GetWorkOrders = function()
4221 : {
4222 0 : return this.workOrders;
4223 : };
4224 :
4225 1 : UnitAI.prototype.SetWorkOrders = function(orders)
4226 : {
4227 0 : this.workOrders = orders;
4228 : };
4229 :
4230 1 : UnitAI.prototype.TimerHandler = function(data, lateness)
4231 : {
4232 : // Reset the timer
4233 0 : if (data.timerRepeat === undefined)
4234 0 : this.timer = undefined;
4235 :
4236 0 : this.UnitFsm.ProcessMessage(this, { "type": "Timer", "data": data, "lateness": lateness });
4237 : };
4238 :
4239 : /**
4240 : * Set up the UnitAI timer to run after 'offset' msecs, and then
4241 : * every 'repeat' msecs until StopTimer is called. A "Timer" message
4242 : * will be sent each time the timer runs.
4243 : */
4244 1 : UnitAI.prototype.StartTimer = function(offset, repeat)
4245 : {
4246 25 : if (this.timer)
4247 0 : error("Called StartTimer when there's already an active timer");
4248 :
4249 25 : var data = { "timerRepeat": repeat };
4250 :
4251 25 : var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
4252 25 : if (repeat === undefined)
4253 19 : this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data);
4254 : else
4255 6 : this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data);
4256 : };
4257 :
4258 : /**
4259 : * Stop the current UnitAI timer.
4260 : */
4261 1 : UnitAI.prototype.StopTimer = function()
4262 : {
4263 24 : if (!this.timer)
4264 24 : return;
4265 :
4266 0 : var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
4267 0 : cmpTimer.CancelTimer(this.timer);
4268 0 : this.timer = undefined;
4269 : };
4270 :
4271 1 : UnitAI.prototype.OnMotionUpdate = function(msg)
4272 : {
4273 0 : if (msg.veryObstructed)
4274 0 : msg.obstructed = true;
4275 0 : this.UnitFsm.ProcessMessage(this, Object.assign({ "type": "MovementUpdate" }, msg));
4276 : };
4277 :
4278 : /**
4279 : * Called directly by cmpFoundation and cmpRepairable to
4280 : * inform builders that repairing has finished.
4281 : * This not done by listening to a global message due to performance.
4282 : */
4283 1 : UnitAI.prototype.ConstructionFinished = function(msg)
4284 : {
4285 0 : this.UnitFsm.ProcessMessage(this, { "type": "ConstructionFinished", "data": msg });
4286 : };
4287 :
4288 1 : UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
4289 : {
4290 3 : let changed = false;
4291 3 : let currentOrderChanged = false;
4292 3 : for (let i = 0; i < this.orderQueue.length; ++i)
4293 : {
4294 3 : let order = this.orderQueue[i];
4295 3 : if (order.data && order.data.target && order.data.target == msg.entity)
4296 : {
4297 3 : changed = true;
4298 3 : if (i == 0)
4299 3 : currentOrderChanged = true;
4300 3 : order.data.target = msg.newentity;
4301 : }
4302 3 : if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity)
4303 : {
4304 0 : changed = true;
4305 0 : if (i == 0)
4306 0 : currentOrderChanged = true;
4307 0 : order.data.formationTarget = msg.newentity;
4308 : }
4309 : }
4310 3 : if (!changed)
4311 0 : return;
4312 :
4313 3 : if (currentOrderChanged)
4314 3 : this.UnitFsm.ProcessMessage(this, { "type": "OrderTargetRenamed", "data": msg });
4315 :
4316 3 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
4317 : };
4318 :
4319 1 : UnitAI.prototype.OnAttacked = function(msg)
4320 : {
4321 0 : if (msg.fromStatusEffect)
4322 0 : return;
4323 :
4324 0 : this.UnitFsm.ProcessMessage(this, { "type": "Attacked", "data": msg });
4325 : };
4326 :
4327 1 : UnitAI.prototype.OnGuardedAttacked = function(msg)
4328 : {
4329 0 : this.UnitFsm.ProcessMessage(this, { "type": "GuardedAttacked", "data": msg.data });
4330 : };
4331 :
4332 1 : UnitAI.prototype.OnRangeUpdate = function(msg)
4333 : {
4334 0 : if (msg.tag == this.losRangeQuery)
4335 0 : this.UnitFsm.ProcessMessage(this, { "type": "LosRangeUpdate", "data": msg });
4336 0 : else if (msg.tag == this.losHealRangeQuery)
4337 0 : this.UnitFsm.ProcessMessage(this, { "type": "LosHealRangeUpdate", "data": msg });
4338 0 : else if (msg.tag == this.losAttackRangeQuery)
4339 0 : this.UnitFsm.ProcessMessage(this, { "type": "LosAttackRangeUpdate", "data": msg });
4340 : };
4341 :
4342 1 : UnitAI.prototype.OnPackFinished = function(msg)
4343 : {
4344 0 : this.UnitFsm.ProcessMessage(this, { "type": "PackFinished", "packed": msg.packed });
4345 : };
4346 :
4347 : /**
4348 : * A general function to process messages sent from components.
4349 : * @param {string} type - The type of message to process.
4350 : * @param {Object} msg - Optionally extra data to use.
4351 : */
4352 1 : UnitAI.prototype.ProcessMessage = function(type, msg)
4353 : {
4354 0 : this.UnitFsm.ProcessMessage(this, { "type": type, "data": msg });
4355 : };
4356 :
4357 : // Helper functions to be called by the FSM
4358 :
4359 1 : UnitAI.prototype.GetWalkSpeed = function()
4360 : {
4361 0 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4362 0 : if (!cmpUnitMotion)
4363 0 : return 0;
4364 0 : return cmpUnitMotion.GetWalkSpeed();
4365 : };
4366 :
4367 1 : UnitAI.prototype.GetRunMultiplier = function()
4368 : {
4369 1 : var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4370 1 : if (!cmpUnitMotion)
4371 0 : return 0;
4372 1 : return cmpUnitMotion.GetRunMultiplier();
4373 : };
4374 :
4375 : /**
4376 : * Returns true if the target exists and has non-zero hitpoints.
4377 : */
4378 1 : UnitAI.prototype.TargetIsAlive = function(ent)
4379 : {
4380 0 : var cmpFormation = Engine.QueryInterface(ent, IID_Formation);
4381 0 : if (cmpFormation)
4382 0 : return true;
4383 :
4384 0 : var cmpHealth = QueryMiragedInterface(ent, IID_Health);
4385 0 : return cmpHealth && cmpHealth.GetHitpoints() != 0;
4386 : };
4387 :
4388 : /**
4389 : * Returns true if the target exists and needs to be killed before
4390 : * beginning to gather resources from it.
4391 : */
4392 1 : UnitAI.prototype.MustKillGatherTarget = function(ent)
4393 : {
4394 0 : var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
4395 0 : if (!cmpResourceSupply)
4396 0 : return false;
4397 :
4398 0 : if (!cmpResourceSupply.GetKillBeforeGather())
4399 0 : return false;
4400 :
4401 0 : return this.TargetIsAlive(ent);
4402 : };
4403 :
4404 : /**
4405 : * Returns the position of target or, if there is none,
4406 : * the entity's position, or undefined.
4407 : */
4408 1 : UnitAI.prototype.TargetPosOrEntPos = function(target)
4409 : {
4410 0 : let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
4411 0 : if (cmpTargetPosition && cmpTargetPosition.IsInWorld())
4412 0 : return cmpTargetPosition.GetPosition2D();
4413 :
4414 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
4415 0 : if (cmpPosition && cmpPosition.IsInWorld())
4416 0 : return cmpPosition.GetPosition2D();
4417 :
4418 0 : return undefined;
4419 : };
4420 :
4421 :
4422 : /**
4423 : * Returns the entity ID of the nearest resource supply where the given
4424 : * filter returns true, or undefined if none can be found.
4425 : * "Nearest" is nearest from @param position.
4426 : * TODO: extend this to exclude resources that already have lots of gatherers.
4427 : */
4428 1 : UnitAI.prototype.FindNearbyResource = function(position, filter)
4429 : {
4430 0 : if (!position)
4431 0 : return undefined;
4432 :
4433 : // We accept resources owned by Gaia or any player
4434 0 : let players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
4435 :
4436 0 : let range = 64; // TODO: what's a sensible number?
4437 :
4438 0 : let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
4439 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
4440 : // Don't account for entity size, we need to match LOS visibility.
4441 0 : let nearby = cmpRangeManager.ExecuteQueryAroundPos(position, 0, range, players, IID_ResourceSupply, false);
4442 0 : return nearby.find(ent => {
4443 0 : if (!this.CanGather(ent) || !this.CheckTargetVisible(ent))
4444 0 : return false;
4445 :
4446 0 : let template = cmpTemplateManager.GetCurrentTemplateName(ent);
4447 0 : if (template.indexOf("resource|") != -1)
4448 0 : template = template.slice(9);
4449 :
4450 0 : let cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
4451 0 : let type = cmpResourceSupply.GetType();
4452 0 : return cmpResourceSupply.IsAvailableTo(this.entity) && filter(ent, type, template);
4453 : });
4454 : };
4455 :
4456 : /**
4457 : * Returns the entity ID of the nearest resource dropsite that accepts
4458 : * the given type, or undefined if none can be found.
4459 : */
4460 1 : UnitAI.prototype.FindNearestDropsite = function(genericType)
4461 : {
4462 0 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
4463 0 : if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
4464 0 : return undefined;
4465 :
4466 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
4467 0 : if (!cmpPosition || !cmpPosition.IsInWorld())
4468 0 : return undefined;
4469 :
4470 0 : let pos = cmpPosition.GetPosition2D();
4471 : let bestDropsite;
4472 0 : let bestDist = Infinity;
4473 : // Maximum distance a point on an obstruction can be from the center of the obstruction.
4474 0 : let maxDifference = 40;
4475 :
4476 0 : let owner = cmpOwnership.GetOwner();
4477 0 : let cmpPlayer = QueryOwnerInterface(this.entity);
4478 0 : let players = cmpPlayer && cmpPlayer.HasSharedDropsites() ? cmpPlayer.GetMutualAllies() : [owner];
4479 0 : let nearestDropsites = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite, false);
4480 :
4481 0 : let isShip = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship");
4482 0 : let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
4483 0 : for (let dropsite of nearestDropsites)
4484 : {
4485 : // Ships are unable to reach land dropsites and shouldn't attempt to do so.
4486 0 : if (isShip && !Engine.QueryInterface(dropsite, IID_Identity).HasClass("Naval"))
4487 0 : continue;
4488 :
4489 0 : let cmpResourceDropsite = Engine.QueryInterface(dropsite, IID_ResourceDropsite);
4490 0 : if (!cmpResourceDropsite.AcceptsType(genericType) || !this.CheckTargetVisible(dropsite))
4491 0 : continue;
4492 0 : if (Engine.QueryInterface(dropsite, IID_Ownership).GetOwner() != owner && !cmpResourceDropsite.IsShared())
4493 0 : continue;
4494 :
4495 : // The range manager sorts entities by the distance to their center,
4496 : // but we want the distance to the point where resources will be dropped off.
4497 0 : let dist = cmpObstructionManager.DistanceToPoint(dropsite, pos.x, pos.y);
4498 0 : if (dist == -1)
4499 0 : continue;
4500 :
4501 0 : if (dist < bestDist)
4502 : {
4503 0 : bestDropsite = dropsite;
4504 0 : bestDist = dist;
4505 : }
4506 0 : else if (dist > bestDist + maxDifference)
4507 0 : break;
4508 : }
4509 0 : return bestDropsite;
4510 : };
4511 :
4512 : /**
4513 : * Returns the entity ID of the nearest building that needs to be constructed.
4514 : * "Nearest" is nearest from @param position.
4515 : */
4516 1 : UnitAI.prototype.FindNearbyFoundation = function(position)
4517 : {
4518 0 : if (!position)
4519 0 : return undefined;
4520 :
4521 0 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
4522 0 : if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
4523 0 : return undefined;
4524 :
4525 0 : let players = [cmpOwnership.GetOwner()];
4526 :
4527 0 : let range = 64; // TODO: what's a sensible number?
4528 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
4529 : // Don't account for entity size, we need to match LOS visibility.
4530 0 : let nearby = cmpRangeManager.ExecuteQueryAroundPos(position, 0, range, players, IID_Foundation, false);
4531 :
4532 : // Skip foundations that are already complete. (This matters since
4533 : // we process the ConstructionFinished message before the foundation
4534 : // we're working on has been deleted.)
4535 0 : return nearby.find(ent => !Engine.QueryInterface(ent, IID_Foundation).IsFinished() && this.CheckTargetVisible(ent));
4536 : };
4537 :
4538 : /**
4539 : * Returns the entity ID of the nearest treasure.
4540 : * "Nearest" is nearest from @param position.
4541 : */
4542 1 : UnitAI.prototype.FindNearbyTreasure = function(position)
4543 : {
4544 0 : if (!position)
4545 0 : return undefined;
4546 :
4547 0 : let cmpTreasureCollector = Engine.QueryInterface(this.entity, IID_TreasureCollector);
4548 0 : if (!cmpTreasureCollector)
4549 0 : return undefined;
4550 :
4551 0 : let players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
4552 :
4553 0 : let range = 64; // TODO: what's a sensible number?
4554 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
4555 : // Don't account for entity size, we need to match LOS visibility.
4556 0 : let nearby = cmpRangeManager.ExecuteQueryAroundPos(position, 0, range, players, IID_Treasure, false);
4557 0 : return nearby.find(ent => cmpTreasureCollector.CanCollect(ent) && this.CheckTargetVisible(ent));
4558 : };
4559 :
4560 : /**
4561 : * Play a sound appropriate to the current entity.
4562 : */
4563 1 : UnitAI.prototype.PlaySound = function(name)
4564 : {
4565 1 : if (this.IsFormationController())
4566 : {
4567 0 : var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
4568 0 : var member = cmpFormation.GetPrimaryMember();
4569 0 : if (member)
4570 0 : PlaySound(name, member);
4571 : }
4572 : else
4573 : {
4574 1 : PlaySound(name, this.entity);
4575 : }
4576 : };
4577 :
4578 : /*
4579 : * Set a visualActor animation variant.
4580 : * By changing the animation variant, you can change animations based on unitAI state.
4581 : * If there are no specific variants or the variant doesn't exist in the actor,
4582 : * the actor fallbacks to any existing animation.
4583 : * @param type if present, switch to a specific animation variant.
4584 : */
4585 1 : UnitAI.prototype.SetAnimationVariant = function(type)
4586 : {
4587 58 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
4588 58 : if (!cmpVisual)
4589 58 : return;
4590 :
4591 0 : cmpVisual.SetVariant("animationVariant", type);
4592 : };
4593 :
4594 : /*
4595 : * Reset the animation variant to default behavior.
4596 : * Default behavior is to pick a resource-carrying variant if resources are being carried.
4597 : * Otherwise pick nothing in particular.
4598 : */
4599 1 : UnitAI.prototype.SetDefaultAnimationVariant = function()
4600 : {
4601 41 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
4602 41 : if (cmpResourceGatherer)
4603 : {
4604 0 : let type = cmpResourceGatherer.GetLastCarriedType();
4605 0 : if (type)
4606 : {
4607 0 : let typename = "carry_" + type.generic;
4608 :
4609 0 : if (type.specific == "meat")
4610 0 : typename = "carry_" + type.specific;
4611 :
4612 0 : this.SetAnimationVariant(typename);
4613 0 : return;
4614 : }
4615 : }
4616 :
4617 41 : this.SetAnimationVariant("");
4618 : };
4619 :
4620 1 : UnitAI.prototype.ResetAnimation = function()
4621 : {
4622 0 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
4623 0 : if (!cmpVisual)
4624 0 : return;
4625 :
4626 0 : cmpVisual.SelectAnimation("idle", false, 1.0);
4627 : };
4628 :
4629 1 : UnitAI.prototype.SelectAnimation = function(name, once = false, speed = 1.0)
4630 : {
4631 19 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
4632 19 : if (!cmpVisual)
4633 19 : return;
4634 :
4635 0 : cmpVisual.SelectAnimation(name, once, speed);
4636 : };
4637 :
4638 1 : UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
4639 : {
4640 0 : var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
4641 0 : if (!cmpVisual)
4642 0 : return;
4643 :
4644 0 : cmpVisual.SetAnimationSyncRepeat(repeattime);
4645 0 : cmpVisual.SetAnimationSyncOffset(actiontime);
4646 : };
4647 :
4648 1 : UnitAI.prototype.StopMoving = function()
4649 : {
4650 20 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4651 20 : if (cmpUnitMotion)
4652 18 : cmpUnitMotion.StopMoving();
4653 : };
4654 :
4655 : /**
4656 : * Generic dispatcher for other MoveTo functions.
4657 : * @param iid - Interface ID (optional) implementing GetRange
4658 : * @param type - Range type for the interface call
4659 : * @returns whether the move succeeded or failed.
4660 : */
4661 1 : UnitAI.prototype.MoveTo = function(data, iid, type)
4662 : {
4663 4 : if (data.target)
4664 : {
4665 0 : if (data.min || data.max)
4666 0 : return this.MoveToTargetRangeExplicit(data.target, data.min || -1, data.max || -1);
4667 0 : else if (!iid)
4668 0 : return this.MoveToTarget(data.target);
4669 :
4670 0 : return this.MoveToTargetRange(data.target, iid, type);
4671 : }
4672 4 : else if (data.min || data.max)
4673 0 : return this.MoveToPointRange(data.x, data.z, data.min || -1, data.max || -1);
4674 :
4675 4 : return this.MoveToPoint(data.x, data.z);
4676 : };
4677 :
4678 1 : UnitAI.prototype.MoveToPoint = function(x, z)
4679 : {
4680 4 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4681 4 : return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToPointRange(x, z, 0, 0); // For point goals, allow a max range of 0.
4682 : };
4683 :
4684 1 : UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
4685 : {
4686 0 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4687 0 : return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
4688 : };
4689 :
4690 1 : UnitAI.prototype.MoveToTarget = function(target)
4691 : {
4692 0 : if (!this.CheckTargetVisible(target))
4693 0 : return false;
4694 :
4695 0 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4696 0 : return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, 0, 1);
4697 : };
4698 :
4699 1 : UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
4700 : {
4701 0 : if (!this.CheckTargetVisible(target))
4702 0 : return false;
4703 :
4704 0 : let range = this.GetRange(iid, type, target);
4705 0 : if (!range)
4706 0 : return false;
4707 :
4708 0 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4709 0 : return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
4710 : };
4711 :
4712 : /**
4713 : * Move unit so we hope the target is in the attack range
4714 : * for melee attacks, this goes straight to the default range checks
4715 : * for ranged attacks, the parabolic range is used
4716 : */
4717 1 : UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
4718 : {
4719 : // for formation members, the formation will take care of the range check
4720 0 : if (this.IsFormationMember())
4721 : {
4722 0 : let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
4723 0 : if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation())
4724 0 : return false;
4725 : }
4726 :
4727 0 : const cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4728 0 : if (!this.AbleToMove(cmpUnitMotion))
4729 0 : return false;
4730 :
4731 0 : const cmpFormation = Engine.QueryInterface(target, IID_Formation);
4732 0 : if (cmpFormation)
4733 0 : target = cmpFormation.GetClosestMember(this.entity);
4734 :
4735 0 : if (type != "Ranged")
4736 0 : return this.MoveToTargetRange(target, IID_Attack, type);
4737 :
4738 0 : if (!this.CheckTargetVisible(target))
4739 0 : return false;
4740 :
4741 0 : const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
4742 0 : if (!cmpAttack)
4743 0 : return false;
4744 0 : const range = cmpAttack.GetRange(type);
4745 :
4746 : // In case the range returns negative, we are probably too high compared to the target. Hope we come close enough.
4747 0 : const parabolicMaxRange = Math.max(0, Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEffectiveParabolicRange(this.entity, target, range.max, cmpAttack.GetAttackYOrigin(type)));
4748 :
4749 : // The parabole changes while walking so be cautious:
4750 0 : const guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange;
4751 :
4752 0 : return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange);
4753 : };
4754 :
4755 1 : UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
4756 : {
4757 0 : if (!this.CheckTargetVisible(target))
4758 0 : return false;
4759 :
4760 0 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4761 0 : return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, min, max);
4762 : };
4763 :
4764 : /**
4765 : * Move unit so we hope the target is in the attack range of the formation.
4766 : *
4767 : * @param {number} target - The target entity ID to attack.
4768 : * @return {boolean} - Whether the order to move has succeeded.
4769 : */
4770 1 : UnitAI.prototype.MoveFormationToTargetAttackRange = function(target)
4771 : {
4772 0 : let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
4773 0 : if (cmpTargetFormation)
4774 0 : target = cmpTargetFormation.GetClosestMember(this.entity);
4775 :
4776 0 : if (!this.CheckTargetVisible(target))
4777 0 : return false;
4778 :
4779 0 : let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack);
4780 0 : if (!cmpFormationAttack)
4781 0 : return false;
4782 0 : let range = cmpFormationAttack.GetRange(target);
4783 :
4784 0 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4785 0 : return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
4786 : };
4787 :
4788 : /**
4789 : * Generic dispatcher for other Check...Range functions.
4790 : * @param iid - Interface ID (optional) implementing GetRange
4791 : * @param type - Range type for the interface call
4792 : */
4793 1 : UnitAI.prototype.CheckRange = function(data, iid, type)
4794 : {
4795 0 : if (data.target)
4796 : {
4797 0 : if (data.min || data.max)
4798 0 : return this.CheckTargetRangeExplicit(data.target, data.min || -1, data.max || -1);
4799 0 : else if (!iid)
4800 0 : return this.CheckTargetRangeExplicit(data.target, 0, 1);
4801 :
4802 0 : return this.CheckTargetRange(data.target, iid, type);
4803 : }
4804 0 : else if (data.min || data.max)
4805 0 : return this.CheckPointRangeExplicit(data.x, data.z, data.min || -1, data.max || -1);
4806 :
4807 0 : return this.CheckPointRangeExplicit(data.x, data.z, 0, 0);
4808 : };
4809 :
4810 1 : UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max)
4811 : {
4812 0 : let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
4813 0 : return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max, false);
4814 : };
4815 :
4816 1 : UnitAI.prototype.CheckTargetRange = function(target, iid, type)
4817 : {
4818 1 : let range = this.GetRange(iid, type, target);
4819 1 : if (!range)
4820 1 : return false;
4821 :
4822 0 : let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
4823 0 : return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
4824 : };
4825 :
4826 : /**
4827 : * Check if the target is inside the attack range
4828 : * For melee attacks, this goes straigt to the regular range calculation
4829 : * For ranged attacks, the parabolic formula is used to accout for bigger ranges
4830 : * when the target is lower, and smaller ranges when the target is higher
4831 : */
4832 1 : UnitAI.prototype.CheckTargetAttackRange = function(target, type)
4833 : {
4834 : // for formation members, the formation will take care of the range check
4835 34 : if (this.IsFormationMember())
4836 : {
4837 32 : let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
4838 32 : if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation() &&
4839 : cmpFormationUnitAI.order.data.target == target)
4840 0 : return true;
4841 : }
4842 :
4843 34 : let cmpFormation = Engine.QueryInterface(target, IID_Formation);
4844 34 : if (cmpFormation)
4845 0 : target = cmpFormation.GetClosestMember(this.entity);
4846 :
4847 34 : let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
4848 34 : return cmpAttack && cmpAttack.IsTargetInRange(target, type);
4849 : };
4850 :
4851 1 : UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
4852 : {
4853 0 : let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
4854 0 : return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max, false);
4855 : };
4856 :
4857 : /**
4858 : * Check if the target is inside the attack range of the formation.
4859 : *
4860 : * @param {number} target - The target entity ID to attack.
4861 : * @return {boolean} - Whether the entity is within attacking distance.
4862 : */
4863 1 : UnitAI.prototype.CheckFormationTargetAttackRange = function(target)
4864 : {
4865 2 : let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
4866 2 : if (cmpTargetFormation)
4867 0 : target = cmpTargetFormation.GetClosestMember(this.entity);
4868 :
4869 2 : let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack);
4870 2 : if (!cmpFormationAttack)
4871 0 : return false;
4872 2 : let range = cmpFormationAttack.GetRange(target);
4873 :
4874 2 : let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
4875 2 : return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
4876 : };
4877 :
4878 : /**
4879 : * Returns true if the target entity is visible through the FoW/SoD.
4880 : */
4881 1 : UnitAI.prototype.CheckTargetVisible = function(target)
4882 : {
4883 0 : if (this.isGarrisoned)
4884 0 : return false;
4885 :
4886 0 : const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
4887 0 : if (!cmpOwnership)
4888 0 : return false;
4889 :
4890 0 : const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
4891 0 : if (!cmpRangeManager)
4892 0 : return false;
4893 :
4894 : // Entities that are hidden and miraged are considered visible
4895 0 : const cmpFogging = Engine.QueryInterface(target, IID_Fogging);
4896 0 : if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
4897 0 : return true;
4898 :
4899 0 : if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) == "hidden")
4900 0 : return false;
4901 :
4902 : // Either visible directly, or visible in fog
4903 0 : return true;
4904 : };
4905 :
4906 : /**
4907 : * Returns true if the given position is currentl visible (not in FoW/SoD).
4908 : */
4909 1 : UnitAI.prototype.CheckPositionVisible = function(x, z)
4910 : {
4911 0 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
4912 0 : if (!cmpOwnership)
4913 0 : return false;
4914 :
4915 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
4916 0 : if (!cmpRangeManager)
4917 0 : return false;
4918 :
4919 0 : return cmpRangeManager.GetLosVisibilityPosition(x, z, cmpOwnership.GetOwner()) == "visible";
4920 : };
4921 :
4922 : /**
4923 : * How close to our goal do we consider it's OK to stop if the goal appears unreachable.
4924 : * Currently 3 terrain tiles as that's relatively close but helps pathfinding.
4925 : */
4926 1 : UnitAI.prototype.DefaultRelaxedMaxRange = 12;
4927 :
4928 : /**
4929 : * @returns true if the unit is in the relaxed-range from the target.
4930 : */
4931 1 : UnitAI.prototype.RelaxedMaxRangeCheck = function(data, relaxedRange)
4932 : {
4933 0 : if (!data.relaxed)
4934 0 : return false;
4935 :
4936 0 : let ndata = data;
4937 0 : ndata.min = 0;
4938 0 : ndata.max = relaxedRange;
4939 0 : return this.CheckRange(ndata);
4940 : };
4941 :
4942 : /**
4943 : * Let an entity face its target.
4944 : * @param {number} target - The entity-ID of the target.
4945 : */
4946 1 : UnitAI.prototype.FaceTowardsTarget = function(target)
4947 : {
4948 19 : let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
4949 19 : if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
4950 19 : return;
4951 :
4952 0 : let targetPosition = cmpTargetPosition.GetPosition2D();
4953 :
4954 : // Use cmpUnitMotion for units that support that, otherwise try cmpPosition (e.g. turrets)
4955 0 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
4956 0 : if (cmpUnitMotion)
4957 : {
4958 0 : cmpUnitMotion.FaceTowardsPoint(targetPosition.x, targetPosition.y);
4959 0 : return;
4960 : }
4961 :
4962 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
4963 0 : if (cmpPosition && cmpPosition.IsInWorld())
4964 0 : cmpPosition.TurnTo(cmpPosition.GetPosition2D().angleTo(targetPosition));
4965 : };
4966 :
4967 1 : UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type)
4968 : {
4969 0 : let range = this.GetRange(iid, type, target);
4970 0 : if (!range)
4971 0 : return false;
4972 :
4973 0 : let cmpPosition = Engine.QueryInterface(target, IID_Position);
4974 0 : if (!cmpPosition || !cmpPosition.IsInWorld())
4975 0 : return false;
4976 :
4977 0 : let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
4978 0 : if (!cmpVision)
4979 0 : return false;
4980 0 : let halfvision = cmpVision.GetRange() / 2;
4981 :
4982 0 : let pos = cmpPosition.GetPosition();
4983 0 : let heldPosition = this.heldPosition;
4984 0 : if (heldPosition === undefined)
4985 0 : heldPosition = { "x": pos.x, "z": pos.z };
4986 :
4987 0 : return Math.euclidDistance2D(pos.x, pos.z, heldPosition.x, heldPosition.z) < halfvision + range.max;
4988 : };
4989 :
4990 1 : UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
4991 : {
4992 0 : let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
4993 0 : if (!cmpVision)
4994 0 : return false;
4995 :
4996 0 : let range = cmpVision.GetRange();
4997 0 : let distance = PositionHelper.DistanceBetweenEntities(this.entity, target);
4998 :
4999 0 : return distance < range;
5000 : };
5001 :
5002 1 : UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture = this.DEFAULT_CAPTURE)
5003 : {
5004 17 : return Engine.QueryInterface(this.entity, IID_Attack)?.GetBestAttackAgainst(target, allowCapture);
5005 : };
5006 :
5007 : /**
5008 : * Try to find one of the given entities which can be attacked,
5009 : * and start attacking it.
5010 : * Returns true if it found something to attack.
5011 : */
5012 1 : UnitAI.prototype.AttackVisibleEntity = function(ents)
5013 : {
5014 1 : var target = ents.find(target => this.CanAttack(target));
5015 1 : if (!target)
5016 0 : return false;
5017 :
5018 1 : this.PushOrderFront("Attack", { "target": target, "force": false });
5019 1 : return true;
5020 : };
5021 :
5022 : /**
5023 : * Try to find one of the given entities which can be attacked
5024 : * and which is close to the hold position, and start attacking it.
5025 : * Returns true if it found something to attack.
5026 : */
5027 1 : UnitAI.prototype.AttackEntityInZone = function(ents)
5028 : {
5029 0 : var target = ents.find(target =>
5030 0 : this.CanAttack(target) &&
5031 : this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true)) &&
5032 : (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
5033 : );
5034 0 : if (!target)
5035 0 : return false;
5036 :
5037 0 : this.PushOrderFront("Attack", { "target": target, "force": false });
5038 0 : return true;
5039 : };
5040 :
5041 : /**
5042 : * Try to respond appropriately given our current stance,
5043 : * given a list of entities that match our stance's target criteria.
5044 : * Returns true if it responded.
5045 : */
5046 1 : UnitAI.prototype.RespondToTargetedEntities = function(ents)
5047 : {
5048 2 : if (!ents.length)
5049 1 : return false;
5050 :
5051 1 : if (this.GetStance().respondChase)
5052 1 : return this.AttackVisibleEntity(ents);
5053 :
5054 0 : if (this.GetStance().respondStandGround)
5055 0 : return this.AttackVisibleEntity(ents);
5056 :
5057 0 : if (this.GetStance().respondHoldGround)
5058 0 : return this.AttackEntityInZone(ents);
5059 :
5060 0 : if (this.GetStance().respondFlee)
5061 : {
5062 0 : if (this.order && this.order.type == "Flee")
5063 0 : this.orderQueue.shift();
5064 0 : this.PushOrderFront("Flee", { "target": ents[0], "force": false });
5065 0 : return true;
5066 : }
5067 :
5068 0 : return false;
5069 : };
5070 :
5071 : /**
5072 : * @param {number} ents - An array of the IDs of the spotted entities.
5073 : * @return {boolean} - Whether we responded.
5074 : */
5075 1 : UnitAI.prototype.RespondToSightedEntities = function(ents)
5076 : {
5077 0 : if (!ents || !ents.length)
5078 0 : return false;
5079 :
5080 0 : if (this.GetStance().respondFleeOnSight)
5081 : {
5082 0 : this.Flee(ents[0], false);
5083 0 : return true;
5084 : }
5085 :
5086 0 : return false;
5087 : };
5088 :
5089 : /**
5090 : * Try to respond to healable entities.
5091 : * Returns true if it responded.
5092 : */
5093 1 : UnitAI.prototype.RespondToHealableEntities = function(ents)
5094 : {
5095 0 : let ent = ents.find(ent => this.CanHeal(ent));
5096 0 : if (!ent)
5097 0 : return false;
5098 :
5099 0 : this.PushOrderFront("Heal", { "target": ent, "force": false });
5100 0 : return true;
5101 : };
5102 :
5103 : /**
5104 : * Returns true if we should stop following the target entity.
5105 : */
5106 1 : UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type)
5107 : {
5108 0 : if (!this.CheckTargetVisible(target))
5109 0 : return true;
5110 :
5111 : // Forced orders shouldn't be interrupted.
5112 0 : if (force)
5113 0 : return false;
5114 :
5115 : // If we are guarding/escorting, don't abandon as long as the guarded unit is in target range of the attacker
5116 0 : if (this.isGuardOf)
5117 : {
5118 0 : let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
5119 0 : let cmpAttack = Engine.QueryInterface(target, IID_Attack);
5120 0 : if (cmpUnitAI && cmpAttack &&
5121 0 : cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)))
5122 0 : return false;
5123 : }
5124 :
5125 0 : if (this.GetStance().respondHoldGround)
5126 0 : if (!this.CheckTargetDistanceFromHeldPosition(target, iid, type))
5127 0 : return true;
5128 :
5129 : // Stop if it's left our vision range, unless we're especially persistent.
5130 0 : if (!this.GetStance().respondChaseBeyondVision)
5131 0 : if (!this.CheckTargetIsInVisionRange(target))
5132 0 : return true;
5133 :
5134 0 : return false;
5135 : };
5136 :
5137 : /*
5138 : * Returns whether we should chase the targeted entity,
5139 : * given our current stance.
5140 : */
5141 1 : UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
5142 : {
5143 0 : if (!this.AbleToMove())
5144 0 : return false;
5145 :
5146 0 : if (this.GetStance().respondChase)
5147 0 : return true;
5148 :
5149 : // If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker
5150 0 : if (this.isGuardOf)
5151 : {
5152 0 : let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
5153 0 : let cmpAttack = Engine.QueryInterface(target, IID_Attack);
5154 0 : if (cmpUnitAI && cmpAttack &&
5155 0 : cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)))
5156 0 : return true;
5157 : }
5158 :
5159 0 : return force;
5160 : };
5161 :
5162 : // External interface functions
5163 :
5164 : /**
5165 : * Order a unit to leave the formation it is in.
5166 : * Used to handle queued no-formation orders for units in formation.
5167 : */
5168 1 : UnitAI.prototype.LeaveFormation = function(queued = true)
5169 : {
5170 : // If queued, add the order even if we're not in formation,
5171 : // maybe we will be later.
5172 0 : if (!queued && !this.IsFormationMember())
5173 0 : return;
5174 :
5175 0 : if (queued)
5176 0 : this.AddOrder("LeaveFormation", { "force": true }, queued);
5177 : else
5178 0 : this.PushOrderFront("LeaveFormation", { "force": true });
5179 : };
5180 :
5181 1 : UnitAI.prototype.SetFormationController = function(ent)
5182 : {
5183 11 : this.formationController = ent;
5184 :
5185 : // Set obstruction group, so we can walk through members of our own formation.
5186 11 : Engine.QueryInterface(this.entity, IID_Obstruction)?.SetControlGroup(ent);
5187 11 : Engine.QueryInterface(this.entity, IID_UnitMotion)?.SetMemberOfFormation(ent);
5188 : };
5189 :
5190 1 : UnitAI.prototype.UnsetFormationController = function()
5191 : {
5192 11 : this.formationController = INVALID_ENTITY;
5193 11 : Engine.QueryInterface(this.entity, IID_Obstruction)?.SetControlGroup(this.entity);
5194 11 : Engine.QueryInterface(this.entity, IID_UnitMotion)?.SetMemberOfFormation(this.formationController);
5195 11 : this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
5196 : };
5197 :
5198 1 : UnitAI.prototype.GetFormationController = function()
5199 : {
5200 0 : return this.formationController;
5201 : };
5202 :
5203 1 : UnitAI.prototype.GetFormationTemplate = function()
5204 : {
5205 0 : return Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.formationController) || NULL_FORMATION;
5206 : };
5207 :
5208 1 : UnitAI.prototype.MoveIntoFormation = function(cmd)
5209 : {
5210 1 : var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
5211 1 : if (!cmpFormation)
5212 0 : return;
5213 :
5214 1 : var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
5215 1 : if (!cmpPosition || !cmpPosition.IsInWorld())
5216 0 : return;
5217 :
5218 1 : var pos = cmpPosition.GetPosition();
5219 1 : this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true });
5220 : };
5221 :
5222 1 : UnitAI.prototype.GetTargetPositions = function()
5223 : {
5224 10 : var targetPositions = [];
5225 10 : for (var i = 0; i < this.orderQueue.length; ++i)
5226 : {
5227 14 : var order = this.orderQueue[i];
5228 14 : switch (order.type)
5229 : {
5230 : case "Walk":
5231 : case "WalkAndFight":
5232 : case "WalkToPointRange":
5233 : case "MoveIntoFormation":
5234 : case "GatherNearPosition":
5235 : case "Patrol":
5236 10 : targetPositions.push(new Vector2D(order.data.x, order.data.z));
5237 10 : break; // and continue the loop
5238 :
5239 : case "WalkToTarget":
5240 : case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
5241 : case "Guard":
5242 : case "Flee":
5243 : case "LeaveFoundation":
5244 : case "Attack":
5245 : case "Heal":
5246 : case "Gather":
5247 : case "ReturnResource":
5248 : case "Repair":
5249 : case "Garrison":
5250 : case "CollectTreasure":
5251 4 : var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
5252 4 : if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
5253 4 : return targetPositions;
5254 0 : targetPositions.push(cmpTargetPosition.GetPosition2D());
5255 0 : return targetPositions;
5256 :
5257 : case "Stop":
5258 0 : return [];
5259 :
5260 : case "DropAtNearestDropSite":
5261 0 : break;
5262 :
5263 : default:
5264 0 : error("GetTargetPositions: Unrecognised order type '"+order.type+"'");
5265 0 : return [];
5266 : }
5267 : }
5268 6 : return targetPositions;
5269 : };
5270 :
5271 : /**
5272 : * Returns the estimated distance that this unit will travel before either
5273 : * finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
5274 : * Intended for Formation to switch to column layout on long walks.
5275 : */
5276 1 : UnitAI.prototype.ComputeWalkingDistance = function()
5277 : {
5278 5 : var distance = 0;
5279 :
5280 5 : var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
5281 5 : if (!cmpPosition || !cmpPosition.IsInWorld())
5282 0 : return 0;
5283 :
5284 : // Keep track of the position at the start of each order
5285 5 : var pos = cmpPosition.GetPosition2D();
5286 5 : var targetPositions = this.GetTargetPositions();
5287 5 : for (var i = 0; i < targetPositions.length; ++i)
5288 : {
5289 5 : distance += pos.distanceTo(targetPositions[i]);
5290 :
5291 : // Remember this as the start position for the next order
5292 5 : pos = targetPositions[i];
5293 : }
5294 :
5295 5 : return distance;
5296 : };
5297 :
5298 1 : UnitAI.prototype.AddOrder = function(type, data, queued, pushFront)
5299 : {
5300 42 : if (this.expectedRoute)
5301 0 : this.expectedRoute = undefined;
5302 :
5303 42 : if (pushFront)
5304 0 : this.PushOrderFront(type, data);
5305 42 : else if (queued)
5306 8 : this.PushOrder(type, data);
5307 : else
5308 34 : this.ReplaceOrder(type, data);
5309 : };
5310 :
5311 : /**
5312 : * Adds guard/escort order to the queue, forced by the player.
5313 : */
5314 1 : UnitAI.prototype.Guard = function(target, queued, pushFront)
5315 : {
5316 0 : if (!this.CanGuard())
5317 : {
5318 0 : this.WalkToTarget(target, queued);
5319 0 : return;
5320 : }
5321 :
5322 0 : if (target === this.entity)
5323 0 : return;
5324 :
5325 0 : if (this.isGuardOf)
5326 : {
5327 0 : if (this.isGuardOf == target && this.order && this.order.type == "Guard")
5328 0 : return;
5329 0 : this.RemoveGuard();
5330 : }
5331 :
5332 0 : this.AddOrder("Guard", { "target": target, "force": false }, queued, pushFront);
5333 : };
5334 :
5335 : /**
5336 : * @return {boolean} - Whether it makes sense to guard the given entity.
5337 : */
5338 1 : UnitAI.prototype.ShouldGuard = function(target)
5339 : {
5340 0 : return this.TargetIsAlive(target) ||
5341 : Engine.QueryInterface(target, IID_Capturable) ||
5342 : Engine.QueryInterface(target, IID_StatusEffectsReceiver);
5343 : };
5344 :
5345 1 : UnitAI.prototype.AddGuard = function(target)
5346 : {
5347 0 : if (!this.CanGuard())
5348 0 : return false;
5349 :
5350 0 : var cmpGuard = Engine.QueryInterface(target, IID_Guard);
5351 0 : if (!cmpGuard)
5352 0 : return false;
5353 :
5354 0 : this.isGuardOf = target;
5355 0 : this.guardRange = cmpGuard.GetRange(this.entity);
5356 0 : cmpGuard.AddGuard(this.entity);
5357 0 : return true;
5358 : };
5359 :
5360 1 : UnitAI.prototype.RemoveGuard = function()
5361 : {
5362 0 : if (!this.isGuardOf)
5363 0 : return;
5364 :
5365 0 : let cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard);
5366 0 : if (cmpGuard)
5367 0 : cmpGuard.RemoveGuard(this.entity);
5368 0 : this.guardRange = undefined;
5369 0 : this.isGuardOf = undefined;
5370 :
5371 0 : if (!this.order)
5372 0 : return;
5373 :
5374 0 : if (this.order.type == "Guard")
5375 0 : this.UnitFsm.ProcessMessage(this, { "type": "RemoveGuard" });
5376 : else
5377 0 : for (let i = 1; i < this.orderQueue.length; ++i)
5378 0 : if (this.orderQueue[i].type == "Guard")
5379 0 : this.orderQueue.splice(i, 1);
5380 0 : Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
5381 : };
5382 :
5383 1 : UnitAI.prototype.IsGuardOf = function()
5384 : {
5385 0 : return this.isGuardOf;
5386 : };
5387 :
5388 1 : UnitAI.prototype.SetGuardOf = function(entity)
5389 : {
5390 : // entity may be undefined
5391 0 : this.isGuardOf = entity;
5392 : };
5393 :
5394 1 : UnitAI.prototype.CanGuard = function()
5395 : {
5396 : // Formation controllers should always respond to commands
5397 : // (then the individual units can make up their own minds)
5398 0 : if (this.IsFormationController())
5399 0 : return true;
5400 :
5401 0 : return this.template.CanGuard == "true";
5402 : };
5403 :
5404 1 : UnitAI.prototype.CanPatrol = function()
5405 : {
5406 : // Formation controllers should always respond to commands
5407 : // (then the individual units can make up their own minds)
5408 0 : return this.IsFormationController() || this.template.CanPatrol == "true";
5409 : };
5410 :
5411 : /**
5412 : * Adds walk order to queue, forced by the player.
5413 : */
5414 1 : UnitAI.prototype.Walk = function(x, z, queued, pushFront)
5415 : {
5416 3 : if (!pushFront && this.expectedRoute && queued)
5417 0 : this.expectedRoute.push({ "x": x, "z": z });
5418 : else
5419 3 : this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued, pushFront);
5420 : };
5421 :
5422 : /**
5423 : * Adds walk to point range order to queue, forced by the player.
5424 : */
5425 1 : UnitAI.prototype.WalkToPointRange = function(x, z, min, max, queued, pushFront)
5426 : {
5427 0 : this.AddOrder("Walk", { "x": x, "z": z, "min": min, "max": max, "force": true }, queued, pushFront);
5428 : };
5429 :
5430 : /**
5431 : * Adds stop order to queue, forced by the player.
5432 : */
5433 1 : UnitAI.prototype.Stop = function(queued, pushFront)
5434 : {
5435 0 : this.AddOrder("Stop", { "force": true }, queued, pushFront);
5436 : };
5437 :
5438 : /**
5439 : * The unit will drop all resources at the closest dropsite. If this unit is no gatherer or
5440 : * no dropsite is available, it will do nothing.
5441 : */
5442 1 : UnitAI.prototype.DropAtNearestDropSite = function(queued, pushFront)
5443 : {
5444 0 : this.AddOrder("DropAtNearestDropSite", { "force": true }, queued, pushFront);
5445 : };
5446 :
5447 : /**
5448 : * Adds walk-to-target order to queue, this only occurs in response
5449 : * to a player order, and so is forced.
5450 : */
5451 1 : UnitAI.prototype.WalkToTarget = function(target, queued, pushFront)
5452 : {
5453 0 : this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued, pushFront);
5454 : };
5455 :
5456 : /**
5457 : * Adds walk-and-fight order to queue, this only occurs in response
5458 : * to a player order, and so is forced.
5459 : * If targetClasses is given, only entities matching the targetClasses can be attacked.
5460 : */
5461 1 : UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, allowCapture = this.DEFAULT_CAPTURE, queued = false, pushFront = false)
5462 : {
5463 0 : this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued, pushFront);
5464 : };
5465 :
5466 1 : UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = this.DEFAULT_CAPTURE, queued = false, pushFront = false)
5467 : {
5468 0 : if (!this.CanPatrol())
5469 : {
5470 0 : this.Walk(x, z, queued);
5471 0 : return;
5472 : }
5473 :
5474 0 : this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued, pushFront);
5475 : };
5476 :
5477 : /**
5478 : * Adds leave foundation order to queue, treated as forced.
5479 : */
5480 1 : UnitAI.prototype.LeaveFoundation = function(target)
5481 : {
5482 : // If we're already being told to leave a foundation, then
5483 : // ignore this new request so we don't end up being too indecisive
5484 : // to ever actually move anywhere.
5485 0 : if (this.order && (this.order.type == "LeaveFoundation" || (this.order.type == "Flee" && this.order.data.target == target)))
5486 0 : return;
5487 :
5488 0 : if (this.orderQueue.length && this.orderQueue[0].type == "Unpack" && this.WillMoveFromFoundation(target, false))
5489 : {
5490 0 : let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
5491 0 : if (cmpPack)
5492 0 : cmpPack.CancelPack();
5493 : }
5494 :
5495 0 : if (this.IsPacking())
5496 0 : return;
5497 :
5498 0 : this.PushOrderFront("LeaveFoundation", { "target": target, "force": true });
5499 : };
5500 :
5501 : /**
5502 : * Adds attack order to the queue, forced by the player.
5503 : */
5504 1 : UnitAI.prototype.Attack = function(target, allowCapture = this.DEFAULT_CAPTURE, queued = false, pushFront = false)
5505 : {
5506 17 : if (!this.CanAttack(target))
5507 : {
5508 : // We don't want to let healers walk to the target unit so they can be easily killed.
5509 : // Instead we just let them get into healing range.
5510 0 : if (this.IsHealer())
5511 0 : this.MoveToTargetRange(target, IID_Heal);
5512 : else
5513 0 : this.WalkToTarget(target, queued, pushFront);
5514 0 : return;
5515 : }
5516 :
5517 17 : let order = {
5518 : "target": target,
5519 : "force": true,
5520 : "allowCapture": allowCapture,
5521 : };
5522 :
5523 17 : this.RememberTargetPosition(order);
5524 :
5525 17 : if (this.order && this.order.type == "Attack" &&
5526 : this.order.data &&
5527 : this.order.data.target === order.target &&
5528 : this.order.data.allowCapture === order.allowCapture)
5529 : {
5530 0 : this.order.data.lastPos = order.lastPos;
5531 0 : this.order.data.force = order.force;
5532 0 : if (order.force)
5533 0 : this.orderQueue = [this.order];
5534 0 : return;
5535 : }
5536 :
5537 17 : this.AddOrder("Attack", order, queued, pushFront);
5538 : };
5539 :
5540 : /**
5541 : * Adds garrison order to the queue, forced by the player.
5542 : */
5543 1 : UnitAI.prototype.Garrison = function(target, queued, pushFront)
5544 : {
5545 : // Not allowed to garrison when occupying a turret, at the moment.
5546 1 : if (this.isGarrisoned || this.IsTurret())
5547 0 : return;
5548 1 : if (target == this.entity)
5549 0 : return;
5550 1 : if (!this.CanGarrison(target))
5551 : {
5552 0 : this.WalkToTarget(target, queued);
5553 0 : return;
5554 : }
5555 1 : this.AddOrder("Garrison", { "target": target, "force": true, "garrison": true }, queued, pushFront);
5556 : };
5557 :
5558 : /**
5559 : * Adds ungarrison order to the queue.
5560 : */
5561 1 : UnitAI.prototype.Ungarrison = function()
5562 : {
5563 0 : if (!this.isGarrisoned && !this.IsTurret())
5564 0 : return;
5565 0 : this.AddOrder("Ungarrison", null, false);
5566 : };
5567 :
5568 : /**
5569 : * Adds garrison order to the queue, forced by the player.
5570 : */
5571 1 : UnitAI.prototype.OccupyTurret = function(target, queued, pushFront)
5572 : {
5573 0 : if (target == this.entity)
5574 0 : return;
5575 0 : if (!this.CanOccupyTurret(target))
5576 : {
5577 0 : this.WalkToTarget(target, queued);
5578 0 : return;
5579 : }
5580 0 : this.AddOrder("Garrison", { "target": target, "force": true, "garrison": false }, queued, pushFront);
5581 : };
5582 :
5583 : /**
5584 : * Adds gather order to the queue, forced by the player
5585 : * until the target is reached
5586 : */
5587 1 : UnitAI.prototype.Gather = function(target, queued, pushFront)
5588 : {
5589 0 : this.PerformGather(target, queued, true, pushFront);
5590 : };
5591 :
5592 : /**
5593 : * Internal function to abstract the force parameter.
5594 : */
5595 1 : UnitAI.prototype.PerformGather = function(target, queued, force, pushFront = false)
5596 : {
5597 0 : if (!this.CanGather(target))
5598 : {
5599 0 : this.WalkToTarget(target, queued);
5600 0 : return;
5601 : }
5602 :
5603 : // Save the resource type now, so if the resource gets destroyed
5604 : // before we process the order then we still know what resource
5605 : // type to look for more of
5606 : var type;
5607 0 : var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
5608 0 : if (cmpResourceSupply)
5609 0 : type = cmpResourceSupply.GetType();
5610 : else
5611 0 : error("CanGather allowed gathering from invalid entity");
5612 :
5613 : // Also save the target entity's template, so that if it's an animal,
5614 : // we won't go from hunting slow safe animals to dangerous fast ones
5615 0 : var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
5616 0 : var template = cmpTemplateManager.GetCurrentTemplateName(target);
5617 0 : if (template.indexOf("resource|") != -1)
5618 0 : template = template.slice(9);
5619 :
5620 0 : let order = {
5621 : "target": target,
5622 : "type": type,
5623 : "template": template,
5624 : "force": force,
5625 : };
5626 :
5627 0 : this.RememberTargetPosition(order);
5628 0 : order.initPos = order.lastPos;
5629 :
5630 0 : if (this.order &&
5631 : (this.order.type == "Gather" || this.order.type == "Attack") &&
5632 : this.order.data &&
5633 : this.order.data.target === order.target)
5634 : {
5635 0 : this.order.data.lastPos = order.lastPos;
5636 0 : this.order.data.force = order.force;
5637 0 : if (order.force)
5638 : {
5639 0 : if (this.orderQueue[1]?.type === "Gather")
5640 0 : this.orderQueue = [this.order, this.orderQueue[1]];
5641 : else
5642 0 : this.orderQueue = [this.order];
5643 : }
5644 0 : return;
5645 : }
5646 :
5647 0 : this.AddOrder("Gather", order, queued, pushFront);
5648 : };
5649 :
5650 : /**
5651 : * Adds gather-near-position order to the queue, not forced, so it can be
5652 : * interrupted by attacks.
5653 : */
5654 1 : UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued, pushFront)
5655 : {
5656 0 : if (template.indexOf("resource|") != -1)
5657 0 : template = template.slice(9);
5658 :
5659 0 : if (this.IsFormationController() || Engine.QueryInterface(this.entity, IID_ResourceGatherer))
5660 0 : this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued, pushFront);
5661 : else
5662 0 : this.AddOrder("Walk", { "x": x, "z": z, "force": false }, queued, pushFront);
5663 : };
5664 :
5665 : /**
5666 : * Adds heal order to the queue, forced by the player.
5667 : */
5668 1 : UnitAI.prototype.Heal = function(target, queued, pushFront)
5669 : {
5670 0 : if (!this.CanHeal(target))
5671 : {
5672 0 : this.WalkToTarget(target, queued);
5673 0 : return;
5674 : }
5675 :
5676 0 : if (this.order && this.order.type == "Heal" &&
5677 : this.order.data &&
5678 : this.order.data.target === target)
5679 : {
5680 0 : this.order.data.force = true;
5681 0 : this.orderQueue = [this.order];
5682 0 : return;
5683 : }
5684 :
5685 0 : this.AddOrder("Heal", { "target": target, "force": true }, queued, pushFront);
5686 : };
5687 :
5688 : /**
5689 : * Adds return resource order to the queue, forced by the player.
5690 : */
5691 1 : UnitAI.prototype.ReturnResource = function(target, queued, pushFront)
5692 : {
5693 0 : if (!this.CanReturnResource(target, true))
5694 : {
5695 0 : this.WalkToTarget(target, queued);
5696 0 : return;
5697 : }
5698 :
5699 0 : this.AddOrder("ReturnResource", { "target": target, "force": true }, queued, pushFront);
5700 : };
5701 :
5702 : /**
5703 : * Adds order to collect a treasure to queue, forced by the player.
5704 : */
5705 1 : UnitAI.prototype.CollectTreasure = function(target, queued, pushFront)
5706 : {
5707 0 : this.AddOrder("CollectTreasure", {
5708 : "target": target,
5709 : "force": true
5710 : }, queued, pushFront);
5711 : };
5712 :
5713 : /**
5714 : * Adds order to collect a treasure to queue, forced by the player.
5715 : */
5716 1 : UnitAI.prototype.CollectTreasureNearPosition = function(posX, posZ, queued, pushFront)
5717 : {
5718 0 : this.AddOrder("CollectTreasureNearPosition", {
5719 : "x": posX,
5720 : "z": posZ,
5721 : "force": true
5722 : }, queued, pushFront);
5723 : };
5724 :
5725 1 : UnitAI.prototype.CancelSetupTradeRoute = function(target)
5726 : {
5727 0 : let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
5728 0 : if (!cmpTrader)
5729 0 : return;
5730 0 : cmpTrader.RemoveTargetMarket(target);
5731 :
5732 0 : if (this.IsFormationController())
5733 0 : this.CallMemberFunction("CancelSetupTradeRoute", [target]);
5734 : };
5735 :
5736 : /**
5737 : * Adds trade order to the queue. Either walk to the first market, or
5738 : * start a new route. Not forced, so it can be interrupted by attacks.
5739 : * The possible route may be given directly as a SetupTradeRoute argument
5740 : * if coming from a RallyPoint, or through this.expectedRoute if a user command.
5741 : */
5742 1 : UnitAI.prototype.SetupTradeRoute = function(target, source, route, queued, pushFront)
5743 : {
5744 0 : if (!this.CanTrade(target))
5745 : {
5746 0 : this.WalkToTarget(target, queued);
5747 0 : return;
5748 : }
5749 :
5750 : // AI has currently no access to BackToWork
5751 0 : let cmpPlayer = QueryOwnerInterface(this.entity);
5752 0 : if (cmpPlayer && cmpPlayer.IsAI() && !this.IsFormationController() &&
5753 : this.workOrders.length && this.workOrders[0].type == "Trade")
5754 : {
5755 0 : let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
5756 0 : if (cmpTrader.HasBothMarkets() &&
5757 : (cmpTrader.GetFirstMarket() == target && cmpTrader.GetSecondMarket() == source ||
5758 : cmpTrader.GetFirstMarket() == source && cmpTrader.GetSecondMarket() == target))
5759 : {
5760 0 : this.BackToWork();
5761 0 : return;
5762 : }
5763 : }
5764 :
5765 0 : var marketsChanged = this.SetTargetMarket(target, source);
5766 0 : if (!marketsChanged)
5767 0 : return;
5768 :
5769 0 : var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
5770 0 : if (cmpTrader.HasBothMarkets())
5771 : {
5772 0 : let data = {
5773 : "target": cmpTrader.GetFirstMarket(),
5774 : "route": route,
5775 : "force": false
5776 : };
5777 :
5778 0 : if (this.expectedRoute)
5779 : {
5780 0 : if (!route && this.expectedRoute.length)
5781 0 : data.route = this.expectedRoute.slice();
5782 0 : this.expectedRoute = undefined;
5783 : }
5784 :
5785 0 : if (this.IsFormationController())
5786 : {
5787 0 : this.CallMemberFunction("AddOrder", ["Trade", data, queued]);
5788 0 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
5789 0 : if (cmpFormation)
5790 0 : cmpFormation.Disband();
5791 : }
5792 : else
5793 0 : this.AddOrder("Trade", data, queued, pushFront);
5794 : }
5795 : else
5796 : {
5797 0 : if (this.IsFormationController())
5798 0 : this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued, pushFront]);
5799 : else
5800 0 : this.WalkToTarget(cmpTrader.GetFirstMarket(), queued, pushFront);
5801 0 : this.expectedRoute = [];
5802 : }
5803 : };
5804 :
5805 1 : UnitAI.prototype.SetTargetMarket = function(target, source)
5806 : {
5807 0 : var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
5808 0 : if (!cmpTrader)
5809 0 : return false;
5810 0 : var marketsChanged = cmpTrader.SetTargetMarket(target, source);
5811 :
5812 0 : if (this.IsFormationController())
5813 0 : this.CallMemberFunction("SetTargetMarket", [target, source]);
5814 :
5815 0 : return marketsChanged;
5816 : };
5817 :
5818 1 : UnitAI.prototype.SwitchMarketOrder = function(oldMarket, newMarket)
5819 : {
5820 0 : if (this.order && this.order.data && this.order.data.target && this.order.data.target == oldMarket)
5821 0 : this.order.data.target = newMarket;
5822 : };
5823 :
5824 1 : UnitAI.prototype.MoveToMarket = function(targetMarket)
5825 : {
5826 : let nextTarget;
5827 0 : if (this.waypoints && this.waypoints.length >= 1)
5828 0 : nextTarget = this.waypoints.pop();
5829 : else
5830 0 : nextTarget = { "target": targetMarket };
5831 0 : this.order.data.nextTarget = nextTarget;
5832 0 : return this.MoveTo(this.order.data.nextTarget, IID_Trader);
5833 : };
5834 :
5835 1 : UnitAI.prototype.MarketRemoved = function(market)
5836 : {
5837 0 : if (this.order && this.order.data && this.order.data.target && this.order.data.target == market)
5838 0 : this.UnitFsm.ProcessMessage(this, { "type": "TradingCanceled", "market": market });
5839 : };
5840 :
5841 : /**
5842 : * Adds repair/build order to the queue, forced by the player
5843 : * until the target is reached
5844 : */
5845 1 : UnitAI.prototype.Repair = function(target, autocontinue, queued, pushFront)
5846 : {
5847 1 : if (!this.CanRepair(target))
5848 : {
5849 0 : this.WalkToTarget(target, queued);
5850 0 : return;
5851 : }
5852 :
5853 1 : if (this.order && this.order.type == "Repair" &&
5854 : this.order.data &&
5855 : this.order.data.target === target &&
5856 : this.order.data.autocontinue === autocontinue)
5857 : {
5858 0 : this.order.data.force = true;
5859 0 : this.orderQueue = [this.order];
5860 0 : return;
5861 : }
5862 :
5863 1 : this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued, pushFront);
5864 : };
5865 :
5866 : /**
5867 : * Adds flee order to the queue, not forced, so it can be
5868 : * interrupted by attacks.
5869 : */
5870 1 : UnitAI.prototype.Flee = function(target, queued, pushFront)
5871 : {
5872 1 : this.AddOrder("Flee", { "target": target, "force": false }, queued, pushFront);
5873 : };
5874 :
5875 1 : UnitAI.prototype.Cheer = function()
5876 : {
5877 0 : this.PushOrderFront("Cheer", { "force": false });
5878 : };
5879 :
5880 1 : UnitAI.prototype.Pack = function(queued, pushFront)
5881 : {
5882 0 : if (this.CanPack())
5883 0 : this.AddOrder("Pack", { "force": true }, queued, pushFront);
5884 : };
5885 :
5886 1 : UnitAI.prototype.Unpack = function(queued, pushFront)
5887 : {
5888 0 : if (this.CanUnpack())
5889 0 : this.AddOrder("Unpack", { "force": true }, queued, pushFront);
5890 : };
5891 :
5892 1 : UnitAI.prototype.CancelPack = function(queued, pushFront)
5893 : {
5894 0 : var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
5895 0 : if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked())
5896 0 : this.AddOrder("CancelPack", { "force": true }, queued, pushFront);
5897 : };
5898 :
5899 1 : UnitAI.prototype.CancelUnpack = function(queued, pushFront)
5900 : {
5901 0 : var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
5902 0 : if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked())
5903 0 : this.AddOrder("CancelUnpack", { "force": true }, queued, pushFront);
5904 : };
5905 :
5906 1 : UnitAI.prototype.SetStance = function(stance)
5907 : {
5908 19 : if (g_Stances[stance])
5909 : {
5910 19 : this.stance = stance;
5911 19 : Engine.PostMessage(this.entity, MT_UnitStanceChanged, { "to": this.stance });
5912 : }
5913 : else
5914 0 : error("UnitAI: Setting to invalid stance '"+stance+"'");
5915 : };
5916 :
5917 1 : UnitAI.prototype.SwitchToStance = function(stance)
5918 : {
5919 0 : var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
5920 0 : if (!cmpPosition || !cmpPosition.IsInWorld())
5921 0 : return;
5922 0 : var pos = cmpPosition.GetPosition();
5923 0 : this.SetHeldPosition(pos.x, pos.z);
5924 :
5925 0 : this.SetStance(stance);
5926 :
5927 : // Reset the range queries, since the range depends on stance.
5928 0 : this.SetupRangeQueries();
5929 : };
5930 :
5931 1 : UnitAI.prototype.SetTurretStance = function()
5932 : {
5933 0 : this.SetImmobile();
5934 0 : this.previousStance = undefined;
5935 0 : if (this.GetStance().respondStandGround)
5936 0 : return;
5937 0 : for (let stance in g_Stances)
5938 : {
5939 0 : if (!g_Stances[stance].respondStandGround)
5940 0 : continue;
5941 0 : this.previousStance = this.GetStanceName();
5942 0 : this.SwitchToStance(stance);
5943 0 : return;
5944 : }
5945 : };
5946 :
5947 1 : UnitAI.prototype.ResetTurretStance = function()
5948 : {
5949 0 : this.SetMobile();
5950 0 : if (!this.previousStance)
5951 0 : return;
5952 0 : this.SwitchToStance(this.previousStance);
5953 0 : this.previousStance = undefined;
5954 : };
5955 :
5956 : /**
5957 : * Resets the losRangeQuery.
5958 : * @return {boolean} - Whether there are targets in range that we ought to react upon.
5959 : */
5960 1 : UnitAI.prototype.FindSightedEnemies = function()
5961 : {
5962 2 : if (!this.losRangeQuery)
5963 2 : return false;
5964 :
5965 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
5966 0 : return this.RespondToSightedEntities(cmpRangeManager.ResetActiveQuery(this.losRangeQuery));
5967 : };
5968 :
5969 : /**
5970 : * Resets losHealRangeQuery, and if there are some targets in range that we can heal
5971 : * then we start healing and this returns true; otherwise, returns false.
5972 : */
5973 1 : UnitAI.prototype.FindNewHealTargets = function()
5974 : {
5975 0 : if (!this.losHealRangeQuery)
5976 0 : return false;
5977 :
5978 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
5979 0 : return this.RespondToHealableEntities(cmpRangeManager.ResetActiveQuery(this.losHealRangeQuery));
5980 : };
5981 :
5982 : /**
5983 : * Resets losAttackRangeQuery, and if there are some targets in range that we can
5984 : * attack then we start attacking and this returns true; otherwise, returns false.
5985 : */
5986 1 : UnitAI.prototype.FindNewTargets = function()
5987 : {
5988 3 : if (!this.losAttackRangeQuery)
5989 0 : return false;
5990 :
5991 3 : if (!this.GetStance().targetVisibleEnemies)
5992 0 : return false;
5993 :
5994 3 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
5995 3 : return this.AttackEntitiesByPreference(cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery));
5996 : };
5997 :
5998 1 : UnitAI.prototype.FindWalkAndFightTargets = function()
5999 : {
6000 10 : if (this.IsFormationController())
6001 0 : return this.CallMemberFunction("FindWalkAndFightTargets", null);
6002 :
6003 10 : let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
6004 :
6005 : let entities;
6006 10 : if (!this.losAttackRangeQuery || !this.GetStance().targetVisibleEnemies || !cmpAttack)
6007 0 : entities = [];
6008 : else
6009 : {
6010 10 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
6011 10 : entities = cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery);
6012 : }
6013 :
6014 10 : let attackfilter = e => {
6015 26 : if (this?.order?.data?.targetClasses)
6016 : {
6017 0 : let cmpIdentity = Engine.QueryInterface(e, IID_Identity);
6018 0 : let targetClasses = this.order.data.targetClasses;
6019 0 : if (cmpIdentity && targetClasses.attack &&
6020 : !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack))
6021 0 : return false;
6022 0 : if (cmpIdentity && targetClasses.avoid &&
6023 : MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid))
6024 0 : return false;
6025 : // Only used by the AIs to prevent some choices of targets
6026 0 : if (targetClasses.vetoEntities && targetClasses.vetoEntities[e])
6027 0 : return false;
6028 : }
6029 26 : let cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
6030 26 : if (cmpOwnership && cmpOwnership.GetOwner() > 0)
6031 17 : return true;
6032 9 : let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
6033 9 : return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
6034 : };
6035 :
6036 10 : const attack = target => {
6037 8 : const order = {
6038 : "target": target,
6039 : "force": false,
6040 : "allowCapture": this.order?.data?.allowCapture || this.DEFAULT_CAPTURE
6041 : };
6042 8 : if (this.IsFormationMember())
6043 0 : this.ReplaceOrder("Attack", order);
6044 : else
6045 8 : this.PushOrderFront("Attack", order);
6046 : };
6047 :
6048 10 : let prefs = {};
6049 : let bestPref;
6050 10 : let targets = [];
6051 : let pref;
6052 10 : for (let v of entities)
6053 : {
6054 40 : if (this.CanAttack(v) && attackfilter(v))
6055 : {
6056 17 : pref = cmpAttack.GetPreference(v);
6057 17 : if (pref === 0)
6058 : {
6059 4 : attack(v);
6060 4 : return true;
6061 : }
6062 13 : targets.push(v);
6063 : }
6064 36 : prefs[v] = pref;
6065 36 : if (pref !== undefined && (bestPref === undefined || pref < bestPref))
6066 5 : bestPref = pref;
6067 : }
6068 :
6069 6 : for (let targ of targets)
6070 : {
6071 6 : if (prefs[targ] !== bestPref)
6072 2 : continue;
6073 4 : attack(targ);
6074 4 : return true;
6075 : }
6076 :
6077 : // healers on a walk-and-fight order should heal injured units
6078 2 : if (this.IsHealer())
6079 0 : return this.FindNewHealTargets();
6080 :
6081 2 : return false;
6082 : };
6083 :
6084 1 : UnitAI.prototype.GetQueryRange = function(iid)
6085 : {
6086 11 : let ret = { "min": 0, "max": 0 };
6087 :
6088 11 : let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
6089 11 : if (!cmpVision)
6090 0 : return ret;
6091 11 : let visionRange = cmpVision.GetRange();
6092 :
6093 11 : if (iid === IID_Vision)
6094 : {
6095 0 : ret.max = visionRange;
6096 0 : return ret;
6097 : }
6098 :
6099 11 : if (this.GetStance().respondStandGround)
6100 : {
6101 0 : let range = this.GetRange(iid);
6102 0 : if (!range)
6103 0 : return ret;
6104 0 : ret.min = range.min;
6105 0 : ret.max = Math.min(range.max, visionRange);
6106 : }
6107 11 : else if (this.GetStance().respondChase)
6108 11 : ret.max = visionRange;
6109 0 : else if (this.GetStance().respondHoldGround)
6110 : {
6111 0 : let range = this.GetRange(iid);
6112 0 : if (!range)
6113 0 : return ret;
6114 0 : ret.max = Math.min(range.max + visionRange / 2, visionRange);
6115 : }
6116 : // We probably have stance 'passive' and we wouldn't have a range,
6117 : // but as it is the default for healers we need to set it to something sane.
6118 0 : else if (iid === IID_Heal)
6119 0 : ret.max = visionRange;
6120 :
6121 11 : return ret;
6122 : };
6123 :
6124 1 : UnitAI.prototype.GetStance = function()
6125 : {
6126 38 : return g_Stances[this.stance];
6127 : };
6128 :
6129 1 : UnitAI.prototype.GetSelectableStances = function()
6130 : {
6131 0 : if (this.IsTurret())
6132 0 : return [];
6133 0 : return Object.keys(g_Stances).filter(key => g_Stances[key].selectable);
6134 : };
6135 :
6136 1 : UnitAI.prototype.GetStanceName = function()
6137 : {
6138 0 : return this.stance;
6139 : };
6140 :
6141 : /*
6142 : * Make the unit walk at its normal pace.
6143 : */
6144 1 : UnitAI.prototype.ResetSpeedMultiplier = function()
6145 : {
6146 0 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
6147 0 : if (cmpUnitMotion)
6148 0 : cmpUnitMotion.SetSpeedMultiplier(1);
6149 : };
6150 :
6151 1 : UnitAI.prototype.SetSpeedMultiplier = function(speed)
6152 : {
6153 1 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
6154 1 : if (cmpUnitMotion)
6155 1 : cmpUnitMotion.SetSpeedMultiplier(speed);
6156 : };
6157 :
6158 : /**
6159 : * Try to match the targets current movement speed.
6160 : *
6161 : * @param {number} target - The entity ID of the target to match.
6162 : * @param {boolean} mayRun - Whether the entity is allowed to run to match the speed.
6163 : */
6164 1 : UnitAI.prototype.TryMatchTargetSpeed = function(target, mayRun = true)
6165 : {
6166 0 : let cmpUnitMotionTarget = Engine.QueryInterface(target, IID_UnitMotion);
6167 0 : if (cmpUnitMotionTarget)
6168 : {
6169 0 : let targetSpeed = cmpUnitMotionTarget.GetCurrentSpeed();
6170 0 : if (targetSpeed)
6171 0 : this.SetSpeedMultiplier(Math.min(mayRun ? this.GetRunMultiplier() : 1, targetSpeed / this.GetWalkSpeed()));
6172 : }
6173 : };
6174 :
6175 : /*
6176 : * Remember the position of the target (in lastPos), if any, in case it disappears later
6177 : * and we want to head to its last known position.
6178 : * @param orderData - The order data to set this on. Defaults to this.order.data
6179 : */
6180 1 : UnitAI.prototype.RememberTargetPosition = function(orderData)
6181 : {
6182 51 : if (!orderData)
6183 34 : orderData = this.order.data;
6184 51 : let cmpPosition = Engine.QueryInterface(orderData.target, IID_Position);
6185 51 : if (cmpPosition && cmpPosition.IsInWorld())
6186 0 : orderData.lastPos = cmpPosition.GetPosition();
6187 : };
6188 :
6189 1 : UnitAI.prototype.SetHeldPosition = function(x, z)
6190 : {
6191 19 : this.heldPosition = { "x": x, "z": z };
6192 : };
6193 :
6194 1 : UnitAI.prototype.SetHeldPositionOnEntity = function(entity)
6195 : {
6196 0 : var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
6197 0 : if (!cmpPosition || !cmpPosition.IsInWorld())
6198 0 : return;
6199 0 : var pos = cmpPosition.GetPosition();
6200 0 : this.SetHeldPosition(pos.x, pos.z);
6201 : };
6202 :
6203 1 : UnitAI.prototype.GetHeldPosition = function()
6204 : {
6205 0 : return this.heldPosition;
6206 : };
6207 :
6208 1 : UnitAI.prototype.WalkToHeldPosition = function()
6209 : {
6210 0 : if (this.heldPosition)
6211 : {
6212 0 : this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false, false);
6213 0 : return true;
6214 : }
6215 0 : return false;
6216 : };
6217 :
6218 : // Helper functions
6219 :
6220 : /**
6221 : * General getter for ranges.
6222 : *
6223 : * @param {number} iid
6224 : * @param {number} target - [Optional]
6225 : * @param {string} type - [Optional]
6226 : * @return {Object | undefined} - The range in the form
6227 : * { "min": number, "max": number }
6228 : * Returns undefined when the entity does not have the requested component.
6229 : */
6230 1 : UnitAI.prototype.GetRange = function(iid, type, target)
6231 : {
6232 1 : let component = Engine.QueryInterface(this.entity, iid);
6233 1 : if (!component)
6234 1 : return undefined;
6235 :
6236 0 : return component.GetRange(type, target);
6237 : };
6238 :
6239 1 : UnitAI.prototype.CanAttack = function(target)
6240 : {
6241 : // Formation controllers should always respond to commands
6242 : // (then the individual units can make up their own minds)
6243 18 : if (this.IsFormationController())
6244 1 : return true;
6245 :
6246 17 : let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
6247 17 : return cmpAttack && cmpAttack.CanAttack(target);
6248 : };
6249 :
6250 1 : UnitAI.prototype.CanGarrison = function(target)
6251 : {
6252 : // Formation controllers should always respond to commands
6253 : // (then the individual units can make up their own minds).
6254 0 : if (this.IsFormationController())
6255 0 : return true;
6256 :
6257 0 : let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable);
6258 0 : return cmpGarrisonable && cmpGarrisonable.CanGarrison(target);
6259 : };
6260 :
6261 1 : UnitAI.prototype.CanGather = function(target)
6262 : {
6263 : // Formation controllers should always respond to commands
6264 : // (then the individual units can make up their own minds).
6265 0 : if (this.IsFormationController())
6266 0 : return true;
6267 :
6268 0 : let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
6269 0 : return cmpResourceGatherer && cmpResourceGatherer.CanGather(target);
6270 : };
6271 :
6272 1 : UnitAI.prototype.CanHeal = function(target)
6273 : {
6274 : // Formation controllers should always respond to commands
6275 : // (then the individual units can make up their own minds)
6276 0 : if (this.IsFormationController())
6277 0 : return true;
6278 :
6279 0 : let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
6280 0 : return cmpHeal && cmpHeal.CanHeal(target);
6281 : };
6282 :
6283 : /**
6284 : * Check if the entity can return carried resources at @param target
6285 : * @param checkCarriedResource check we are carrying resources
6286 : * @param cmpResourceGatherer if present, use this directly instead of re-querying.
6287 : */
6288 1 : UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource, cmpResourceGatherer = undefined)
6289 : {
6290 : // Formation controllers should always respond to commands
6291 : // (then the individual units can make up their own minds).
6292 0 : if (this.IsFormationController())
6293 0 : return true;
6294 :
6295 0 : if (!cmpResourceGatherer)
6296 0 : cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
6297 :
6298 0 : return cmpResourceGatherer && cmpResourceGatherer.CanReturnResource(target, checkCarriedResource);
6299 : };
6300 :
6301 1 : UnitAI.prototype.CanTrade = function(target)
6302 : {
6303 : // Formation controllers should always respond to commands
6304 : // (then the individual units can make up their own minds).
6305 0 : if (this.IsFormationController())
6306 0 : return true;
6307 :
6308 0 : let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
6309 0 : return cmpTrader && cmpTrader.CanTrade(target);
6310 : };
6311 :
6312 1 : UnitAI.prototype.CanRepair = function(target)
6313 : {
6314 : // Formation controllers should always respond to commands
6315 : // (then the individual units can make up their own minds).
6316 0 : if (this.IsFormationController())
6317 0 : return true;
6318 :
6319 0 : let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
6320 0 : return cmpBuilder && cmpBuilder.CanRepair(target);
6321 : };
6322 :
6323 1 : UnitAI.prototype.CanOccupyTurret = function(target)
6324 : {
6325 : // Formation controllers should always respond to commands
6326 : // (then the individual units can make up their own minds).
6327 0 : if (this.IsFormationController())
6328 0 : return true;
6329 :
6330 0 : let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
6331 0 : return cmpTurretable && cmpTurretable.CanOccupy(target);
6332 : };
6333 :
6334 1 : UnitAI.prototype.CanPack = function()
6335 : {
6336 12 : let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
6337 12 : return cmpPack && cmpPack.CanPack();
6338 : };
6339 :
6340 1 : UnitAI.prototype.CanUnpack = function()
6341 : {
6342 17 : let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
6343 17 : return cmpPack && cmpPack.CanUnpack();
6344 : };
6345 :
6346 1 : UnitAI.prototype.IsPacking = function()
6347 : {
6348 35 : let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
6349 35 : return cmpPack && cmpPack.IsPacking();
6350 : };
6351 :
6352 : // Formation specific functions
6353 :
6354 1 : UnitAI.prototype.IsAttackingAsFormation = function()
6355 : {
6356 32 : var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
6357 32 : return cmpAttack && cmpAttack.CanAttackAsFormation() &&
6358 : this.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING";
6359 : };
6360 :
6361 1 : UnitAI.prototype.MoveRandomly = function(distance)
6362 : {
6363 : // To minimize drift all across the map, describe circles
6364 : // approximated by polygons.
6365 : // And to avoid getting stuck in obstacles or narrow spaces, each side
6366 : // of the polygon is obtained by trying to go away from a point situated
6367 : // half a meter backwards of the current position, after rotation.
6368 : // We also add a fluctuation on the length of each side of the polygon (dist)
6369 : // which, in addition to making the move more random, helps escaping narrow spaces
6370 : // with bigger values of dist.
6371 :
6372 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
6373 0 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
6374 0 : if (!cmpPosition || !cmpPosition.IsInWorld() || !cmpUnitMotion)
6375 0 : return;
6376 :
6377 0 : let pos = cmpPosition.GetPosition();
6378 0 : let ang = cmpPosition.GetRotation().y;
6379 :
6380 0 : if (!this.roamAngle)
6381 : {
6382 0 : this.roamAngle = (randBool() ? 1 : -1) * Math.PI / 6;
6383 0 : ang -= this.roamAngle / 2;
6384 0 : this.startAngle = ang;
6385 : }
6386 0 : else if (Math.abs((ang - this.startAngle + Math.PI) % (2 * Math.PI) - Math.PI) < Math.abs(this.roamAngle / 2))
6387 0 : this.roamAngle *= randBool() ? 1 : -1;
6388 :
6389 0 : let halfDelta = randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4);
6390 : // First half rotation to decrease the impression of immediate rotation
6391 0 : ang += halfDelta;
6392 0 : cmpUnitMotion.FaceTowardsPoint(pos.x + 0.5 * Math.sin(ang), pos.z + 0.5 * Math.cos(ang));
6393 : // Then second half of the rotation
6394 0 : ang += halfDelta;
6395 0 : let dist = randFloat(0.5, 1.5) * distance;
6396 0 : cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, -1);
6397 : };
6398 :
6399 1 : UnitAI.prototype.SetFacePointAfterMove = function(val)
6400 : {
6401 28 : var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
6402 28 : if (cmpMotion)
6403 28 : cmpMotion.SetFacePointAfterMove(val);
6404 : };
6405 :
6406 1 : UnitAI.prototype.GetFacePointAfterMove = function()
6407 : {
6408 14 : let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
6409 14 : return cmpUnitMotion && cmpUnitMotion.GetFacePointAfterMove();
6410 : };
6411 :
6412 1 : UnitAI.prototype.AttackEntitiesByPreference = function(ents)
6413 : {
6414 3 : if (!ents.length)
6415 1 : return false;
6416 :
6417 2 : let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
6418 2 : if (!cmpAttack)
6419 0 : return false;
6420 :
6421 2 : let attackfilter = function(e) {
6422 2 : if (!cmpAttack.CanAttack(e))
6423 0 : return false;
6424 :
6425 2 : let cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
6426 2 : if (cmpOwnership && cmpOwnership.GetOwner() > 0)
6427 0 : return true;
6428 :
6429 2 : let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
6430 2 : return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
6431 : };
6432 :
6433 2 : let entsByPreferences = {};
6434 2 : let preferences = [];
6435 2 : let entsWithoutPref = [];
6436 2 : for (let ent of ents)
6437 : {
6438 2 : if (!attackfilter(ent))
6439 1 : continue;
6440 1 : let pref = cmpAttack.GetPreference(ent);
6441 1 : if (pref === null || pref === undefined)
6442 0 : entsWithoutPref.push(ent);
6443 1 : else if (!entsByPreferences[pref])
6444 : {
6445 1 : preferences.push(pref);
6446 1 : entsByPreferences[pref] = [ent];
6447 : }
6448 : else
6449 0 : entsByPreferences[pref].push(ent);
6450 : }
6451 :
6452 2 : if (preferences.length)
6453 : {
6454 1 : preferences.sort((a, b) => a - b);
6455 1 : for (let pref of preferences)
6456 1 : if (this.RespondToTargetedEntities(entsByPreferences[pref]))
6457 1 : return true;
6458 : }
6459 :
6460 1 : return this.RespondToTargetedEntities(entsWithoutPref);
6461 : };
6462 :
6463 : /**
6464 : * Call UnitAI.funcname(args) on all formation members.
6465 : * @param resetFinishedEntities - If true, call ResetFinishedEntities first.
6466 : * If the controller wants to wait on its members to finish their order,
6467 : * this needs to be reset before sending new orders (in case they instafail)
6468 : * so it makes sense to do it here.
6469 : * Only set this to false if you're sure it's safe.
6470 : */
6471 1 : UnitAI.prototype.CallMemberFunction = function(funcname, args, resetFinishedEntities = true)
6472 : {
6473 16 : const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
6474 16 : if (!cmpFormation)
6475 0 : return false;
6476 :
6477 16 : if (resetFinishedEntities)
6478 16 : cmpFormation.ResetFinishedEntities();
6479 :
6480 16 : let result = false;
6481 16 : cmpFormation.GetMembers().forEach(ent => {
6482 46 : const cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
6483 46 : if (cmpUnitAI[funcname].apply(cmpUnitAI, args))
6484 0 : result = true;
6485 : });
6486 16 : return result;
6487 : };
6488 :
6489 : /**
6490 : * Call obj.funcname(args) on UnitAI components owned by player in given range.
6491 : */
6492 1 : UnitAI.prototype.CallPlayerOwnedEntitiesFunctionInRange = function(funcname, args, range)
6493 : {
6494 0 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
6495 0 : if (!cmpOwnership)
6496 0 : return;
6497 0 : let owner = cmpOwnership.GetOwner();
6498 0 : if (owner == INVALID_PLAYER)
6499 0 : return;
6500 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
6501 0 : let nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, [owner], IID_UnitAI, true);
6502 0 : for (let i = 0; i < nearby.length; ++i)
6503 : {
6504 0 : let cmpUnitAI = Engine.QueryInterface(nearby[i], IID_UnitAI);
6505 0 : cmpUnitAI[funcname].apply(cmpUnitAI, args);
6506 : }
6507 : };
6508 :
6509 : /**
6510 : * Call obj.functname(args) on UnitAI components of all formation members,
6511 : * and return true if all calls return true.
6512 : */
6513 1 : UnitAI.prototype.TestAllMemberFunction = function(funcname, args)
6514 : {
6515 0 : let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
6516 0 : return cmpFormation && cmpFormation.GetMembers().every(ent => {
6517 0 : let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
6518 0 : return cmpUnitAI[funcname].apply(cmpUnitAI, args);
6519 : });
6520 : };
6521 :
6522 1 : UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
6523 :
6524 1 : Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
|