Ticket #3444: UnitAI_my_changes.js

File UnitAI_my_changes.js, 175.5 KB (added by erraunt, 8 years ago)

the whole file for simpler comparison.

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