LCOV - code coverage report
Current view: top level - simulation/components - UnitAI.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 740 2863 25.8 %
Date: 2023-04-02 12:52:40 Functions: 109 482 22.6 %

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

Generated by: LCOV version 1.14