Ticket #2579: unitai_proposal.diff

File unitai_proposal.diff, 232.7 KB (added by sanderd17, 10 years ago)
  • binaries/data/mods/public/simulation/components/%UnitAI.js

     
    124124};
    125125
    126126// See ../helpers/FSM.js for some documentation of this FSM specification syntax
    127 var UnitFsmSpec = {
     127UnitAI.prototype.UnitFsmSpec = {
    128128
    129129    // Default event handlers:
    130130
     
    552552        this.FinishOrder();
    553553    },
    554554
    555     "Order.Gather": function(msg) {
    556         // If the target is still alive, we need to kill it first
    557         if (this.MustKillGatherTarget(this.order.data.target))
    558         {
    559             // Make sure we can attack the target, else we'll get very stuck
    560             if (!this.GetBestAttackAgainst(this.order.data.target))
    561             {
    562                 // Oops, we can't attack at all - give up
    563                 // TODO: should do something so the player knows why this failed
    564                 this.FinishOrder();
    565                 return;
    566             }
    567             // The target was visible when this order was issued,
    568             // but could now be invisible again.
    569             if (!this.CheckTargetVisible(this.order.data.target))
    570             {
    571                 if (this.order.data.secondTry === undefined)
    572                 {
    573                     this.order.data.secondTry = true;
    574                     this.PushOrderFront("Walk", this.order.data.lastPos);
    575                 }
    576                 else
    577                 {
    578                     // We couldn't move there, or the target moved away
    579                     this.FinishOrder();
    580                 }
    581                 return;
    582             }
    583 
    584             this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true });
    585             return;
    586         }
    587 
    588         // Try to move within range
    589         if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
    590         {
    591             // We've started walking to the given point
    592             this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
    593         }
    594         else
    595         {
    596             // We are already at the target, or can't move at all,
    597             // so try gathering it from here.
    598             // TODO: need better handling of the can't-reach-target case
    599             this.StopMoving();
    600             this.SetNextStateAlwaysEntering("INDIVIDUAL.GATHER.GATHERING");
    601         }
    602     },
    603 
    604     "Order.GatherNearPosition": function(msg) {
    605         // Move the unit to the position to gather from.
    606         this.MoveToPoint(this.order.data.x, this.order.data.z);
    607         this.SetNextState("INDIVIDUAL.GATHER.WALKING");
    608     },
    609 
    610     "Order.ReturnResource": function(msg) {
    611         // Check if the dropsite is already in range
    612         if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
    613         {
    614             var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
    615             if (cmpResourceDropsite)
    616             {
    617                 // Dump any resources we can
    618                 var dropsiteTypes = cmpResourceDropsite.GetTypes();
    619 
    620                 Engine.QueryInterface(this.entity, IID_ResourceGatherer).CommitResources(dropsiteTypes);
    621 
    622                 // Our next order should always be a Gather,
    623                 // so just switch back to that order
    624                 this.FinishOrder();
    625                 return;
    626             }
    627         }
    628         // Try to move to the dropsite
    629         if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
    630         {
    631             // We've started walking to the target
    632             this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
    633             return;
    634         }
    635         // Oops, we can't reach the dropsite.
    636         // Maybe we should try to pick another dropsite, to find an
    637         // accessible one?
    638         // For now, just give up.
    639         this.StopMoving();
    640         this.FinishOrder();
    641         return;
    642     },
    643 
    644555    "Order.Trade": function(msg) {
    645556        // We must check if this trader has both markets in case it was a back-to-work order
    646557        var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
     
    871782            this.SetNextState("GARRISON.GARRISONING");
    872783        },
    873784
    874         "Order.Gather": function(msg) {
    875             if (this.MustKillGatherTarget(msg.data.target))
    876             {
    877                 // The target was visible when this order was given,
    878                 // but could now be invisible.
    879                 if (!this.CheckTargetVisible(msg.data.target))
    880                 {
    881                     if (msg.data.secondTry === undefined)
    882                     {
    883                         msg.data.secondTry = true;
    884                         this.PushOrderFront("Walk", msg.data.lastPos);
    885                     }
    886                     else
    887                     {
    888                         // We couldn't move there, or the target moved away
    889                         this.FinishOrder();
    890                     }
    891                     return;
    892                 }
    893 
    894                 this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true });
    895                 return;
    896             }
    897 
    898             // TODO: on what should we base this range?
    899             // Check if we are already in range, otherwise walk there
    900             if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
    901             {
    902                 if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
    903                     // The target isn't gatherable or not visible any more.
    904                     this.FinishOrder();
    905                 // TODO: Should we issue a gather-near-position order
    906                 // if the target isn't gatherable/doesn't exist anymore?
    907                 else
    908                     // Out of range; move there in formation
    909                     this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
    910                 return;
    911             }
    912 
    913             this.CallMemberFunction("Gather", [msg.data.target, false]);
    914 
    915             this.SetNextStateAlwaysEntering("MEMBER");
    916         },
    917 
    918         "Order.GatherNearPosition": function(msg) {
    919             // TODO: on what should we base this range?
    920             // Check if we are already in range, otherwise walk there
    921             if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20))
    922             {
    923                 // Out of range; move there in formation
    924                 this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 });
    925                 return;
    926             }
    927 
    928             this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
    929 
    930             this.SetNextStateAlwaysEntering("MEMBER");
    931         },
    932 
    933785        "Order.Heal": function(msg) {
    934786            // TODO: on what should we base this range?
    935787            // Check if we are already in range, otherwise walk there
     
    968820            this.SetNextStateAlwaysEntering("MEMBER");
    969821        },
    970822
    971         "Order.ReturnResource": function(msg) {
    972             // TODO: on what should we base this range?
    973             // Check if we are already in range, otherwise walk there
    974             if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
    975             {
    976                 if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
    977                     // The target was destroyed
    978                     this.FinishOrder();
    979                 else
    980                     // Out of range; move there in formation
    981                     this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
    982                 return;
    983             }
    984 
    985             this.CallMemberFunction("ReturnResource", [msg.data.target, false]);
    986 
    987             this.SetNextStateAlwaysEntering("MEMBER");
    988         },
    989 
    990823        "Order.Pack": function(msg) {
    991824            this.CallMemberFunction("Pack", [false]);
    992825
     
    19691802            },
    19701803        },
    19711804
    1972         "GATHER": {
    1973             "APPROACHING": {
    1974                 "enter": function() {
    1975                     this.SelectAnimation("move");
    1976 
    1977                     this.gatheringTarget = this.order.data.target;  // temporary, deleted in "leave".
    1978 
    1979                     // check that we can gather from the resource we're supposed to gather from.
    1980                     var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    1981                     var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
    1982                     if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))
    1983                     {
    1984                         // Save the current order's data in case we need it later
    1985                         var oldType = this.order.data.type;
    1986                         var oldTarget = this.order.data.target;
    1987                         var oldTemplate = this.order.data.template;
    1988 
    1989                         // Try the next queued order if there is any
    1990                         if (this.FinishOrder())
    1991                             return true;
    1992                        
    1993                         // Try to find another nearby target of the same specific type
    1994                         // Also don't switch to a different type of huntable animal
    1995                         var nearby = this.FindNearbyResource(function (ent, type, template) {
    1996                             return (
    1997                                 ent != oldTarget
    1998                                  && ((type.generic == "treasure" && oldType.generic == "treasure")
    1999                                  || (type.specific == oldType.specific
    2000                                  && (type.specific != "meat" || oldTemplate == template)))
    2001                             );
    2002                         });
    2003                         if (nearby)
    2004                         {
    2005                             this.PerformGather(nearby, false, false);
    2006                             return true;
    2007                         }
    2008                         else
    2009                         {
    2010                             // It's probably better in this case, to avoid units getting stuck around a dropsite
    2011                             // in a "Target is far away, full, nearby are no good resources, return to dropsite" loop
    2012                             // to order it to GatherNear the resource position.
    2013                             var cmpPosition = Engine.QueryInterface(this.gatheringTarget, IID_Position);
    2014                             if (cmpPosition)
    2015                             {
    2016                                 var pos = cmpPosition.GetPosition();
    2017                                 this.GatherNearPosition(pos.x, pos.z, oldType, oldTemplate);
    2018                                 return true;
    2019                             } else {
    2020                                 // we're kind of stuck here. Return resource.
    2021                                 var nearby = this.FindNearestDropsite(oldType.generic);
    2022                                 if (nearby)
    2023                                 {
    2024                                     this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
    2025                                     return true;
    2026                                 }
    2027                             }
    2028                         }
    2029                         return true;
    2030                     }
    2031                     return false;
    2032                 },
    2033 
    2034                 "MoveCompleted": function(msg) {
    2035                     if (msg.data.error)
    2036                     {
    2037                         // We failed to reach the target
    2038 
    2039                         // remove us from the list of entities gathering from Resource.
    2040                         var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    2041                         var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
    2042                         if (cmpSupply && cmpOwnership)
    2043                             cmpSupply.RemoveGatherer(this.entity, cmpOwnership.GetOwner());
    2044                         else if (cmpSupply)
    2045                             cmpSupply.RemoveGatherer(this.entity);
    2046 
    2047                         // Save the current order's data in case we need it later
    2048                         var oldType = this.order.data.type;
    2049                         var oldTarget = this.order.data.target;
    2050                         var oldTemplate = this.order.data.template;
    2051 
    2052                         // Try the next queued order if there is any
    2053                         if (this.FinishOrder())
    2054                             return;
    2055 
    2056                         // Try to find another nearby target of the same specific type
    2057                         // Also don't switch to a different type of huntable animal
    2058                         var nearby = this.FindNearbyResource(function (ent, type, template) {
    2059                             return (
    2060                                 ent != oldTarget
    2061                                 && ((type.generic == "treasure" && oldType.generic == "treasure")
    2062                                 || (type.specific == oldType.specific
    2063                                 && (type.specific != "meat" || oldTemplate == template)))
    2064                             );
    2065                         });
    2066                         if (nearby)
    2067                         {
    2068                             this.PerformGather(nearby, false, false);
    2069                             return;
    2070                         }
    2071 
    2072                         // Couldn't find anything else. Just try this one again,
    2073                         // maybe we'll succeed next time
    2074                         this.PerformGather(oldTarget, false, false);
    2075                         return;
    2076                     }
    2077 
    2078                     // We reached the target - start gathering from it now
    2079                     this.SetNextState("GATHERING");
    2080                 },
    2081                
    2082                 "leave": function() {
    2083                     // don't use ownership because this is called after a conversion/resignation
    2084                     // and the ownership would be invalid then.
    2085                     var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
    2086                     if (cmpSupply)
    2087                         cmpSupply.RemoveGatherer(this.entity);
    2088                     delete this.gatheringTarget;
    2089                 },
    2090             },
    2091            
    2092             // Walking to a good place to gather resources near, used by GatherNearPosition
    2093             "WALKING": {
    2094                 "enter": function() {
    2095                     this.SelectAnimation("move");
    2096                 },
    2097 
    2098                 "MoveCompleted": function(msg) {
    2099                     var resourceType = this.order.data.type;
    2100                     var resourceTemplate = this.order.data.template;
    2101 
    2102                     // Try to find another nearby target of the same specific type
    2103                     // Also don't switch to a different type of huntable animal
    2104                     var nearby = this.FindNearbyResource(function (ent, type, template) {
    2105                         return (
    2106                             (type.generic == "treasure" && resourceType.generic == "treasure")
    2107                             || (type.specific == resourceType.specific
    2108                             && (type.specific != "meat" || resourceTemplate == template))
    2109                         );
    2110                     });
    2111 
    2112                     // If there is a nearby resource start gathering
    2113                     if (nearby)
    2114                     {
    2115                         this.PerformGather(nearby, false, false);
    2116                         return;
    2117                     }
    2118 
    2119                     // Couldn't find nearby resources, so give up
    2120                     if (this.FinishOrder())
    2121                         return;
    2122 
    2123                     // Nothing better to do: go back to dropsite
    2124                     var nearby = this.FindNearestDropsite(resourceType.generic);
    2125                     if (nearby)
    2126                     {
    2127                         this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
    2128                         return;
    2129                     }
    2130 
    2131                     // No dropsites, just give up
    2132                 },
    2133             },
    2134 
    2135             "GATHERING": {
    2136                 "enter": function() {
    2137                     this.gatheringTarget = this.order.data.target;  // deleted in "leave".
    2138 
    2139                     // Check if the resource is full.
    2140                     if (this.gatheringTarget)
    2141                     {
    2142                         // Check that we can gather from the resource we're supposed to gather from.
    2143                         // Will only be added if we're not already in.
    2144                         var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    2145                         var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
    2146                         if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))
    2147                         {
    2148                             this.gatheringTarget = INVALID_ENTITY;
    2149                             this.StartTimer(0);
    2150                             return false;
    2151                         }
    2152                     }
    2153 
    2154                     // If this order was forced, the player probably gave it, but now we've reached the target
    2155                     //  switch to an unforced order (can be interrupted by attacks)
    2156                     this.order.data.force = false;
    2157                     this.order.data.autoharvest = true;
    2158 
    2159                     // Calculate timing based on gather rates
    2160                     // This allows the gather rate to control how often we gather, instead of how much.
    2161                     var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
    2162                     var rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget);
    2163 
    2164                     if (!rate)
    2165                     {
    2166                         // Try to find another target if the current one stopped existing
    2167                         if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity))
    2168                         {
    2169                             // Let the Timer logic handle this
    2170                             this.StartTimer(0);
    2171                             return false;
    2172                         }
    2173 
    2174                         // No rate, give up on gathering
    2175                         this.FinishOrder();
    2176                         return true;
    2177                     }
    2178 
    2179                     // Scale timing interval based on rate, and start timer
    2180                     // The offset should be at least as long as the repeat time so we use the same value for both.
    2181                     var offset = 1000/rate;
    2182                     var repeat = offset;
    2183                     this.StartTimer(offset, repeat);
    2184 
    2185                     // We want to start the gather animation as soon as possible,
    2186                     // but only if we're actually at the target and it's still alive
    2187                     // (else it'll look like we're chopping empty air).
    2188                     // (If it's not alive, the Timer handler will deal with sending us
    2189                     // off to a different target.)
    2190                     if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
    2191                     {
    2192                         var typename = "gather_" + this.order.data.type.specific;
    2193                         this.SelectAnimation(typename, false, 1.0, typename);
    2194                     }
    2195                     return false;
    2196                 },
    2197 
    2198                 "leave": function() {
    2199                     this.StopTimer();
    2200 
    2201                     // don't use ownership because this is called after a conversion/resignation
    2202                     // and the ownership would be invalid then.
    2203                     var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
    2204                     if (cmpSupply)
    2205                         cmpSupply.RemoveGatherer(this.entity);
    2206                     delete this.gatheringTarget;
    2207 
    2208                     // Show the carried resource, if we've gathered anything.
    2209                     this.SetGathererAnimationOverride();
    2210                 },
    2211 
    2212                 "Timer": function(msg) {
    2213                     var resourceTemplate = this.order.data.template;
    2214                     var resourceType = this.order.data.type;
    2215 
    2216                     var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    2217                     if (!cmpOwnership)
    2218                         return;
    2219 
    2220                     var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
    2221                     if (cmpSupply && cmpSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity))
    2222                     {
    2223                         // Check we can still reach and gather from the target
    2224                         if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer) && this.CanGather(this.gatheringTarget))
    2225                         {
    2226                             // Gather the resources:
    2227 
    2228                             var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
    2229 
    2230                             // Try to gather treasure
    2231                             if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget))
    2232                                 return;
    2233 
    2234                             // If we've already got some resources but they're the wrong type,
    2235                             // drop them first to ensure we're only ever carrying one type
    2236                             if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic))
    2237                                 cmpResourceGatherer.DropResources();
    2238 
    2239                             // Collect from the target
    2240                             var status = cmpResourceGatherer.PerformGather(this.gatheringTarget);
    2241 
    2242                             // If we've collected as many resources as possible,
    2243                             // return to the nearest dropsite
    2244                             if (status.filled)
    2245                             {
    2246                                 var nearby = this.FindNearestDropsite(resourceType.generic);
    2247                                 if (nearby)
    2248                                 {
    2249                                     // (Keep this Gather order on the stack so we'll
    2250                                     // continue gathering after returning)
    2251                                     this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
    2252                                     return;
    2253                                 }
    2254 
    2255                                 // Oh no, couldn't find any drop sites. Give up on gathering.
    2256                                 this.FinishOrder();
    2257                                 return;
    2258                             }
    2259 
    2260                             // We can gather more from this target, do so in the next timer
    2261                             if (!status.exhausted)
    2262                                 return;
    2263                         }
    2264                         else
    2265                         {
    2266                             // Try to follow the target
    2267                             if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer))
    2268                             {
    2269                                 this.SetNextState("APPROACHING");
    2270                                 return;
    2271                             }
    2272 
    2273                             // Can't reach the target, or it doesn't exist any more
    2274 
    2275                             // We want to carry on gathering resources in the same area as
    2276                             // the old one. So try to get close to the old resource's
    2277                             // last known position
    2278 
    2279                             var maxRange = 8; // get close but not too close
    2280                             if (this.order.data.lastPos &&
    2281                                 this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z,
    2282                                     0, maxRange))
    2283                             {
    2284                                 this.SetNextState("APPROACHING");
    2285                                 return;
    2286                             }
    2287                         }
    2288                     }
    2289 
    2290                     // We're already in range, can't get anywhere near it or the target is exhausted.
    2291 
    2292                     var herdPos = this.order.data.initPos;
    2293 
    2294                     // Give up on this order and try our next queued order
    2295                     if (this.FinishOrder())
    2296                         return;
    2297 
    2298                     // No remaining orders - pick a useful default behaviour
    2299 
    2300                     // Try to find a new resource of the same specific type near our current position:
    2301                     // Also don't switch to a different type of huntable animal
    2302                     var nearby = this.FindNearbyResource(function (ent, type, template) {
    2303                         return (
    2304                             (type.generic == "treasure" && resourceType.generic == "treasure")
    2305                             || (type.specific == resourceType.specific
    2306                             && (type.specific != "meat" || resourceTemplate == template))
    2307                         );
    2308                     });
    2309                     if (nearby)
    2310                     {
    2311                         this.PerformGather(nearby, false, false);
    2312                         return;
    2313                     }
    2314 
    2315                     // If hunting, try to go to the initial herd position to see if we are more lucky
    2316                     if (herdPos)
    2317                         this.GatherNearPosition(herdPos.x, herdPos.z, resourceType, resourceTemplate);
    2318 
    2319                     // Nothing else to gather - if we're carrying anything then we should
    2320                     // drop it off, and if not then we might as well head to the dropsite
    2321                     // anyway because that's a nice enough place to congregate and idle
    2322 
    2323                     var nearby = this.FindNearestDropsite(resourceType.generic);
    2324                     if (nearby)
    2325                     {
    2326                         this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
    2327                         return;
    2328                     }
    2329                    
    2330                     // No dropsites - just give up
    2331                 },
    2332             },
    2333         },
    2334 
    23351805        "HEAL": {
    23361806            "Attacked": function(msg) {
    23371807                // If we stand ground we will rather die than flee
     
    24661936            }, 
    24671937        },
    24681938
    2469         // Returning to dropsite
    2470         "RETURNRESOURCE": {
    2471             "APPROACHING": {
    2472                 "enter": function () {
    2473                     this.SelectAnimation("move");
    2474                 },
    2475 
    2476                 "MoveCompleted": function() {
    2477                     // Switch back to idle animation to guarantee we won't
    2478                     // get stuck with the carry animation after stopping moving
    2479                     this.SelectAnimation("idle");
    2480 
    2481                     // Check the dropsite is in range and we can return our resource there
    2482                     // (we didn't get stopped before reaching it)
    2483                     if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
    2484                     {
    2485                         var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
    2486                         if (cmpResourceDropsite)
    2487                         {
    2488                             // Dump any resources we can
    2489                             var dropsiteTypes = cmpResourceDropsite.GetTypes();
    2490 
    2491                             var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
    2492                             cmpResourceGatherer.CommitResources(dropsiteTypes);
    2493 
    2494                             // Stop showing the carried resource animation.
    2495                             this.SetGathererAnimationOverride();
    2496 
    2497                             // Our next order should always be a Gather,
    2498                             // so just switch back to that order
    2499                             this.FinishOrder();
    2500                             return;
    2501                         }
    2502                     }
    2503 
    2504                     // The dropsite was destroyed, or we couldn't reach it, or ownership changed
    2505                     // Look for a new one.
    2506 
    2507                     var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
    2508                     var genericType = cmpResourceGatherer.GetMainCarryingType();
    2509                     var nearby = this.FindNearestDropsite(genericType);
    2510                     if (nearby)
    2511                     {
    2512                         this.FinishOrder();
    2513                         this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
    2514                         return;
    2515                     }
    2516 
    2517                     // Oh no, couldn't find any drop sites. Give up on returning.
    2518                     this.FinishOrder();
    2519                 },
    2520             },
    2521         },
    2522 
    25231939        "TRADE": {
    25241940            "Attacked": function(msg) {
    25251941                // Ignore attack
     
    30912507    },
    30922508};
    30932509
    3094 var UnitFsm = new FSM(UnitFsmSpec);
    3095 
    30962510UnitAI.prototype.Init = function()
    30972511{
    30982512    this.orderQueue = []; // current order is at the front of the list
     
    32252639UnitAI.prototype.OnCreate = function()
    32262640{
    32272641    if (this.IsAnimal())
    3228         UnitFsm.Init(this, "ANIMAL.FEEDING");
     2642        this.UnitFsm.Init(this, "ANIMAL.FEEDING");
    32292643    else if (this.IsFormationController())
    3230         UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
     2644        this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
    32312645    else
    3232         UnitFsm.Init(this, "INDIVIDUAL.IDLE");
     2646        this.UnitFsm.Init(this, "INDIVIDUAL.IDLE");
    32332647};
    32342648
    32352649UnitAI.prototype.OnDiplomacyChanged = function(msg)
     
    32492663        // Switch to a virgin state to let states execute their leave handlers.
    32502664        var index = this.GetCurrentState().indexOf(".");
    32512665        if (index != -1)
    3252             UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0,index));
     2666            this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0,index));
    32532667
    32542668        this.SetStance(this.template.DefaultStance);
    32552669        if(!this.isGarrisoned)
     
    32602674UnitAI.prototype.OnDestroy = function()
    32612675{
    32622676    // Switch to an empty state to let states execute their leave handlers.
    3263     UnitFsm.SwitchToNextState(this, "");
     2677    this.UnitFsm.SwitchToNextState(this, "");
    32642678
    32652679    // Clean up range queries
    32662680    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     
    33022716        if (this.orderQueue[i].type == "PickupUnit" && this.orderQueue[i].data.target == msg.entity)
    33032717        {
    33042718            if (i == 0)
    3305                 UnitFsm.ProcessMessage(this, {"type": "PickupCanceled", "data": msg});
     2719                this.UnitFsm.ProcessMessage(this, {"type": "PickupCanceled", "data": msg});
    33062720            else
    33072721                this.orderQueue.splice(i, 1);
    33082722            break;
     
    34012815
    34022816UnitAI.prototype.SetNextState = function(state)
    34032817{
    3404     UnitFsm.SetNextState(this, state);
     2818    this.UnitFsm.SetNextState(this, state);
    34052819};
    34062820
    34072821// This will make sure that the state is always entered even if this means leaving it and reentering it
     
    34082822// This is so that a state can be reinitialized with new order data without having to switch to an intermediate state
    34092823UnitAI.prototype.SetNextStateAlwaysEntering = function(state)
    34102824{
    3411     UnitFsm.SetNextStateAlwaysEntering(this, state);
     2825    this.UnitFsm.SetNextStateAlwaysEntering(this, state);
    34122826};
    34132827
    34142828UnitAI.prototype.DeferMessage = function(msg)
    34152829{
    3416     UnitFsm.DeferMessage(this, msg);
     2830    this.UnitFsm.DeferMessage(this, msg);
    34172831};
    34182832
    34192833UnitAI.prototype.GetCurrentState = function()
    34202834{
    3421     return UnitFsm.GetCurrentState(this);
     2835    return this.UnitFsm.GetCurrentState(this);
    34222836};
    34232837
    34242838UnitAI.prototype.FsmStateNameChanged = function(state)
     
    34472861
    34482862    if (this.orderQueue.length)
    34492863    {
    3450         var ret = UnitFsm.ProcessMessage(this,
     2864        var ret = this.UnitFsm.ProcessMessage(this,
    34512865            {"type": "Order."+this.order.type, "data": this.order.data}
    34522866        );
    34532867
     
    35012915    if (this.orderQueue.length == 1)
    35022916    {
    35032917        this.order = order;
    3504         var ret = UnitFsm.ProcessMessage(this,
     2918        var ret = this.UnitFsm.ProcessMessage(this,
    35052919            {"type": "Order."+this.order.type, "data": this.order.data}
    35062920        );
    35072921
     
    35372951    {
    35382952        this.orderQueue.unshift(order);
    35392953        this.order = order;
    3540         var ret = UnitFsm.ProcessMessage(this,
     2954        var ret = this.UnitFsm.ProcessMessage(this,
    35412955            {"type": "Order."+this.order.type, "data": this.order.data}
    35422956        );
    35432957
     
    37383152        this.timer = undefined;
    37393153    }
    37403154
    3741     UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
     3155    this.UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
    37423156};
    37433157
    37443158/**
     
    37793193{
    37803194    if (msg.starting && !msg.error)
    37813195    {
    3782         UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
     3196        this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
    37833197    }
    37843198    else if (!msg.starting || msg.error)
    37853199    {
    3786         UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
     3200        this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
    37873201    }
    37883202};
    37893203
     
    37923206    // TODO: This is a bit inefficient since every unit listens to every
    37933207    // construction message - ideally we could scope it to only the one we're building
    37943208
    3795     UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg});
     3209    this.UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg});
    37963210};
    37973211
    37983212UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
     
    38113225
    38123226UnitAI.prototype.OnAttacked = function(msg)
    38133227{
    3814     UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
     3228    this.UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
    38153229};
    38163230
    38173231UnitAI.prototype.OnGuardedAttacked = function(msg)
    38183232{
    3819     UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data});
     3233    this.UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data});
    38203234};
    38213235
    38223236UnitAI.prototype.OnHealthChanged = function(msg)
    38233237{
    3824     UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
     3238    this.UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
    38253239};
    38263240
    38273241UnitAI.prototype.OnRangeUpdate = function(msg)
    38283242{
    38293243    if (msg.tag == this.losRangeQuery)
    3830         UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
     3244        this.UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
    38313245    else if (msg.tag == this.losHealRangeQuery)
    3832         UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg});
     3246        this.UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg});
    38333247};
    38343248
    38353249UnitAI.prototype.OnPackFinished = function(msg)
    38363250{
    3837     UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed});
     3251    this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed});
    38383252};
    38393253
    38403254//// Helper functions to be called by the FSM ////
     
    38903304};
    38913305
    38923306/**
    3893  * Returns the entity ID of the nearest resource supply where the given
    3894  * filter returns true, or undefined if none can be found.
    3895  * TODO: extend this to exclude resources that already have lots of
    3896  * gatherers.
    3897  */
    3898 UnitAI.prototype.FindNearbyResource = function(filter)
    3899 {
    3900     var range = 64; // TODO: what's a sensible number?
    3901 
    3902     var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
    3903     // We accept resources owned by Gaia or any player
    3904     var players = [0];
    3905     for (var i = 1; i < playerMan.GetNumPlayers(); ++i)
    3906         players.push(i);
    3907 
    3908     var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
    3909     var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    3910     var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    3911     var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_ResourceSupply);
    3912     for each (var ent in nearby)
    3913     {
    3914         if (!this.CanGather(ent))
    3915             continue;
    3916         var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
    3917         var type = cmpResourceSupply.GetType();
    3918         var amount = cmpResourceSupply.GetCurrentAmount();
    3919         var template = cmpTemplateManager.GetCurrentTemplateName(ent);
    3920 
    3921         // Remove "resource|" prefix from template names, if present.
    3922         if (template.indexOf("resource|") != -1)
    3923             template = template.slice(9);
    3924 
    3925         if (amount > 0 && cmpResourceSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity) && filter(ent, type, template))
    3926             return ent;
    3927     }
    3928 
    3929     return undefined;
    3930 };
    3931 
    3932 /**
    3933  * Returns the entity ID of the nearest resource dropsite that accepts
    3934  * the given type, or undefined if none can be found.
    3935  */
    3936 UnitAI.prototype.FindNearestDropsite = function(genericType)
    3937 {
    3938     // Find dropsites owned by this unit's player
    3939     var players = [];
    3940     var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    3941     if (cmpOwnership)
    3942         players = [cmpOwnership.GetOwner()];
    3943 
    3944     // Ships are unable to reach land dropsites and shouldn't attempt to do so.
    3945     var excludeLand = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship");
    3946 
    3947     var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    3948     var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite);
    3949     if (excludeLand)
    3950     {
    3951         nearby = nearby.filter( function(e) {
    3952             return Engine.QueryInterface(e, IID_Identity).HasClass("Naval");
    3953         });
    3954     }
    3955 
    3956     for each (var ent in nearby)
    3957     {
    3958         var cmpDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
    3959         if (!cmpDropsite.AcceptsType(genericType))
    3960             continue;
    3961 
    3962         return ent;
    3963     }
    3964 
    3965     return undefined;
    3966 };
    3967 
    3968 /**
    39693307 * Returns the entity ID of the nearest building that needs to be constructed,
    39703308 * or undefined if none can be found close enough.
    39713309 */
     
    46293967
    46303968    // If we were removed from a formation, let the FSM switch back to INDIVIDUAL
    46313969    if (ent == INVALID_ENTITY)
    4632         UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
     3970        this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
    46333971};
    46343972
    46353973UnitAI.prototype.GetFormationController = function()
     
    48064144        return;
    48074145
    48084146    if (this.order.type == "Guard")
    4809         UnitFsm.ProcessMessage(this, {"type": "RemoveGuard"});
     4147        this.UnitFsm.ProcessMessage(this, {"type": "RemoveGuard"});
    48104148    else
    48114149        for (var i = 1; i < this.orderQueue.length; ++i)
    48124150            if (this.orderQueue[i].type == "Guard")
     
    49464284};
    49474285
    49484286/**
    4949  * Adds gather order to the queue, forced by the player
    4950  * until the target is reached
    4951  */
    4952 UnitAI.prototype.Gather = function(target, queued)
    4953 {
    4954     this.PerformGather(target, queued, true);
    4955 };
    4956 
    4957 /**
    4958  * Internal function to abstract the force parameter.
    4959  */
    4960 UnitAI.prototype.PerformGather = function(target, queued, force)
    4961 {
    4962     if (!this.CanGather(target))
    4963     {
    4964         this.WalkToTarget(target, queued);
    4965         return;
    4966     }
    4967 
    4968     // Save the resource type now, so if the resource gets destroyed
    4969     // before we process the order then we still know what resource
    4970     // type to look for more of
    4971     var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
    4972     var type = cmpResourceSupply.GetType();
    4973 
    4974     // Also save the target entity's template, so that if it's an animal,
    4975     // we won't go from hunting slow safe animals to dangerous fast ones
    4976     var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
    4977     var template = cmpTemplateManager.GetCurrentTemplateName(target);
    4978 
    4979     // Remove "resource|" prefix from template name, if present.
    4980     if (template.indexOf("resource|") != -1)
    4981         template = template.slice(9);
    4982 
    4983     // Remember the position of our target, if any, in case it disappears
    4984     // later and we want to head to its last known position
    4985     var lastPos = undefined;
    4986     var cmpPosition = Engine.QueryInterface(target, IID_Position);
    4987     if (cmpPosition && cmpPosition.IsInWorld())
    4988         lastPos = cmpPosition.GetPosition();
    4989 
    4990     this.AddOrder("Gather", { "target": target, "type": type, "template": template, "lastPos": lastPos, "force": force }, queued);
    4991 };
    4992 
    4993 /**
    4994  * Adds gather-near-position order to the queue, not forced, so it can be
    4995  * interrupted by attacks.
    4996  */
    4997 UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued)
    4998 {
    4999     // Remove "resource|" prefix from template name, if present.
    5000     if (template.indexOf("resource|") != -1)
    5001         template = template.slice(9);
    5002 
    5003     this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued);
    5004 };
    5005 
    5006 /**
    50074287 * Adds heal order to the queue, forced by the player.
    50084288 */
    50094289UnitAI.prototype.Heal = function(target, queued)
     
    50184298};
    50194299
    50204300/**
    5021  * Adds return resource order to the queue, forced by the player.
    5022  */
    5023 UnitAI.prototype.ReturnResource = function(target, queued)
    5024 {
    5025     if (!this.CanReturnResource(target, true))
    5026     {
    5027         this.WalkToTarget(target, queued);
    5028         return;
    5029     }
    5030 
    5031     this.AddOrder("ReturnResource", { "target": target, "force": true }, queued);
    5032 };
    5033 
    5034 /**
    50354301 * Adds trade order to the queue. Either walk to the first market, or
    50364302 * start a new route. Not forced, so it can be interrupted by attacks.
    50374303 * The possible route may be given directly as a SetupTradeRoute argument
     
    55044770    return true;
    55054771};
    55064772
    5507 UnitAI.prototype.CanGather = function(target)
    5508 {
    5509     // The target must be a valid resource supply.
    5510     var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
    5511     if (!cmpResourceSupply)
    5512         return false;
    5513 
    5514     // Formation controllers should always respond to commands
    5515     // (then the individual units can make up their own minds)
    5516     if (this.IsFormationController())
    5517         return true;
    5518 
    5519     // Verify that we're able to respond to Gather commands
    5520     var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
    5521     if (!cmpResourceGatherer)
    5522         return false;
    5523 
    5524     // Verify that we can gather from this target
    5525     if (!cmpResourceGatherer.GetTargetGatherRate(target))
    5526         return false;
    5527 
    5528     // No need to verify ownership as we should be able to gather from
    5529     // a target regardless of ownership.
    5530     // No need to call "cmpResourceSupply.IsAvailable()" either because that
    5531     // would cause units to walk to full entities instead of choosing another one
    5532     // nearby to gather from, which is undesirable.
    5533     return true;
    5534 };
    5535 
    55364773UnitAI.prototype.CanHeal = function(target)
    55374774{
    55384775    // Formation controllers should always respond to commands
     
    55864823    return true;
    55874824};
    55884825
    5589 UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
    5590 {
    5591     // Formation controllers should always respond to commands
    5592     // (then the individual units can make up their own minds)
    5593     if (this.IsFormationController())
    5594         return true;
    5595 
    5596     // Verify that we're able to respond to ReturnResource commands
    5597     var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
    5598     if (!cmpResourceGatherer)
    5599         return false;
    5600 
    5601     // Verify that the target is a dropsite
    5602     var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
    5603     if (!cmpResourceDropsite)
    5604         return false;
    5605 
    5606     if (checkCarriedResource)
    5607     {
    5608         // Verify that we are carrying some resources,
    5609         // and can return our current resource to this target
    5610         var type = cmpResourceGatherer.GetMainCarryingType();
    5611         if (!type || !cmpResourceDropsite.AcceptsType(type))
    5612             return false;
    5613     }
    5614 
    5615     // Verify that the dropsite is owned by this entity's player
    5616     var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    5617     if (!cmpOwnership || !IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
    5618         return false;
    5619 
    5620     return true;
    5621 };
    5622 
    56234826UnitAI.prototype.CanTrade = function(target)
    56244827{
    56254828    // Formation controllers should always respond to commands
  • binaries/data/mods/public/simulation/components/ResourceGatherer.js

     
    341341};
    342342
    343343Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer);
     344
     345
     346// UNIT AI COMMANDS
     347
     348UnitAI.prototype.UnitFsmSpec["Order.Gather"] = function(msg)
     349{
     350    // If the target is still alive, we need to kill it first
     351    if (this.MustKillGatherTarget(this.order.data.target))
     352    {
     353        // Make sure we can attack the target, else we'll get very stuck
     354        if (!this.GetBestAttackAgainst(this.order.data.target))
     355        {
     356            // Oops, we can't attack at all - give up
     357            // TODO: should do something so the player knows why this failed
     358            this.FinishOrder();
     359            return;
     360        }
     361        // The target was visible when this order was issued,
     362        // but could now be invisible again.
     363        if (!this.CheckTargetVisible(this.order.data.target))
     364        {
     365            if (this.order.data.secondTry === undefined)
     366            {
     367                this.order.data.secondTry = true;
     368                this.PushOrderFront("Walk", this.order.data.lastPos);
     369            }
     370            else
     371            {
     372                // We couldn't move there, or the target moved away
     373                this.FinishOrder();
     374            }
     375            return;
     376        }
     377
     378        this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true });
     379        return;
     380    }
     381    // Try to move within range
     382    if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
     383    {
     384        // We've started walking to the given point
     385        this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
     386    }
     387    else
     388    {
     389        // We are already at the target, or can't move at all,
     390        // so try gathering it from here.
     391        // TODO: need better handling of the can't-reach-target case
     392        this.StopMoving();
     393        this.SetNextStateAlwaysEntering("INDIVIDUAL.GATHER.GATHERING");
     394    }
     395};
     396
     397UnitAI.prototype.UnitFsmSpec["Order.GatherNearPosition"] = function(msg)
     398{
     399    // Move the unit to the position to gather from.
     400    this.MoveToPoint(this.order.data.x, this.order.data.z);
     401    this.SetNextState("INDIVIDUAL.GATHER.WALKING");
     402};
     403
     404UnitAI.prototype.UnitFsmSpec["Order.ReturnResource"] = function(msg)
     405{
     406    // Check if the dropsite is already in range
     407    if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
     408    {
     409        var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
     410        if (cmpResourceDropsite)
     411        {
     412            // Dump any resources we can
     413            var dropsiteTypes = cmpResourceDropsite.GetTypes();
     414
     415            Engine.QueryInterface(this.entity, IID_ResourceGatherer).CommitResources(dropsiteTypes);
     416
     417            // Our next order should always be a Gather,
     418            // so just switch back to that order
     419            this.FinishOrder();
     420            return;
     421        }
     422    }
     423    // Try to move to the dropsite
     424    if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
     425    {
     426        // We've started walking to the target
     427        this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
     428        return;
     429    }
     430    // Oops, we can't reach the dropsite.
     431    // Maybe we should try to pick another dropsite, to find an
     432    // accessible one?
     433    // For now, just give up.
     434    this.StopMoving();
     435    this.FinishOrder();
     436    return;
     437};
     438
     439UnitAI.prototype.UnitFsmSpec.FORMATIONCONTROLLER["Order.Gather"] = function(msg)
     440{
     441    if (this.MustKillGatherTarget(msg.data.target))
     442    {
     443        // The target was visible when this order was given,
     444        // but could now be invisible.
     445        if (!this.CheckTargetVisible(msg.data.target))
     446        {
     447            if (msg.data.secondTry === undefined)
     448            {
     449                msg.data.secondTry = true;
     450                this.PushOrderFront("Walk", msg.data.lastPos);
     451            }
     452            else
     453            {
     454                // We couldn't move there, or the target moved away
     455                this.FinishOrder();
     456            }
     457            return;
     458        }
     459
     460        this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true });
     461        return;
     462    }
     463
     464    // TODO: on what should we base this range?
     465    // Check if we are already in range, otherwise walk there
     466    if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
     467    {
     468        if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
     469            // The target isn't gatherable or not visible any more.
     470            this.FinishOrder();
     471        // TODO: Should we issue a gather-near-position order
     472        // if the target isn't gatherable/doesn't exist anymore?
     473        else
     474            // Out of range; move there in formation
     475            this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
     476        return;
     477    }
     478
     479    this.CallMemberFunction("Gather", [msg.data.target, false]);
     480
     481    this.SetNextStateAlwaysEntering("MEMBER");
     482};
     483
     484UnitAI.prototype.UnitFsmSpec.FORMATIONCONTROLLER["Order.GatherNearPosition"] = function(msg)
     485{
     486    // TODO: on what should we base this range?
     487    // Check if we are already in range, otherwise walk there
     488    if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20))
     489    {
     490        // Out of range; move there in formation
     491        this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 });
     492        return;
     493    }
     494
     495    this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
     496
     497    this.SetNextStateAlwaysEntering("MEMBER");
     498};
     499
     500UnitAI.prototype.UnitFsmSpec.FORMATIONCONTROLLER["Order.ReturnResource"] = function(msg)
     501{
     502    // TODO: on what should we base this range?
     503    // Check if we are already in range, otherwise walk there
     504    if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
     505    {
     506        if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
     507            // The target was destroyed
     508            this.FinishOrder();
     509        else
     510            // Out of range; move there in formation
     511            this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
     512        return;
     513    }
     514
     515    this.CallMemberFunction("ReturnResource", [msg.data.target, false]);
     516
     517    this.SetNextStateAlwaysEntering("MEMBER");
     518};
     519
     520UnitAI.prototype.UnitFsmSpec.INDIVIDUAL.GATHER = {
     521    "APPROACHING": {
     522        "enter": function() {
     523            this.SelectAnimation("move");
     524
     525            this.gatheringTarget = this.order.data.target;  // temporary, deleted in "leave".
     526
     527            // check that we can gather from the resource we're supposed to gather from.
     528            var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     529            var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
     530            if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))
     531            {
     532                // Save the current order's data in case we need it later
     533                var oldType = this.order.data.type;
     534                var oldTarget = this.order.data.target;
     535                var oldTemplate = this.order.data.template;
     536
     537                // Try the next queued order if there is any
     538                if (this.FinishOrder())
     539                    return true;
     540               
     541                // Try to find another nearby target of the same specific type
     542                // Also don't switch to a different type of huntable animal
     543                var nearby = this.FindNearbyResource(function (ent, type, template) {
     544                    return (
     545                        ent != oldTarget
     546                         && ((type.generic == "treasure" && oldType.generic == "treasure")
     547                         || (type.specific == oldType.specific
     548                         && (type.specific != "meat" || oldTemplate == template)))
     549                    );
     550                });
     551                if (nearby)
     552                {
     553                    this.PerformGather(nearby, false, false);
     554                    return true;
     555                }
     556                else
     557                {
     558                    // It's probably better in this case, to avoid units getting stuck around a dropsite
     559                    // in a "Target is far away, full, nearby are no good resources, return to dropsite" loop
     560                    // to order it to GatherNear the resource position.
     561                    var cmpPosition = Engine.QueryInterface(this.gatheringTarget, IID_Position);
     562                    if (cmpPosition)
     563                    {
     564                        var pos = cmpPosition.GetPosition();
     565                        this.GatherNearPosition(pos.x, pos.z, oldType, oldTemplate);
     566                        return true;
     567                    } else {
     568                        // we're kind of stuck here. Return resource.
     569                        var nearby = this.FindNearestDropsite(oldType.generic);
     570                        if (nearby)
     571                        {
     572                            this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
     573                            return true;
     574                        }
     575                    }
     576                }
     577                return true;
     578            }
     579            return false;
     580        },
     581
     582        "MoveCompleted": function(msg) {
     583            if (msg.data.error)
     584            {
     585                // We failed to reach the target
     586
     587                // remove us from the list of entities gathering from Resource.
     588                var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     589                var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
     590                if (cmpSupply && cmpOwnership)
     591                    cmpSupply.RemoveGatherer(this.entity, cmpOwnership.GetOwner());
     592                else if (cmpSupply)
     593                    cmpSupply.RemoveGatherer(this.entity);
     594
     595                // Save the current order's data in case we need it later
     596                var oldType = this.order.data.type;
     597                var oldTarget = this.order.data.target;
     598                var oldTemplate = this.order.data.template;
     599
     600                // Try the next queued order if there is any
     601                if (this.FinishOrder())
     602                    return;
     603
     604                // Try to find another nearby target of the same specific type
     605                // Also don't switch to a different type of huntable animal
     606                var nearby = this.FindNearbyResource(function (ent, type, template) {
     607                    return (
     608                        ent != oldTarget
     609                        && ((type.generic == "treasure" && oldType.generic == "treasure")
     610                        || (type.specific == oldType.specific
     611                        && (type.specific != "meat" || oldTemplate == template)))
     612                    );
     613                });
     614                if (nearby)
     615                {
     616                    this.PerformGather(nearby, false, false);
     617                    return;
     618                }
     619
     620                // Couldn't find anything else. Just try this one again,
     621                // maybe we'll succeed next time
     622                this.PerformGather(oldTarget, false, false);
     623                return;
     624            }
     625
     626            // We reached the target - start gathering from it now
     627            this.SetNextState("GATHERING");
     628        },
     629       
     630        "leave": function() {
     631            // don't use ownership because this is called after a conversion/resignation
     632            // and the ownership would be invalid then.
     633            var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
     634            if (cmpSupply)
     635                cmpSupply.RemoveGatherer(this.entity);
     636            delete this.gatheringTarget;
     637        },
     638    },
     639   
     640    // Walking to a good place to gather resources near, used by GatherNearPosition
     641    "WALKING": {
     642        "enter": function() {
     643            this.SelectAnimation("move");
     644        },
     645
     646        "MoveCompleted": function(msg) {
     647            var resourceType = this.order.data.type;
     648            var resourceTemplate = this.order.data.template;
     649
     650            // Try to find another nearby target of the same specific type
     651            // Also don't switch to a different type of huntable animal
     652            var nearby = this.FindNearbyResource(function (ent, type, template) {
     653                return (
     654                    (type.generic == "treasure" && resourceType.generic == "treasure")
     655                    || (type.specific == resourceType.specific
     656                    && (type.specific != "meat" || resourceTemplate == template))
     657                );
     658            });
     659
     660            // If there is a nearby resource start gathering
     661            if (nearby)
     662            {
     663                this.PerformGather(nearby, false, false);
     664                return;
     665            }
     666
     667            // Couldn't find nearby resources, so give up
     668            if (this.FinishOrder())
     669                return;
     670
     671            // Nothing better to do: go back to dropsite
     672            var nearby = this.FindNearestDropsite(resourceType.generic);
     673            if (nearby)
     674            {
     675                this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
     676                return;
     677            }
     678
     679            // No dropsites, just give up
     680        },
     681    },
     682
     683    "GATHERING": {
     684        "enter": function() {
     685            this.gatheringTarget = this.order.data.target;  // deleted in "leave".
     686
     687            // Check if the resource is full.
     688            if (this.gatheringTarget)
     689            {
     690                // Check that we can gather from the resource we're supposed to gather from.
     691                // Will only be added if we're not already in.
     692                var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     693                var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
     694                if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))
     695                {
     696                    this.gatheringTarget = INVALID_ENTITY;
     697                    this.StartTimer(0);
     698                    return false;
     699                }
     700            }
     701
     702            // If this order was forced, the player probably gave it, but now we've reached the target
     703            //  switch to an unforced order (can be interrupted by attacks)
     704            this.order.data.force = false;
     705            this.order.data.autoharvest = true;
     706
     707            // Calculate timing based on gather rates
     708            // This allows the gather rate to control how often we gather, instead of how much.
     709            var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
     710            var rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget);
     711
     712            if (!rate)
     713            {
     714                // Try to find another target if the current one stopped existing
     715                if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity))
     716                {
     717                    // Let the Timer logic handle this
     718                    this.StartTimer(0);
     719                    return false;
     720                }
     721
     722                // No rate, give up on gathering
     723                this.FinishOrder();
     724                return true;
     725            }
     726
     727            // Scale timing interval based on rate, and start timer
     728            // The offset should be at least as long as the repeat time so we use the same value for both.
     729            var offset = 1000/rate;
     730            var repeat = offset;
     731            this.StartTimer(offset, repeat);
     732
     733            // We want to start the gather animation as soon as possible,
     734            // but only if we're actually at the target and it's still alive
     735            // (else it'll look like we're chopping empty air).
     736            // (If it's not alive, the Timer handler will deal with sending us
     737            // off to a different target.)
     738            if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
     739            {
     740                var typename = "gather_" + this.order.data.type.specific;
     741                this.SelectAnimation(typename, false, 1.0, typename);
     742            }
     743            return false;
     744        },
     745
     746        "leave": function() {
     747            this.StopTimer();
     748
     749            // don't use ownership because this is called after a conversion/resignation
     750            // and the ownership would be invalid then.
     751            var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
     752            if (cmpSupply)
     753                cmpSupply.RemoveGatherer(this.entity);
     754            delete this.gatheringTarget;
     755
     756            // Show the carried resource, if we've gathered anything.
     757            this.SetGathererAnimationOverride();
     758        },
     759
     760        "Timer": function(msg) {
     761            var resourceTemplate = this.order.data.template;
     762            var resourceType = this.order.data.type;
     763
     764            var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     765            if (!cmpOwnership)
     766                return;
     767
     768            var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
     769            if (cmpSupply && cmpSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity))
     770            {
     771                // Check we can still reach and gather from the target
     772                if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer) && this.CanGather(this.gatheringTarget))
     773                {
     774                    // Gather the resources:
     775
     776                    var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
     777
     778                    // Try to gather treasure
     779                    if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget))
     780                        return;
     781
     782                    // If we've already got some resources but they're the wrong type,
     783                    // drop them first to ensure we're only ever carrying one type
     784                    if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic))
     785                        cmpResourceGatherer.DropResources();
     786
     787                    // Collect from the target
     788                    var status = cmpResourceGatherer.PerformGather(this.gatheringTarget);
     789
     790                    // If we've collected as many resources as possible,
     791                    // return to the nearest dropsite
     792                    if (status.filled)
     793                    {
     794                        var nearby = this.FindNearestDropsite(resourceType.generic);
     795                        if (nearby)
     796                        {
     797                            // (Keep this Gather order on the stack so we'll
     798                            // continue gathering after returning)
     799                            this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
     800                            return;
     801                        }
     802
     803                        // Oh no, couldn't find any drop sites. Give up on gathering.
     804                        this.FinishOrder();
     805                        return;
     806                    }
     807
     808                    // We can gather more from this target, do so in the next timer
     809                    if (!status.exhausted)
     810                        return;
     811                }
     812                else
     813                {
     814                    // Try to follow the target
     815                    if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer))
     816                    {
     817                        this.SetNextState("APPROACHING");
     818                        return;
     819                    }
     820
     821                    // Can't reach the target, or it doesn't exist any more
     822
     823                    // We want to carry on gathering resources in the same area as
     824                    // the old one. So try to get close to the old resource's
     825                    // last known position
     826
     827                    var maxRange = 8; // get close but not too close
     828                    if (this.order.data.lastPos &&
     829                        this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z,
     830                            0, maxRange))
     831                    {
     832                        this.SetNextState("APPROACHING");
     833                        return;
     834                    }
     835                }
     836            }
     837
     838            // We're already in range, can't get anywhere near it or the target is exhausted.
     839
     840            var herdPos = this.order.data.initPos;
     841
     842            // Give up on this order and try our next queued order
     843            if (this.FinishOrder())
     844                return;
     845
     846            // No remaining orders - pick a useful default behaviour
     847
     848            // Try to find a new resource of the same specific type near our current position:
     849            // Also don't switch to a different type of huntable animal
     850            var nearby = this.FindNearbyResource(function (ent, type, template) {
     851                return (
     852                    (type.generic == "treasure" && resourceType.generic == "treasure")
     853                    || (type.specific == resourceType.specific
     854                    && (type.specific != "meat" || resourceTemplate == template))
     855                );
     856            });
     857            if (nearby)
     858            {
     859                this.PerformGather(nearby, false, false);
     860                return;
     861            }
     862
     863            // If hunting, try to go to the initial herd position to see if we are more lucky
     864            if (herdPos)
     865                this.GatherNearPosition(herdPos.x, herdPos.z, resourceType, resourceTemplate);
     866
     867            // Nothing else to gather - if we're carrying anything then we should
     868            // drop it off, and if not then we might as well head to the dropsite
     869            // anyway because that's a nice enough place to congregate and idle
     870
     871            var nearby = this.FindNearestDropsite(resourceType.generic);
     872            if (nearby)
     873            {
     874                this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
     875                return;
     876            }
     877           
     878            // No dropsites - just give up
     879        },
     880    },
     881};
     882
     883UnitAI.prototype.UnitFsmSpec.INDIVIDUAL.RETURNRESOURCE = {
     884    "APPROACHING": {
     885        "enter": function () {
     886            this.SelectAnimation("move");
     887        },
     888
     889        "MoveCompleted": function() {
     890            // Switch back to idle animation to guarantee we won't
     891            // get stuck with the carry animation after stopping moving
     892            this.SelectAnimation("idle");
     893
     894            // Check the dropsite is in range and we can return our resource there
     895            // (we didn't get stopped before reaching it)
     896            if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
     897            {
     898                var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
     899                if (cmpResourceDropsite)
     900                {
     901                    // Dump any resources we can
     902                    var dropsiteTypes = cmpResourceDropsite.GetTypes();
     903
     904                    var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
     905                    cmpResourceGatherer.CommitResources(dropsiteTypes);
     906
     907                    // Stop showing the carried resource animation.
     908                    this.SetGathererAnimationOverride();
     909
     910                    // Our next order should always be a Gather,
     911                    // so just switch back to that order
     912                    this.FinishOrder();
     913                    return;
     914                }
     915            }
     916
     917            // The dropsite was destroyed, or we couldn't reach it, or ownership changed
     918            // Look for a new one.
     919
     920            var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
     921            var genericType = cmpResourceGatherer.GetMainCarryingType();
     922            var nearby = this.FindNearestDropsite(genericType);
     923            if (nearby)
     924            {
     925                this.FinishOrder();
     926                this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
     927                return;
     928            }
     929
     930            // Oh no, couldn't find any drop sites. Give up on returning.
     931            this.FinishOrder();
     932        },
     933    },
     934};
     935
     936/**
     937 * Returns the entity ID of the nearest resource supply where the given
     938 * filter returns true, or undefined if none can be found.
     939 * TODO: extend this to exclude resources that already have lots of
     940 * gatherers.
     941 */
     942UnitAI.prototype.FindNearbyResource = function(filter)
     943{
     944    var range = 64; // TODO: what's a sensible number?
     945
     946    var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     947    // We accept resources owned by Gaia or any player
     948    var players = [0];
     949    for (var i = 1; i < playerMan.GetNumPlayers(); ++i)
     950        players.push(i);
     951
     952    var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
     953    var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     954    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     955    var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_ResourceSupply);
     956    for each (var ent in nearby)
     957    {
     958        if (!this.CanGather(ent))
     959            continue;
     960        var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
     961        var type = cmpResourceSupply.GetType();
     962        var amount = cmpResourceSupply.GetCurrentAmount();
     963        var template = cmpTemplateManager.GetCurrentTemplateName(ent);
     964
     965        // Remove "resource|" prefix from template names, if present.
     966        if (template.indexOf("resource|") != -1)
     967            template = template.slice(9);
     968
     969        if (amount > 0 && cmpResourceSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity) && filter(ent, type, template))
     970            return ent;
     971    }
     972
     973    return undefined;
     974};
     975
     976/**
     977 * Returns the entity ID of the nearest resource dropsite that accepts
     978 * the given type, or undefined if none can be found.
     979 */
     980UnitAI.prototype.FindNearestDropsite = function(genericType)
     981{
     982    // Find dropsites owned by this unit's player
     983    var players = [];
     984    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     985    if (cmpOwnership)
     986        players = [cmpOwnership.GetOwner()];
     987
     988    // Ships are unable to reach land dropsites and shouldn't attempt to do so.
     989    var excludeLand = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship");
     990
     991    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     992    var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite);
     993    if (excludeLand)
     994    {
     995        nearby = nearby.filter( function(e) {
     996            return Engine.QueryInterface(e, IID_Identity).HasClass("Naval");
     997        });
     998    }
     999
     1000    for each (var ent in nearby)
     1001    {
     1002        var cmpDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
     1003        if (!cmpDropsite.AcceptsType(genericType))
     1004            continue;
     1005
     1006        return ent;
     1007    }
     1008
     1009    return undefined;
     1010};
     1011
     1012/**
     1013 * Adds gather order to the queue, forced by the player
     1014 * until the target is reached
     1015 */
     1016UnitAI.prototype.Gather = function(target, queued)
     1017{
     1018    this.PerformGather(target, queued, true);
     1019};
     1020
     1021/*
     1022 * Internal function to abstract the force parameter.
     1023 */
     1024UnitAI.prototype.PerformGather = function(target, queued, force)
     1025{
     1026    if (!this.CanGather(target))
     1027    {
     1028        this.WalkToTarget(target, queued);
     1029        return;
     1030    }
     1031
     1032    // Save the resource type now, so if the resource gets destroyed
     1033    // before we process the order then we still know what resource
     1034    // type to look for more of
     1035    var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
     1036    var type = cmpResourceSupply.GetType();
     1037
     1038    // Also save the target entity's template, so that if it's an animal,
     1039    // we won't go from hunting slow safe animals to dangerous fast ones
     1040    var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
     1041    var template = cmpTemplateManager.GetCurrentTemplateName(target);
     1042
     1043    // Remove "resource|" prefix from template name, if present.
     1044    if (template.indexOf("resource|") != -1)
     1045        template = template.slice(9);
     1046
     1047    // Remember the position of our target, if any, in case it disappears
     1048    // later and we want to head to its last known position
     1049    var lastPos = undefined;
     1050    var cmpPosition = Engine.QueryInterface(target, IID_Position);
     1051    if (cmpPosition && cmpPosition.IsInWorld())
     1052        lastPos = cmpPosition.GetPosition();
     1053
     1054    this.AddOrder("Gather", { "target": target, "type": type, "template": template, "lastPos": lastPos, "force": force }, queued);
     1055};
     1056
     1057/**
     1058 * Adds gather-near-position order to the queue, not forced, so it can be
     1059 * interrupted by attacks.
     1060 */
     1061UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued)
     1062{
     1063    // Remove "resource|" prefix from template name, if present.
     1064    if (template.indexOf("resource|") != -1)
     1065        template = template.slice(9);
     1066
     1067    this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued);
     1068};
     1069
     1070/**
     1071 * Adds return resource order to the queue, forced by the player.
     1072 */
     1073UnitAI.prototype.ReturnResource = function(target, queued)
     1074{
     1075    if (!this.CanReturnResource(target, true))
     1076    {
     1077        this.WalkToTarget(target, queued);
     1078        return;
     1079    }
     1080
     1081    this.AddOrder("ReturnResource", { "target": target, "force": true }, queued);
     1082};
     1083
     1084UnitAI.prototype.CanGather = function(target)
     1085{
     1086    // The target must be a valid resource supply.
     1087    var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
     1088    if (!cmpResourceSupply)
     1089        return false;
     1090
     1091    // Formation controllers should always respond to commands
     1092    // (then the individual units can make up their own minds)
     1093    if (this.IsFormationController())
     1094        return true;
     1095
     1096    // Verify that we're able to respond to Gather commands
     1097    var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
     1098    if (!cmpResourceGatherer)
     1099        return false;
     1100
     1101    // Verify that we can gather from this target
     1102    if (!cmpResourceGatherer.GetTargetGatherRate(target))
     1103        return false;
     1104
     1105    // No need to verify ownership as we should be able to gather from
     1106    // a target regardless of ownership.
     1107    // No need to call "cmpResourceSupply.IsAvailable()" either because that
     1108    // would cause units to walk to full entities instead of choosing another one
     1109    // nearby to gather from, which is undesirable.
     1110    return true;
     1111};
     1112
     1113UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
     1114{
     1115    // Formation controllers should always respond to commands
     1116    // (then the individual units can make up their own minds)
     1117    if (this.IsFormationController())
     1118        return true;
     1119
     1120    // Verify that we're able to respond to ReturnResource commands
     1121    var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
     1122    if (!cmpResourceGatherer)
     1123        return false;
     1124
     1125    // Verify that the target is a dropsite
     1126    var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
     1127    if (!cmpResourceDropsite)
     1128        return false;
     1129
     1130    if (checkCarriedResource)
     1131    {
     1132        // Verify that we are carrying some resources,
     1133        // and can return our current resource to this target
     1134        var type = cmpResourceGatherer.GetMainCarryingType();
     1135        if (!type || !cmpResourceDropsite.AcceptsType(type))
     1136            return false;
     1137    }
     1138
     1139    // Verify that the dropsite is owned by this entity's player
     1140    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     1141    if (!cmpOwnership || !IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
     1142        return false;
     1143
     1144    return true;
     1145};
  • binaries/data/mods/public/simulation/components/UnitAI.js

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

     
     1UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
     2