Ticket #52: WIP-Triggers.patch

File WIP-Triggers.patch, 22.4 KB (added by O.Davoodi, 10 years ago)

WIP patch for triggers

  • binaries/data/mods/public/maps/scripts/test.js

     
     1var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
     2cmpTrigger.StartUpdateTimer(500); // An interval of 0.5 seconds
     3
     4function trigger1_Action(data)
     5{
     6    var cmpKilled = Engine.QueryInterface(data.killedEntity, IID_Identity);
     7    var killedPlayerId = Engine.QueryInterface(data.killedEntity, IID_Ownership).GetOwner()
     8    var killerPlayerId = Engine.QueryInterface(data.killerEntity, IID_Ownership).GetOwner()
     9    var cmpKiller = Engine.QueryInterface(data.killerEntity, IID_Identity);
     10    log("A player " + killerPlayerId + "'s " + cmpKiller.GetGenericName() + " killed a player " + killedPlayerId + "'s " + cmpKilled.GetGenericName() + ".");
     11   
     12    if (cmpKilled.GetClassesList().indexOf("Hero") >= 0)
     13    {
     14        var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     15        var playerEnt = cmpPlayerMan.GetPlayerByID(killedPlayerId);
     16        Engine.PostMessage(playerEnt, MT_PlayerDefeated, { "playerId": killedPlayerId } );
     17    }
     18}
     19
     20function trigger2_Action(data)
     21{
     22    if (data.entity == 473)
     23    {
     24        var cmpProductionQueue = Engine.QueryInterface(273, IID_ProductionQueue);
     25        cmpProductionQueue.SpawnUnits ("units/cart_infantry_swordsman_2_b",5, null);
     26       
     27        log("Ok. Finish it.");
     28        cmpTrigger.StopUpdateTimer();
     29    }
     30}
     31
     32cmpTrigger.RegisterTrigger("trigger1", "OnEntityKilled", trigger1_Action);
     33cmpTrigger.RegisterOnUnitRangeFromEntityTrigger("entered", "trigger2", 273, 30, trigger2_Action, [0, 1, 2]);
     34 No newline at end of file
  • binaries/data/mods/public/simulation/components/Foundation.js

     
    202202            // (via CCmpTemplateManager). Now we need to remove that temporary
    203203            // blocker-disabling, so that we'll perform standard unit blocking instead.
    204204            cmpObstruction.SetDisableBlockMovementPathfinding(false, false, -1);
     205           
     206            // Call the related trigger event
     207            var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
     208            cmpTrigger.CallEvent("OnConstructionStarted", {"foundation": this.entity, "template": this.finalTemplateName});
    205209        }
    206210
    207211        // Switch foundation to scaffold variant
  • binaries/data/mods/public/simulation/components/ProductionQueue.js

     
    288288                "timeTotal": time*1000,
    289289                "timeRemaining": time*1000,
    290290            });
     291           
     292            // Call the related trigger event
     293            var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
     294            cmpTrigger.CallEvent("OnTrainingQueued", {"playerid": cmpPlayer.GetPlayerID(), "unitTemplate": templateName, "count": count, "metadata": metadata, "trainerEntity": this.entity});
    291295        }
    292296        else if (type == "technology")
    293297        {
     
    324328                "timeTotal": time*1000,
    325329                "timeRemaining": time*1000,
    326330            });
     331           
     332            // Call the related trigger event
     333            var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
     334            cmpTrigger.CallEvent("OnResearchQueued", {"playerid": cmpPlayer.GetPlayerID(), "technologyTemplate": templateName, "researcherEntity": this.entity});
    327335        }
    328336        else
    329337        {
  • binaries/data/mods/public/simulation/components/TechnologyManager.js

     
    283283        return;
    284284    }
    285285   
     286    // Call the related trigger event
     287    var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
     288    cmpTrigger.CallEvent("OnResearchFinished", {"researcher": this.entity, "tech": tech});
     289   
    286290    var modifiedComponents = {};
    287291    this.researchedTechs[tech] = template;
    288292    // store the modifications in an easy to access structure
  • binaries/data/mods/public/simulation/components/Trigger.js

     
     1function Trigger() {}
     2
     3Trigger.prototype.Schema =
     4    "<a:component type='system'/><empty/>";
     5
     6Trigger.prototype.Init = function()
     7{
     8    // Each event has its own set of actions determined by the player.
     9    this.eventOnEntityTookDamageActions = {};
     10    this.eventOnEntityKilledActions = {};
     11    this.eventOnStructureBuiltActions = {};
     12    this.eventOnConstructionStartedActions = {};
     13    this.eventOnTrainingFinishedActions = {};
     14    this.eventOnTrainingQueuedActions = {};
     15    this.eventOnResearchFinishedActions = {};
     16    this.eventOnResearchQueuedActions = {};
     17    this.eventAlwaysActions = {};
     18    this.eventOnUnitRangeFromEntityData = {};
     19   
     20    // We are going to have all of the triggers here to be able to enable/disbale them in runtime.
     21    this.triggerEnabled = {};
     22   
     23   
     24};
     25
     26Trigger.prototype.StartUpdateTimer = function(interval)
     27{
     28    // Stop the last timer before starting a new one
     29    if (this.timer !== undefined)
     30        this.StopUpdateTimer();
     31
     32    // Start the update timer
     33    var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     34    this.timer = cmpTimer.SetInterval(this.entity, IID_Trigger, "TimerUpdate", 0, interval, undefined);
     35}
     36
     37Trigger.prototype.StopUpdateTimer = function()
     38{
     39    // Call the update timer
     40    if (this.timer !== undefined)
     41    {
     42        var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     43        cmpTimer.CancelTimer(this.timer);
     44    }
     45}
     46
     47// Binds the "action" function to one of the implemented events. "name" is a string for further access
     48// to the trigger for enabling/disabling.
     49Trigger.prototype.RegisterTrigger = function(name, event, action, enabled)
     50{
     51    enabled = (enabled !== undefined ? enabled : true);
     52    var validEvent = true;
     53    switch (event)
     54    {
     55    case "OnEntityTookDamage":
     56        this.eventOnEntityTookDamageActions[name] = action;
     57        break;
     58    case "OnEntityKilled":
     59        this.eventOnEntityKilledActions[name] = action;
     60        break;
     61    case "OnStructureBuilt":
     62        this.eventOnStructureBuiltActions[name] = action;
     63        break;
     64    case "OnConstructionStarted":
     65        this.eventOnConstructionStartedActions[name] = action;
     66        break;
     67    case "OnTrainingFinished":
     68        this.eventOnTrainingFinishedActions[name] = action;
     69        break;
     70    case "OnTrainingQueued":
     71        this.eventOnTrainingQueuedActions[name] = action;
     72        break;
     73    case "OnResearchFinished":
     74        this.eventOnResearchFinishedActions[name] = action;
     75        break;
     76    case "OnResearchQueued":
     77        this.eventOnResearchQueuedActions[name] = action;
     78        break;
     79    case "Always":
     80        this.eventAlwaysActions[name] = action;
     81        break;
     82    default:
     83        validEvent = false;
     84        break;
     85    }
     86    if (validEvent)
     87        this.triggerEnabled[name] = enabled
     88    else
     89        error("Invalid trigger event \"" + event + "\".");
     90};
     91
     92// This is a performance taxing event to check for, so we should have a special register function for it.
     93Trigger.prototype.RegisterOnUnitRangeFromEntityTrigger = function(event, name, entity, radius, action, players, enabled)
     94{
     95   
     96    enabled = (enabled !== undefined ? enabled : true);
     97   
     98    // If the players parameter is not specified use all players.
     99    if (!players)
     100    {
     101        var playerEntities = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayerEntities();
     102        players = [];
     103        for each (var pentity in playerEntities)
     104            players.push(Engine.QueryInterface(pentity, IID_Player).GetPlayerID());
     105    }
     106   
     107    // To determine if a unit has "entered" or "left" the range, we should know if it was in the range before. We have "currentCollection" for this purpose.
     108    // For the purpose of finding out the intersection of the previous and the current rangeQuery faster, we are sorting the current one.
     109    this.eventOnUnitRangeFromEntityData[name] = {"event": event, "action": action, "entity": entity, "radius": radius, "currentCollection": [], "players": players};
     110    this.triggerEnabled[name] = enabled;
     111    //log(uneval(this.eventOnUnitRangeFromEntityData));
     112}
     113
     114// Disable trigger
     115Trigger.prototype.DisableTrigger = function(name)
     116{
     117    if (this.triggerEnabled[name] !== undefined)
     118        this.triggerEnabled[name] = false;
     119    else
     120        warn("Trigger doesn't exist:" + name);
     121}
     122
     123// Enable trigger
     124Trigger.prototype.EnableTrigger = function(name)
     125{
     126    if (this.triggerEnabled[name] !== undefined)
     127        this.triggerEnabled[name] = true;
     128    else
     129        warn("Trigger doesn't exist:" + name);
     130}
     131
     132// To handle the events that cannot be tracked by messages, this function should be called in the simulation code.
     133Trigger.prototype.CallEvent = function(event, data)
     134{
     135    switch (event)
     136    {
     137    case "OnEntityTookDamage":
     138        for (var i in this.eventOnEntityTookDamageActions)
     139            if (this.triggerEnabled[i])    // Check if it is enabled
     140                this.eventOnEntityTookDamageActions[i](data); // The data for this one is {"attackerEntity": data.attacker,
     141                                                         //"attackedEntity": data.target, "type":data.type, "ammount":-targetState.change}
     142        break;
     143    case "OnTrainingQueued":
     144        for (var i in this.eventOnTrainingQueuedActions)
     145            if (this.triggerEnabled[i])    // Check if it is enabled
     146                this.eventOnTrainingQueuedActions[i](data); // The data for this one is {"playerid": playerid, "unitTemplate": templateName, "count": count, "metadata": metadata, "trainerEntity": this.entity}
     147        break;
     148    case "OnEntityKilled":
     149        for (var i in this.eventOnEntityKilledActions)
     150            {
     151                if (this.triggerEnabled[i])    // Check if it is enabled
     152                    this.eventOnEntityKilledActions[i](data); // The data for this one is {"killerEntity": killerEntity, "killedEntity": targetEntity}
     153           
     154                // Remove the "OnUnitFromRangeEntity" triggers which have this entity as their argument
     155                for (var j in this.eventOnUnitRangeFromEntityData)
     156                {
     157                    if (this.eventOnUnitRangeFromEntityData[j].entity == data.killedEntity)
     158                    {
     159                        delete this.eventOnUnitRangeFromEntityData[j];
     160                        delete this.triggerEnabled[j];
     161                    }
     162                }
     163            }
     164        break;
     165    case "OnConstructionStarted":
     166        for (var i in this.eventOnConstructionStartedActions)
     167            if (this.triggerEnabled[i])    // Check if it is enabled
     168                this.eventOnConstructionStartedActions[i](data); // The data for this one is {"foundation": entity, "template": templateName}
     169        break;
     170    case "OnResearchQueued":
     171        for (var i in this.eventOnResearchQueuedActions)
     172            if (this.triggerEnabled[i])    // Check if it is enabled
     173                this.eventOnResearchQueuedActions[i](data); // The data for this one is {"playerid": cmpPlayer.GetPlayerID(), "technologyTemplate": templateName, "researcherEntity": this.entity}
     174        break;
     175    case "OnResearchFinished":
     176        for (var i in this.eventOnResearchFinishedActions)
     177            if (this.triggerEnabled[i])    // Check if it is enabled
     178                this.eventOnResearchFinishedActions[i](data); // The data for this one is {"playerid": cmpPlayer.GetPlayerID(), "technologyTemplate": templateName, "researcherEntity": this.entity}
     179        break;
     180    default:
     181        warn("Invalid trigger event \"" + event + "\" called.");
     182        break;
     183    }
     184}
     185
     186// Handles "OnStructureBuilt" event.
     187Trigger.prototype.OnGlobalConstructionFinished = function(msg)
     188{
     189    for (var i in this.eventOnStructureBuiltActions)
     190        if (this.triggerEnabled[i])    // Check if it is enabled
     191            this.eventOnStructureBuiltActions[i]({"building": msg.newentity}); // The data for this one is {"building": constructedBuilding}
     192}
     193
     194// Handles "OnTrainingFinished" event.
     195Trigger.prototype.OnGlobalTrainingFinished = function(msg)
     196{
     197    for (var i in this.eventOnTrainingFinishedActions)
     198        if (this.triggerEnabled[i])    // Check if it is enabled
     199            this.eventOnTrainingFinishedActions[i](msg); // The data for this one is {"entities": createdEnts,
     200                                                         // "owner": cmpOwnership.GetOwner(),
     201                                                         // "metadata": metadata,}
     202                                                         // See function "SpawnUnits" in ProductionQueue for more details
     203}
     204
     205// Handles an event that occurs by the interval the map designer specifies
     206// Also handles "OnUnitRangeFromEntity"
     207Trigger.prototype.TimerUpdate = function(data, lateness)
     208{
     209    // Handle "Always" event.
     210    for (var i in this.eventAlwaysActions)
     211        if (this.triggerEnabled[i])    // Check if it is enabled
     212            this.eventAlwaysActions[i](); // This function event has no data
     213   
     214    // OnUnitRangeFromEntity handler
     215    for (var i in this.eventOnUnitRangeFromEntityData)
     216    {
     217        if (this.triggerEnabled[i])    // Check if it is enabled
     218        {
     219            var players = this.eventOnUnitRangeFromEntityData[i].players;
     220            if (!players)
     221            {
     222                var playerEntities = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayerEntities();
     223                players = [];
     224                for each (var entity in playerEntities)
     225                    players.push(Engine.QueryInterface(entity, IID_Player).GetPlayerID());
     226            }
     227           
     228            var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     229           
     230            var rangeQuery = rangeManager.ExecuteQuery(this.eventOnUnitRangeFromEntityData[i].entity, 0, this.eventOnUnitRangeFromEntityData[i].radius, players, 0);
     231            var previousQuery = this.eventOnUnitRangeFromEntityData[i].currentCollection;
     232           
     233            // To find the entities that have entered and left the range:
     234            var entitiesMovingInside = [];
     235            var entitiesEntered = [];
     236            var entitiesLeft = [];
     237           
     238            var isMovingInsideEvent = false, isEnteredEvent = false, isLeftEvent = false;
     239            if (this.eventOnUnitRangeFromEntityData[i].event == "moved")
     240                isMovingInsideEvent = true;
     241            else if (this.eventOnUnitRangeFromEntityData[i].event == "entered")
     242                isEnteredEvent = true;
     243            else
     244                isLeftEvent = true;
     245           
     246            rangeQuery.sort();
     247            var m = 0, n = 0;
     248           
     249            // Find the entities that entered, the ones that left, and the ones that are moving inside the range
     250            while (m < previousQuery.length && n < rangeQuery.length)
     251            {
     252                if (previousQuery[m] == rangeQuery[n])
     253                {
     254                    if (isMovingInsideEvent)
     255                    {
     256                        var cmpUnitMotion = Engine.QueryInterface(previousQuery[m], IID_UnitMotion);
     257                        if (cmpUnitMotion && cmpUnitMotion.IsMoving())
     258                            entitiesMovingInside.push(previousQuery[m]);
     259                    }
     260                    ++m;
     261                    ++n;
     262                }
     263                else if (previousQuery[m] > rangeQuery[n])
     264                {
     265                    if (isEnteredEvent)
     266                        entitiesEntered.push(rangeQuery[n]);
     267                    ++n;
     268                }
     269                else
     270                {
     271                    if (isLeftEvent)
     272                        entitiesLeft.push(previousQuery[m]);
     273                    ++m;
     274                }
     275            }
     276            if (isLeftEvent)
     277                while (m < previousQuery.length)
     278                {
     279                    entitiesLeft.push(previousQuery[m]);
     280                    ++m;
     281                }
     282            else if (isEnteredEvent)
     283                while (n < rangeQuery.length)
     284                {
     285                    entitiesEntered.push(rangeQuery[n]);
     286                    ++n;
     287                }
     288           
     289            // Now do the actions for all of the entities that are calling those events
     290            if (isMovingInsideEvent)
     291                for (var m = 0; m < entitiesMovingInside.length; ++m)
     292                    this.eventOnUnitRangeFromEntityData[i].action({"entity": entitiesMovingInside[m]});
     293            else if (isEnteredEvent)
     294                for (var m = 0; m < entitiesEntered.length; ++m)
     295                    this.eventOnUnitRangeFromEntityData[i].action({"entity": entitiesEntered[m]});
     296            else
     297                for (var m = 0; m < entitiesLeft.length; ++m)
     298                    this.eventOnUnitRangeFromEntityData[i].action({"entity": entitiesLeft[m]});
     299           
     300            this.eventOnUnitRangeFromEntityData[i].currentCollection = rangeQuery;
     301        }
     302    }
     303}
     304
     305Engine.RegisterComponentType(IID_Trigger, "Trigger", Trigger);
  • binaries/data/mods/public/simulation/helpers/Damage.js

     
    7474    // Damage the target
    7575    var targetState = cmpDamageReceiver.TakeDamage(data.strengths.hack * data.multiplier, data.strengths.pierce * data.multiplier, data.strengths.crush * data.multiplier, data.attacker);
    7676
     77    // Call the related trigger event
     78    // We have to call the event instead of listening to it by messages because the message is sent
     79    // after the target gets killed, which is not our intended case
     80    var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
     81    cmpTrigger.CallEvent("OnEntityTookDamage", {"attackerEntity": data.attacker, "attackedEntity": data.target, "type":data.type, "ammount":-targetState.change});
     82   
    7783    // If the target was killed run some cleanup
    7884    if (targetState.killed)
    7985        Damage.TargetKilled(data.attacker, data.target);
     
    110116    // Call RangeManager with dummy entity and return the result.
    111117    var rangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    112118    var rangeQuery = rangeManager.ExecuteQueryAroundPos(origin, 0, radius, players, IID_DamageReceiver);
     119   
    113120    return rangeQuery;
    114121};
    115122
     
    133140    var cmpLooter = Engine.QueryInterface(killerEntity, IID_Looter);
    134141    if (cmpLooter)
    135142        cmpLooter.Collect(targetEntity);
     143       
     144    // Call the related trigger event
     145    var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
     146    cmpTrigger.CallEvent("OnEntityKilled", {"killerEntity": killerEntity, "killedEntity": targetEntity});
    136147};
    137148
    138149Engine.RegisterGlobal("Damage", Damage);
  • source/simulation2/components/ICmpTrigger.cpp

     
     1/* Copyright (C) 2011 Wildfire Games.
     2 * This file is part of 0 A.D.
     3 *
     4 * 0 A.D. is free software: you can redistribute it and/or modify
     5 * it under the terms of the GNU General Public License as published by
     6 * the Free Software Foundation, either version 2 of the License, or
     7 * (at your option) any later version.
     8 *
     9 * 0 A.D. is distributed in the hope that it will be useful,
     10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12 * GNU General Public License for more details.
     13 *
     14 * You should have received a copy of the GNU General Public License
     15 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
     16 */
     17
     18#include "precompiled.h"
     19
     20#include "ICmpTrigger.h"
     21
     22#include "simulation2/system/InterfaceScripted.h"
     23#include "simulation2/scripting/ScriptComponent.h"
     24
     25
     26BEGIN_INTERFACE_WRAPPER(Trigger)
     27END_INTERFACE_WRAPPER(Trigger)
     28
     29class CCmpTriggerScripted : public ICmpTrigger
     30{
     31public:
     32    DEFAULT_SCRIPT_WRAPPER(TriggerScripted)
     33};
     34
     35REGISTER_COMPONENT_SCRIPT_WRAPPER(TriggerScripted)
  • source/simulation2/components/ICmpTrigger.h

     
     1/* Copyright (C) 2011 Wildfire Games.
     2 * This file is part of 0 A.D.
     3 *
     4 * 0 A.D. is free software: you can redistribute it and/or modify
     5 * it under the terms of the GNU General Public License as published by
     6 * the Free Software Foundation, either version 2 of the License, or
     7 * (at your option) any later version.
     8 *
     9 * 0 A.D. is distributed in the hope that it will be useful,
     10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12 * GNU General Public License for more details.
     13 *
     14 * You should have received a copy of the GNU General Public License
     15 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
     16 */
     17
     18#ifndef INCLUDED_ICMPTRIGGER
     19#define INCLUDED_ICMPTRIGGER
     20
     21#include "simulation2/system/Interface.h"
     22
     23class ICmpTrigger : public IComponent
     24{
     25public:
     26    DECLARE_INTERFACE_TYPE(Trigger)
     27};
     28
     29#endif // INCLUDED_ICMPTRIGGER
  • source/simulation2/serialization/BinarySerializer.cpp

     
    273273    }
    274274    case JSTYPE_FUNCTION:
    275275    {
    276         // We can't serialise functions, but we can at least name the offender (hopefully)
    277         std::wstring funcname(L"(unnamed)");
    278         JSFunction* func = JS_ValueToFunction(cx, val);
    279         if (func)
    280         {
    281             JSString* string = JS_GetFunctionId(func);
    282             if (string)
    283             {
    284                 size_t length;
    285                 const jschar* ch = JS_GetStringCharsAndLength(cx, string, &length);
    286                 if (ch && length > 0)
    287                     funcname = std::wstring(ch, ch + length);
    288             }
    289         }
    290 
    291         LOGERROR(L"Cannot serialise JS objects of type 'function': %ls", funcname.c_str());
    292         throw PSERROR_Serialize_InvalidScriptValue();
     276        //TEMPORARY
     277        ScriptString("string", JS_ValueToString(cx, val));
     278        break;
    293279    }
    294280    case JSTYPE_STRING:
    295281    {
  • source/simulation2/Simulation2.cpp

     
    133133            LOAD_SCRIPTED_COMPONENT("PlayerManager");
    134134            LOAD_SCRIPTED_COMPONENT("TechnologyTemplateManager");
    135135            LOAD_SCRIPTED_COMPONENT("Timer");
     136            LOAD_SCRIPTED_COMPONENT("Trigger");
    136137            LOAD_SCRIPTED_COMPONENT("ValueModificationManager");
    137138
    138139#undef LOAD_SCRIPTED_COMPONENT
     
    748749
    749750    if (!m->m_StartupScript.empty())
    750751        GetScriptInterface().LoadScript(L"map startup script", m->m_StartupScript);
     752   
     753    // Load the trigger script after we have loaded the simulation and the map.
     754    if (GetScriptInterface().HasProperty(m->m_MapSettings.get(), "TriggerScript"))
     755    {
     756        std::string script_name;
     757        GetScriptInterface().GetProperty(m->m_MapSettings.get(), "TriggerScript", script_name);
     758
     759        script_name = "maps/scripts/" + script_name;
     760        m->m_ComponentManager.LoadScript(script_name.data());
     761    }
    751762}
    752763
    753764int CSimulation2::ProgressiveLoad()
  • source/simulation2/TypeList.h

     
    155155INTERFACE(TerritoryManager)
    156156COMPONENT(TerritoryManager)
    157157
     158INTERFACE(Trigger)
     159COMPONENT(TriggerScripted)
     160
    158161INTERFACE(UnitMotion)
    159162COMPONENT(UnitMotion) // must be after Obstruction
    160163COMPONENT(UnitMotionScripted)