Ticket #999: #999-2012-03-07.patch

File #999-2012-03-07.patch, 46.4 KB (added by leper, 12 years ago)
  • binaries/data/mods/public/art/textures/cursors/action-heal-disabled.txt

     
     11 1
  • binaries/data/mods/public/art/textures/cursors/action-heal.txt

     
     11 1
  • binaries/data/mods/public/gui/session/input.js

     
    1515const ACTION_NONE = 0;
    1616const ACTION_GARRISON = 1;
    1717const ACTION_REPAIR = 2;
     18const ACTION_HEAL = 3;
    1819var preSelectedAction = ACTION_NONE;
    1920
    2021var INPUT_NORMAL = 0;
     
    255256                }
    256257            }
    257258            break;
     259        case "heal":
     260            // The check if the target is unhealable is done by targetState.needsHeal
     261            if (entState.Ability && isUnit(targetState) && targetState.needsHeal && (playerOwned || allyOwned))
     262            {
     263                var unhealableClasses = entState.Ability.Healer.unhealableClasses;
     264                for each (var unitClass in targetState.identity.classes)
     265                {
     266                    if (unhealableClasses.indexOf(unitClass) != -1)
     267                    {
     268                        return {"possible": false};
     269                    }
     270                }
     271               
     272                var healableClasses = entState.Ability.Healer.healableClasses;
     273                for each (var unitClass in targetState.identity.classes)
     274                {
     275                    if (healableClasses.indexOf(unitClass) != -1)
     276                    {
     277                        return {"possible": true};
     278                    }
     279                }
     280            }
     281            break;
    258282        case "gather":
    259283            if (targetState.resourceSupply && (playerOwned || gaiaOwned))
    260284            {
     
    350374            else
    351375                return {"type": "none", "cursor": "action-repair-disabled", "target": undefined};
    352376            break;
     377        case ACTION_HEAL:
     378            if (getActionInfo("heal", target).possible)
     379                return {"type": "heal", "cursor": "action-heal", "target": target};
     380            else
     381                return {"type": "none", "cursor": "action-heal-disabled", "target": undefined};
     382            break;
    353383        }
    354384    }
    355385    else if (Engine.HotkeyIsPressed("session.garrison"))
     
    372402            return {"type": "build", "cursor": "action-repair", "target": target};
    373403        else if ((actionInfo = getActionInfo("set-rallypoint", target)).possible)
    374404            return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "position": actionInfo.position};
     405        else if (getActionInfo("heal", target).possible)
     406            return {"type": "heal", "cursor": "action-heal", "target": target};
    375407        else if (getActionInfo("attack", target).possible)
    376408            return {"type": "attack", "cursor": "action-attack", "target": target};
    377409        else if (getActionInfo("unset-rallypoint", target).possible)
     
    9931025        Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
    9941026        return true;
    9951027
     1028    case "promote":
     1029        Engine.PostNetworkCommand({"type": "promote", "entities": selection, "target": action.target, "queued": queued});
     1030        // TODO:Play a sound?
     1031        return true;
     1032
     1033    case "heal":
     1034        Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued});
     1035        // TODO: Play a sound?
     1036//      Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] });
     1037        return true;
     1038
    9961039    case "build": // (same command as repair)
    9971040    case "repair":
    9981041        Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
     
    12051248                inputState = INPUT_PRESELECTEDACTION;
    12061249                preSelectedAction = ACTION_REPAIR;
    12071250                break;
     1251            case "heal":
     1252                inputState = INPUT_PRESELECTEDACTION;
     1253                preSelectedAction = ACTION_HEAL;
     1254                break;
     1255            case "promote":
     1256                doAction({ "type": "promote"})
     1257                break;
    12081258            case "unload-all":
    12091259                unloadAll(entity);
    12101260                break;
  • binaries/data/mods/public/gui/session/session.xml

     
    725725                    </object>
    726726                </object>
    727727
     728                <object name="unitAbilityPanel"
     729                    size="14 12 100% 100%"
     730                >
     731                    <object size="0 0 100% 100%">
     732                        <repeat count="24">
     733                            <object name="unitAbilityButton[n]" hidden="true" style="iconButton" type="button" size="0 0 46 46" tooltip_style="sessionToolTipBottom">
     734                                <object name="unitAbilityIcon[n]" type="image" ghost="true" size="3 3 43 43"/>
     735                            </object>
     736                        </repeat>
     737                    </object>
     738                </object>
     739
    728740                <object name="unitResearchPanel"
    729741                    style="TranslucentPanelThinBorder"
    730742                    size="0 100%-56 100% 100%"
  • binaries/data/mods/public/gui/session/unit_commands.js

     
    55const FORMATION = "Formation";
    66const TRAINING = "Training";
    77const CONSTRUCTION = "Construction";
     8const ABILITY = "Ability";
    89const COMMAND = "Command";
    910const STANCE = "Stance";
    1011
     
    2021const BARTER_ACTIONS = ["Sell", "Buy"];
    2122
    2223// The number of currently visible buttons (used to optimise showing/hiding)
    23 var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Barter": 0, "Training": 0, "Construction": 0, "Command": 0, "Stance": 0};
     24var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Barter": 0, "Training": 0, "Construction": 0, "Ability": 0, "Command": 0, "Stance": 0};
    2425
    2526// Unit panels are panels with row(s) of buttons
    26 var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Barter", "Training", "Construction", "Research", "Stance", "Command"];
     27var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Barter", "Training", "Construction", "Ability", "Research", "Stance", "Command"];
    2728
    2829// Indexes of resources to sell and buy on barter panel
    2930var g_barterSell = 0;
     
    174175                numberOfItems =  24;
    175176            break;
    176177
     178        case ABILITY:
     179            if (numberOfItems > 24)
     180                numberOfItems = 24;
     181            break;
     182
    177183        case COMMAND:
    178184            if (numberOfItems > 6)
    179185                numberOfItems = 6;
     
    190196        var item = items[i];
    191197        var entType = ((guiName == "Queue")? item.template : item);
    192198        var template;
    193         if (guiName != "Formation" && guiName != "Command" && guiName != "Stance")
     199        if (guiName != "Formation" && guiName != "Command" && guiName != "Stance" && guiName != "Ability")
    194200        {
    195201            template = GetTemplateData(entType);
    196202            if (!template)
     
    269275
    270276                break;
    271277
     278            case ABILITY:
     279                // TODO read tooltips from some file or template based on 'item'
     280                var tooltip;
     281                switch(item)
     282                {
     283                    case "heal":
     284                        tooltip = "Heal units";
     285                        break;
     286                    case "promote":
     287                        tooltip = "Promote this unit";
     288                        break;
     289                    default:
     290                        tooltip = "No tooltip defined";
     291                    break;
     292                }
     293                break;
     294
    272295            case COMMAND:
    273296                // here, "item" is an object with properties .name (command name), .tooltip and .icon (relative to session/icons/single)
    274297                if (item.name == "unload-all")
     
    338361            icon.sprite = "stretched:session/icons/single/" + item.icon;
    339362
    340363        }
     364        else if (guiName == "Ability")
     365        {
     366            icon.sprite = "stretched:session/icons/single/"+item+".png";
     367        }
    341368        else if (template.icon)
    342369        {
    343370            icon.sprite = "stretched:session/portraits/" + template.icon;
     
    511538            setupUnitBarterPanel(entState);
    512539        }
    513540
     541        if (entState.Ability)
     542        {
     543            var abilities = [];
     544            if (entState.Ability.Healer)
     545                abilities.push("heal");
     546            if (entState.Ability.Promote)
     547                abilities.push("promote");
     548            setupUnitPanel("Ability", usedPanels, entState, abilities, function (item) { performCommand(entState.id, item); });
     549        }
     550
    514551        if (entState.buildEntities && entState.buildEntities.length)
    515552        {
    516553            setupUnitPanel("Construction", usedPanels, entState, entState.buildEntities, startBuildingPlacement);
  • binaries/data/mods/public/simulation/components/GarrisonHolder.js

     
    308308            var cmpHealth = Engine.QueryInterface(entity, IID_Health);
    309309            if (cmpHealth)
    310310            {
    311                 if (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
     311                // We do not want to heal unhealable units
     312                if (!cmpHealth.IsUnhealable() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
    312313                    cmpHealth.Increase(this.healRate);
    313314            }
    314315        }
  • binaries/data/mods/public/simulation/components/GuiInterface.js

     
    155155        ret.hitpoints = cmpHealth.GetHitpoints();
    156156        ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
    157157        ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
     158        ret.needsHeal = !cmpHealth.IsUnhealable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
    158159    }
    159160
    160161    var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
     
    264265        ret.barterMarket = { "prices": cmpBarter.GetPrices() };
    265266    }
    266267
     268    // Abilities
     269    var cmpHeal = Engine.QueryInterface(ent, IID_Heal);
     270    // Check if we have abilities
     271    if (cmpHeal || (cmpHeal && cmpPromotion))
     272        ret.Ability = [];
     273    if (cmpHeal)
     274    {
     275        ret.Ability.Healer = {
     276            "unhealableClasses": cmpHeal.GetUnhealableClasses(),
     277            "healableClasses": cmpHeal.GetHealableClasses(),
     278        };
     279    }
     280
     281    // TODO remove this; This is just used to test/demonstrate the extensibility
     282    // of the Ability system
     283    // promoteAbility (just for healers)
     284    if (cmpPromotion && cmpHeal)
     285    {
     286        ret.Ability.Promote = true ;
     287    }
     288
    267289    var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    268290    ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
    269291
  • binaries/data/mods/public/simulation/components/Heal.js

     
     1function Heal() {}
     2
     3Heal.prototype.Schema =
     4    "<a:help>Controls the healing abilities of the unit.</a:help>" +
     5    "<a:example>" +
     6        "<Range>20</Range>" +
     7        "<HP>5</HP>" +
     8        "<Rate>2000</Rate>" +
     9        "<UnhealableClasses datatype=\"tokens\">Cavalry</UnhealableClasses>" +
     10        "<HealableClasses datatype=\"tokens\">Support Infantry</HealableClasses>" +
     11    "</a:example>" +
     12    "<element name='Range' a:help='Range (in metres) where healing is possible'>" +
     13        "<ref name='nonNegativeDecimal'/>" +
     14    "</element>" +
     15    "<element name='HP' a:help='Hitpoints healed per Rate'>" +
     16        "<ref name='nonNegativeDecimal'/>" +
     17    "</element>" +
     18    "<element name='Rate' a:help='A heal is performed every Rate ms'>" +
     19        "<ref name='nonNegativeDecimal'/>" +
     20    "</element>" +
     21    "<element name='UnhealableClasses' a:help='If the target has any of these classes it can not be healed (even if it has a class from HealableClasses)'>" +
     22        "<attribute name='datatype'>" +
     23            "<value>tokens</value>" +
     24        "</attribute>" +
     25        "<text/>" +
     26    "</element>" +
     27    "<element name='HealableClasses' a:help='The target must have one of these classes to be healable'>" +
     28        "<attribute name='datatype'>" +
     29            "<value>tokens</value>" +
     30        "</attribute>" +
     31        "<text/>" +
     32    "</element>";
     33
     34Heal.prototype.Init = function()
     35{
     36};
     37
     38Heal.prototype.Serialize = null; // we have no dynamic state to save
     39
     40Heal.prototype.GetTimers = function()
     41{
     42    var prepare = 1000;
     43    var repeat = +(this.template.Rate || 1000);
     44    return { "prepare": prepare, "repeat": repeat };
     45};
     46
     47Heal.prototype.GetRange = function()
     48{
     49    var max = +this.template.Range;
     50    var min = 0;
     51    return { "max": max, "min": min };
     52};
     53
     54Heal.prototype.GetUnhealableClasses = function()
     55{
     56    var classes = this.template.UnhealableClasses._string;
     57    // If we have no unhealable classes defined classes is undefined
     58    return classes?classes.split(/\s+/):"";
     59};
     60
     61Heal.prototype.GetHealableClasses = function()
     62{
     63    var classes = this.template.HealableClasses._string;
     64    return classes.split(/\s+/);
     65};
     66
     67/**
     68 * Heal the target entity. This should only be called after a successful range
     69 * check, and should only be called after GetTimers().repeat msec has passed
     70 * since the last call to PerformHeal.
     71 */
     72Heal.prototype.PerformHeal = function(target)
     73{
     74    var cmpHealth = Engine.QueryInterface(target, IID_Health);
     75    if (!cmpHealth)
     76        return;
     77    var targetState = cmpHealth.Increase(Math.max(0,this.template.HP));
     78
     79    // Add XP
     80    var cmpLoot = Engine.QueryInterface(target, IID_Loot);
     81    var cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
     82    if (targetState.old && targetState.new && cmpLoot && cmpPromotion)
     83    {
     84        // HP healed * XP per HP
     85        cmpPromotion.IncreaseXp((targetState.new-targetState.old)*(cmpLoot.GetXp()/cmpHealth.GetMaxHitpoints()));
     86    }
     87    //TODO we need a sound file
     88//  PlaySound("heal_impact", this.entity);
     89};
     90
     91Engine.RegisterComponentType(IID_Heal, "Heal", Heal);
  • binaries/data/mods/public/simulation/components/Health.js

     
    2525            "<value a:help='Remain in the world with 0 health'>remain</value>" +
    2626        "</choice>" +
    2727    "</element>" +
    28     "<element name='Healable' a:help='Indicates that the entity can be healed by healer units'>" +
     28    "<element name='Unhealable' a:help='Indicates that the entity can not be healed by healer units'>" +
    2929        "<data type='boolean'/>" +
    3030    "</element>" +
    3131    "<element name='Repairable' a:help='Indicates that the entity can be repaired by builder units'>" +
     
    7272    return (this.template.Repairable == "true");
    7373};
    7474
     75Health.prototype.IsUnhealable = function()
     76{
     77    return (this.template.Unhealable == "true");
     78};
     79
    7580Health.prototype.Kill = function()
    7681{
    7782    this.Reduce(this.hitpoints);
     
    131136{
    132137    // If we're already dead, don't allow resurrection
    133138    if (this.hitpoints == 0)
    134         return;
     139        return false;
    135140
    136141    var old = this.hitpoints;
    137142    this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints());
    138143
    139144    Engine.PostMessage(this.entity, MT_HealthChanged, { "from": old, "to": this.hitpoints });
     145    // We return the old and the actual hp
     146    return { "old": old, "new": this.hitpoints};
    140147};
    141148
    142149//// Private functions ////
  • binaries/data/mods/public/simulation/components/Loot.js

     
    2121
    2222Loot.prototype.GetXp = function()
    2323{
    24     return this.template.xp;
     24    return +(this.template.xp || 0);
    2525};
    2626
    2727Loot.prototype.GetResources = function()
  • binaries/data/mods/public/simulation/components/UnitAI.js

     
    139139    "HealthChanged": function(msg) {
    140140        // ignore
    141141    },
     142   
     143    // TODO: This is part of a really UGLY EVIL HACK
     144    "GlobalHealthChanged": function() {
     145        // ignore
     146    },
    142147
    143148    "EntityRenamed": function(msg) {
    144149        // ignore
     
    283288        this.FinishOrder();
    284289    },
    285290
     291    "Order.Heal": function(msg) {
     292        // Check the target is alive
     293        if (!this.TargetIsAlive(this.order.data.target))
     294        {
     295            this.FinishOrder();
     296            return;
     297        }
     298
     299        // Check if the target is in range
     300        if (this.CheckTargetRange(this.order.data.target, IID_Heal))
     301        {
     302            this.StopMoving();
     303            this.SetNextState("INDIVIDUAL.HEAL.HEALING");
     304            return;
     305        }
     306
     307        // If we can't reach the target, but are standing ground,
     308        // then abandon this heal order
     309        if (this.GetStance().respondStandGround && !this.order.data.force)
     310        {
     311            this.FinishOrder();
     312            return;
     313        }
     314
     315        // Try to move within heal range
     316        if (this.MoveToTargetRange(this.order.data.target, IID_Heal))
     317        {
     318            // We've started walking to the given point
     319            this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
     320            return;
     321        }
     322
     323        // We can't reach the target, and can't move towards it,
     324        // so abandon this heal order
     325        this.FinishOrder();
     326    },
     327
    286328    "Order.Gather": function(msg) {
    287329       
    288330        // If the target is still alive, we need to kill it first
     
    402444            cmpFormation.Disband();
    403445        },
    404446
     447        "Order.Heal": function(msg) {
     448            // TODO: see notes in Order.Attack
     449            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     450            cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]);
     451            cmpFormation.Disband();
     452        },
     453
    405454        "Order.Repair": function(msg) {
    406455            // TODO: see notes in Order.Attack
    407456            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     
    540589                // So we'll set a timer here and only report the idle event if we
    541590                // remain idle
    542591                this.StartTimer(1000);
     592               
     593                // TODO: We need to set up a Query that watches own and ally units in LOS
     594                // and calls something if they loose health (or change it to simplify)
     595                // to replace the really UGLY EVIL HACK using GlobalHealthChanged as that can
     596                // have a performance impact as it gets called for each healer for every entity
     597                // in the whole map.
     598               
     599                // If a unit can heal and attack we first want to heal wounded units,
     600                // so check if we are a healer and find whether there's anybody nearby to heal.
     601                // If anyone approaches later it'll be handled via LosRangeUpdate.)
     602                if (this.IsHealer() && this.FindNewHealTargets())
     603                    return true; // (abort the FSM transition since we may have already switched state)
    543604
    544605                // If we entered the idle state we must have nothing better to do,
    545606                // so immediately check whether there's anybody nearby to attack.
    546607                // (If anyone approaches later, it'll be handled via LosRangeUpdate.)
    547608                if (this.FindNewTargets())
    548609                    return true; // (abort the FSM transition since we may have already switched state)
    549 
     610               
    550611                // Nobody to attack - stay in idle
    551612                return false;
    552613            },
     
    554615            "leave": function() {
    555616                var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    556617                rangeMan.DisableActiveQuery(this.losRangeQuery);
     618                if (this.losHealRangeQuery)
     619                    rangeMan.DisableActiveQuery(this.losHealRangeQuery);
    557620
    558621                this.StopTimer();
    559622
     
    563626                    Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
    564627                }
    565628            },
    566 
     629           
     630            // TODO: This is part of an really UGLY EVIL HACK
     631            "GlobalHealthChanged": function() {
     632                if (this.IsHealer())
     633                {
     634                    this.FindNewHealTargets();
     635                }
     636            },
     637           
    567638            "LosRangeUpdate": function(msg) {
     639                if (this.IsHealer())
     640                {
     641                    // Start healing one of the newly-seen own or ally units (if any)
     642                    this.FindNewHealTargets();
     643                }
    568644                if (this.GetStance().targetVisibleEnemies)
    569645                {
    570646                    // Start attacking one of the newly-seen enemy (if any)
     
    9941070            },
    9951071        },
    9961072
     1073        "HEAL": {
     1074            "EntityRenamed": function(msg) {
     1075                if (this.order.data.target == msg.entity)
     1076                    this.order.data.target = msg.newentity;
     1077            },
     1078
     1079            "Attacked": function(msg) {
     1080                // If we stand ground we will rather die than flee
     1081                if (!this.GetStance().respondStandGround)
     1082                    this.Flee(msg.data.attacker, false);
     1083            },
     1084
     1085            "APPROACHING": {
     1086                "enter": function () {
     1087                    this.SelectAnimation("move");
     1088                    this.StartTimer(1000, 1000);
     1089                },
     1090
     1091                "leave": function() {
     1092                    this.StopTimer();
     1093                },
     1094
     1095                "Timer": function(msg) {
     1096                    if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
     1097                    {
     1098                        this.StopMoving();
     1099                        this.FinishOrder();
     1100
     1101                        // Return to our original position
     1102                        if (this.GetStance().respondHoldGround)
     1103                            this.WalkToHeldPosition();
     1104                    }
     1105                },
     1106
     1107                "MoveCompleted": function() {
     1108                    this.SetNextState("HEALING");
     1109                },
     1110
     1111                "Attacked": function(msg) {
     1112                    // If we stand ground we will rather die than flee
     1113                    if (!this.GetStance().respondStandGround)
     1114                        this.Flee(msg.data.attacker, false);
     1115                },
     1116            },
     1117
     1118            "HEALING": {
     1119                "enter": function() {
     1120                    var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     1121                    this.healTimers = cmpHeal.GetTimers();
     1122//                  this.SelectAnimation("heal", false, 1.0, "heal"); // TODO needs animation
     1123//                  this.SetAnimationSync(this.healTimers.prepare, this.healTimers.repeat);
     1124                    this.StartTimer(this.healTimers.prepare, this.healTimers.repeat);
     1125                    // TODO if .prepare is short, players can cheat by cycling heal/stop/heal
     1126                    // to beat the .repeat time; should enforce a minimum time
     1127                    // see comment in ATTACKING.enter
     1128                    this.FaceTowardsTarget(this.order.data.target);
     1129                },
     1130
     1131                "leave": function() {
     1132                    this.StopTimer();
     1133                },
     1134
     1135                "Timer": function(msg) {
     1136                    var target = this.order.data.target;
     1137                    // Check the target is still alive and healable
     1138                    if (this.TargetIsAlive(target) && this.CanHeal(target))
     1139                    {
     1140                        // Check if we can still reach the target
     1141                        if (this.CheckTargetRange(target, IID_Heal))
     1142                        {
     1143                            this.FaceTowardsTarget(target);
     1144                            var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     1145                            cmpHeal.PerformHeal(target);
     1146                            return;
     1147                        }
     1148                        // Can't reach it - try to chase after it
     1149                        if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
     1150                        {
     1151                            if (this.MoveToTargetRange(target, IID_Heal))
     1152                            {
     1153                                this.SetNextState("HEAL.CHASING");
     1154                                return;
     1155                            }
     1156                        }
     1157                    }
     1158                    // Can't reach it, healed to max hp or doesn't exist any more - give up
     1159                    if (this.FinishOrder())
     1160                        return;
     1161
     1162                    // Heal another one
     1163                    if (this.FindNewHealTargets())
     1164                        return;
     1165                   
     1166                    // Return to our original position
     1167                    if (this.GetStance().respondHoldGround)
     1168                        this.WalkToHeldPosition();
     1169                },
     1170                "Attacked": function(msg) {
     1171                    // If we stand ground we will rather die than flee
     1172                    if (!this.GetStance().respondStandGround)
     1173                        this.Flee(msg.data.attacker, false);
     1174                },
     1175            },
     1176            "CHASING": {
     1177                "enter": function () {
     1178                    this.SelectAnimation("move");
     1179                    this.StartTimer(1000, 1000);
     1180                },
     1181
     1182                "leave": function () {
     1183                    this.StopTimer();
     1184                },
     1185                "Timer": function(msg) {
     1186                    if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
     1187                    {
     1188                        this.StopMoving();
     1189                        this.FinishOrder();
     1190
     1191                        // Return to our original position
     1192                        if (this.GetStance().respondHoldGround)
     1193                            this.WalkToHeldPosition();
     1194                    }
     1195                },
     1196                "MoveCompleted": function () {
     1197                    this.SetNextState("HEALING");
     1198                },
     1199            }, 
     1200        },
     1201
    9971202        // Returning to dropsite
    9981203        "RETURNRESOURCE": {
    9991204            "APPROACHING": {
     
    14211626    return (this.template.NaturalBehaviour ? true : false);
    14221627};
    14231628
     1629UnitAI.prototype.IsHealer = function()
     1630{
     1631    return Engine.QueryInterface(this.entity, IID_Heal);
     1632};
     1633
    14241634UnitAI.prototype.IsIdle = function()
    14251635{
    14261636    return this.isIdle;
     
    14441654UnitAI.prototype.OnOwnershipChanged = function(msg)
    14451655{
    14461656    this.SetupRangeQuery();
     1657    if (this.IsHealer())
     1658        this.SetupHealRangeQuery();
    14471659};
    14481660
    14491661UnitAI.prototype.OnDestroy = function()
     
    14551667    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    14561668    if (this.losRangeQuery)
    14571669        rangeMan.DestroyActiveQuery(this.losRangeQuery);
     1670    if (this.losHealRangeQuery)
     1671        rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
    14581672};
    14591673
    14601674// Set up a range query for all enemy units within LOS range
     
    14931707    rangeMan.EnableActiveQuery(this.losRangeQuery);
    14941708};
    14951709
     1710// Set up a range query for all own or ally units within LOS range
     1711// which can be healed.
     1712// This should be called whenever our ownership changes.
     1713UnitAI.prototype.SetupHealRangeQuery = function()
     1714{
     1715    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     1716    var owner = cmpOwnership.GetOwner();
     1717
     1718    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     1719    var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     1720
     1721    if (this.losHealRangeQuery)
     1722        rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
     1723
     1724    var players = [owner];
     1725
     1726    if (owner != -1)
     1727    {
     1728        // If unit not just killed, get ally players via diplomacy
     1729        var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
     1730        var numPlayers = playerMan.GetNumPlayers();
     1731        for (var i = 1; i < numPlayers; ++i)
     1732        {
     1733            // Exclude gaia and enemies
     1734            if (cmpPlayer.IsAlly(i))
     1735                players.push(i);
     1736        }
     1737    }
     1738
     1739    var range = this.GetQueryRange(true);
     1740
     1741    this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health);
     1742    rangeMan.EnableActiveQuery(this.losHealRangeQuery);
     1743};
     1744
    14961745//// FSM linkage functions ////
    14971746
    14981747UnitAI.prototype.SetNextState = function(state)
     
    17141963
    17151964UnitAI.prototype.OnRangeUpdate = function(msg)
    17161965{
    1717     if (msg.tag == this.losRangeQuery)
     1966    if (msg.tag == this.losRangeQuery || msg.tag == this.losHealRangeQuery)
    17181967        UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
    17191968};
    17201969
     1970// really UGLY EVIL HACK
     1971// TODO This should be replaced with a c++ component that tracks all entities within a range and notifies
     1972// Healers if any of the entities receives a HealthChanged message.
     1973// If any entity changes health we call this, so this probably has a huge performance impact.
     1974UnitAI.prototype.OnGlobalHealthChanged = function(msg)
     1975{
     1976    if (this.IsHealer())
     1977    {
     1978        UnitFsm.ProcessMessage(this, {"type": "GlobalHealthChanged"});
     1979    }
     1980}
     1981
    17211982//// Helper functions to be called by the FSM ////
    17221983
    17231984UnitAI.prototype.GetWalkSpeed = function()
     
    17452006};
    17462007
    17472008/**
     2009 * Returns true if the target exists and the current hitpoints are at maximum.
     2010 */
     2011UnitAI.prototype.TargetIsAtMaxHitpoints = function(ent)
     2012{
     2013    var cmpHealth = Engine.QueryInterface(ent, IID_Health);
     2014    if (!cmpHealth)
     2015        return false;
     2016
     2017    return (cmpHealth.GetHitpoints() == cmpHealth.GetMaxHitpoints());
     2018};
     2019
     2020/**
     2021 * Returns true if the target exists and is unhealable.
     2022 */
     2023UnitAI.prototype.TargetIsUnhealable = function(ent)
     2024{
     2025    var cmpHealth = Engine.QueryInterface(ent, IID_Health);
     2026    if (!cmpHealth)
     2027        return false;
     2028
     2029    return cmpHealth.IsUnhealable();
     2030};
     2031
     2032/**
    17482033 * Returns true if the target exists and needs to be killed before
    17492034 * beginning to gather resources from it.
    17502035 */
     
    22352520        case "Flee":
    22362521        case "LeaveFoundation":
    22372522        case "Attack":
     2523        case "Heal":
    22382524        case "Gather":
    22392525        case "ReturnResource":
    22402526        case "Repair":
     
    23582644    this.AddOrder("GatherNearPosition", { "type": type, "x": x, "z": z }, queued);
    23592645}
    23602646
     2647UnitAI.prototype.Heal = function(target, queued)
     2648{
     2649    if (!this.CanHeal(target))
     2650    {
     2651        this.WalkToTarget(target, queued);
     2652        return;
     2653    }
     2654   
     2655    this.AddOrder("Heal", { "target": target, "force": true }, queued);
     2656};
     2657
    23612658UnitAI.prototype.ReturnResource = function(target, queued)
    23622659{
    23632660    if (!this.CanReturnResource(target, true))
     
    23962693        this.stance = stance;
    23972694    else
    23982695        error("UnitAI: Setting to invalid stance '"+stance+"'");
    2399 }
     2696};
    24002697
    24012698UnitAI.prototype.SwitchToStance = function(stance)
    24022699{
     
    24122709
    24132710    // Reset the range query, since the range depends on stance
    24142711    this.SetupRangeQuery();
    2415 }
     2712    // Just if we are a healer
     2713    // TODO maybe move those two to a SetupRangeQuerys()
     2714    if (this.IsHealer())
     2715        this.SetupHealRangeQuery();
     2716};
    24162717
    24172718/**
    24182719 * Resets losRangeQuery, and if there are some targets in range that we can
     
    24332734    return this.RespondToTargetedEntities(ents);
    24342735};
    24352736
    2436 UnitAI.prototype.GetQueryRange = function()
     2737/**
     2738 * Resets losHealRangeQuery, and if there are some targets in range that we can heal
     2739 * then we start healing and this returns true; otherwise, returns false.
     2740 */
     2741UnitAI.prototype.FindNewHealTargets = function()
     2742{
     2743    if (!this.losHealRangeQuery)
     2744        return false;
     2745   
     2746    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     2747    var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery);
     2748   
     2749    for each (var ent in ents)
     2750    {
     2751        if (this.CanHeal(ent))
     2752        {
     2753            this.PushOrderFront("Heal", { "target": ent, "force": false });
     2754            return true;
     2755        }
     2756    }
     2757    // We haven't found any target to heal
     2758    return false;
     2759};
     2760
     2761UnitAI.prototype.GetQueryRange = function(healer)
    24372762{
    24382763    var ret = { "min": 0, "max": 0 };
    2439     if (this.GetStance().respondStandGround)
     2764    // If we have a healer with passive stance we need to set some defaults (eg same as stand ground)
     2765    if (this.GetStance().respondStandGround || (healer && !this.GetStance().respondStandGround && !this.GetStance().respondChase && !this.GetStance().respondHoldGround))
    24402766    {
    2441         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2767        var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack);
    24422768        if (!cmpRanged)
    24432769            return ret;
    2444         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2770        var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack());
    24452771        ret.min = range.min;
    24462772        ret.max = range.max;
    24472773    }
     
    24552781    }
    24562782    else if (this.GetStance().respondHoldGround)
    24572783    {
    2458         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2784        var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack);
    24592785        if (!cmpRanged)
    24602786            return ret;
    2461         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2787        var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack());
    24622788        var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
    24632789        if (!cmpVision)
    24642790            return ret;
     
    25542880    return true;
    25552881};
    25562882
     2883UnitAI.prototype.CanHeal = function(target)
     2884{
     2885    // Formation controllers should always respond to commands
     2886    // (then the individual units can make up their own minds)
     2887    if (this.IsFormationController())
     2888        return true;
     2889
     2890    // Verify that we're able to respond to Heal commands
     2891    var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     2892    if (!cmpHeal)
     2893        return false;
     2894
     2895    // Verify that the target is alive
     2896    if (!this.TargetIsAlive(target))
     2897        return false;
     2898
     2899    // Verify that the target is owned by the same player as the entity or of an ally
     2900    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     2901    if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
     2902        return false;
     2903   
     2904    // Verify that the target is not unhealable
     2905    if (this.TargetIsUnhealable(target))
     2906    {
     2907        return false;
     2908    }
     2909   
     2910    // Verify that the target has no unhealable class
     2911    // We could also use cmpIdentity.GetClassesList but this way is cleaner
     2912    var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     2913    if (!cmpIdentity)
     2914        return false;
     2915    for each (var unhealableClass in cmpHeal.GetUnhealableClasses())
     2916    {
     2917        if (cmpIdentity.HasClass(unhealableClass) != -1)
     2918        {
     2919            return false;
     2920        }
     2921    }
     2922
     2923    // Verify that the target is a healable class
     2924    // We could also use cmpIdentity.GetClassesList but this way is cleaner
     2925    var healable = false;
     2926    for each (var healableClass in cmpHeal.GetHealableClasses())
     2927    {
     2928        if (cmpIdentity.HasClass(healableClass) != -1)
     2929        {
     2930            healable = true;
     2931        }
     2932    }
     2933    if (!healable)
     2934        return false;
     2935
     2936    // Check that the target is not at MaxHealth
     2937    if (this.TargetIsAtMaxHitpoints(target))
     2938        return false;
     2939
     2940    return true;
     2941};
     2942
    25572943UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
    25582944{
    25592945    // Formation controllers should always respond to commands
  • binaries/data/mods/public/simulation/components/interfaces/Heal.js

     
     1Engine.RegisterInterface("Heal");
  • binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js

     
    55Engine.LoadComponentScript("interfaces/DamageReceiver.js");
    66Engine.LoadComponentScript("interfaces/Foundation.js");
    77Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
     8Engine.LoadComponentScript("interfaces/Heal.js");
    89Engine.LoadComponentScript("interfaces/Health.js");
    910Engine.LoadComponentScript("interfaces/Promotion.js");
    1011Engine.LoadComponentScript("interfaces/RallyPoint.js");
     
    270271    GetHitpoints: function() { return 50; },
    271272    GetMaxHitpoints: function() { return 60; },
    272273    IsRepairable: function() { return false; },
     274    IsUnhealable: function() { return false; },
    273275});
    274276
    275277AddMock(10, IID_Identity, {
     
    303305    hitpoints: 50,
    304306    maxHitpoints: 60,
    305307    needsRepair: false,
     308    needsHeal: true,
    306309    buildEntities: ["test1", "test2"],
    307310    barterMarket: {
    308311        prices: { "buy": {"food":150}, "sell": {"food":25} },
  • binaries/data/mods/public/simulation/components/tests/test_UnitAI.js

     
    44Engine.LoadComponentScript("interfaces/Attack.js");
    55Engine.LoadComponentScript("interfaces/DamageReceiver.js");
    66Engine.LoadComponentScript("interfaces/Formation.js");
     7Engine.LoadComponentScript("interfaces/Heal.js");
    78Engine.LoadComponentScript("interfaces/Health.js");
    89Engine.LoadComponentScript("interfaces/ResourceSupply.js");
    910Engine.LoadComponentScript("interfaces/Timer.js");
  • binaries/data/mods/public/simulation/helpers/Commands.js

     
    6666        });
    6767        break;
    6868
     69    case "heal":
     70        if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
     71        {
     72            // This check is for debugging only!
     73            warn("Invalid command: heal target is not owned by an ally of or player "+player+" itself: "+uneval(cmd));
     74        }
     75
     76        // See UnitAI.CanHeal for target checks
     77        var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
     78        GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
     79            cmpUnitAI.Heal(cmd.target, cmd.queued);
     80        });
     81        break;
     82
    6983    case "repair":
    7084        // This covers both repairing damaged buildings, and constructing unfinished foundations
    7185        if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
  • binaries/data/mods/public/simulation/templates/template_structure.xml

     
    3333  <Health>
    3434    <DeathType>corpse</DeathType>
    3535    <RegenRate>0</RegenRate>
    36     <Healable>false</Healable>
     36    <Unhealable>true</Unhealable>
    3737    <Repairable>true</Repairable>
    3838  </Health>
    3939  <Identity>
  • binaries/data/mods/public/simulation/templates/template_unit.xml

     
    3030    <DeathType>corpse</DeathType>
    3131    <Max>100</Max>
    3232    <RegenRate>0</RegenRate>
    33     <Healable>true</Healable>
     33    <Unhealable>false</Unhealable>
    3434    <Repairable>false</Repairable>
    3535  </Health>
    3636  <Identity>
  • binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml

     
    44    <Max>100</Max>
    55    <DeathType>remain</DeathType>
    66    <RegenRate>1</RegenRate>
    7     <Healable>false</Healable>
     7    <Unhealable>true</Unhealable>
    88    <Repairable>false</Repairable>
    99  </Health>
    1010  <Identity>
  • binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml

     
    77    <SinkAccel>2.0</SinkAccel>
    88  </Decay>
    99  <Health>
    10     <Healable>false</Healable>
     10    <Unhealable>true</Unhealable>
    1111    <Repairable>true</Repairable>
    1212  </Health>
    1313  <Identity>
  • binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml

     
    55    <Pierce>2.0</Pierce>
    66    <Crush>2.0</Crush>
    77  </Armour>
    8   <Auras>
     8<!--  <Auras>
    99    <Heal>
    1010      <Radius>20</Radius>
    1111      <Speed>2000</Speed>
    1212    </Heal>
    13   </Auras>
     13  </Auras>-->
     14  <Heal>
     15    <Range>30</Range>
     16    <HP>5</HP>
     17    <Rate>2000</Rate>
     18    <UnhealableClasses datatype="tokens"/>
     19    <HealableClasses datatype="tokens">Support Infantry Cavalry</HealableClasses>
     20  </Heal>
    1421  <Cost>
    1522    <Resources>
    1623      <metal>120</metal>
     
    2330  <Identity>
    2431    <Classes datatype="tokens">Healer</Classes>
    2532    <GenericName>Healer</GenericName>
    26     <Tooltip>Heal units within his Aura. (Not implemented yet)</Tooltip>
     33    <Tooltip>Heal units.</Tooltip>
    2734  </Identity>
     35  <Promotion>
     36    <RequiredXp>100</RequiredXp>
     37  </Promotion>
    2838  <Sound>
    2939    <SoundGroups>
    3040      <select>voice/hellenes/civ/civ_male_select.xml</select>
  • binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml

     
    3636    </Resources>
    3737  </Cost>
    3838  <Health>
    39     <Healable>false</Healable>
     39    <Unhealable>true</Unhealable>
    4040  </Health>
    4141  <Identity>
    4242    <Classes datatype="tokens">Slave</Classes>
  • binaries/data/mods/public/simulation/templates/units/cart_support_healer_b.xml

     
    66    <History>Tanit (also spelled TINITH, TINNIT, or TINT), chief goddess of Carthage, equivalent of Astarte. Although she seems to have had some connection with the heavens, she was also a mother goddess, and fertility symbols often accompany representations of her. She was probably the consort of Baal Hammon (or Amon), the chief god of Carthage, and was often given the attribute "face of Baal." Although Tanit did not appear at Carthage before the 5th century BC, she soon eclipsed the more established cult of Baal Hammon and, in the Carthaginian area at least, was frequently listed before him on the monuments. In the worship of Tanit and Baal Hammon, children, probably firstborn, were sacrificed. Ample evidence of the practice has been found west of Carthage in the precinct of Tanit, where a tofet (a sanctuary for the sacrifice of children) was discovered. Tanit was also worshipped on Malta, Sardinia, and in Spain. There is no other reason for giving the Carthaginians a priestess instead of a priest in 0 A.D., although Tanit was the most popular of their two main gods with the people. </History>
    77    <Tooltip>Heal units within her aura. (Not implemented yet)</Tooltip>
    88    <Icon>units/cart_support_healer.png</Icon>
     9    <Rank>Basic</Rank>
    910  </Identity>
     11  <Promotion>
     12    <Entity>units/cart_support_healer_a</Entity>
     13  </Promotion>
    1014  <VisualActor>
    1115    <Actor>units/carthaginians/healer.xml</Actor>
    1216  </VisualActor>
  • binaries/data/mods/public/simulation/templates/units/celt_support_healer_b.xml

     
    55    <SpecificName>Druides </SpecificName>
    66    <History>A druid may be one of many different professions; priest, historian, lawyer, judges, teachers, philosophers, poets, composers, musicians, astronomers, prophets, councillors, high craftsmen like a blacksmith, the classes of the 'men of art', and sometimes kings, chieftains, or other politicians. Druids were very hierarchal, with classes and ranks based on the length of their education and what fields they practiced. They learned their trades through mnemonics by way of poetry and songs, as writing was rarely used by Celts outside of prayers on votive objects, or lists of names for migratory records.</History>
    77    <Icon>units/celt_support_healer.png</Icon>
     8    <Rank>Basic</Rank>
    89  </Identity>
     10  <Promotion>
     11    <Entity>units/celt_support_healer_a</Entity>
     12  </Promotion>
    913  <VisualActor>
    1014    <Actor>units/celts/healer.xml</Actor>
    1115  </VisualActor>
  • binaries/data/mods/public/simulation/templates/units/hele_support_healer_b.xml

     
    55    <SpecificName>Hiereús</SpecificName>
    66    <History>The art of medicine was widely practised in Classical Greece. Hippocrates was the first physician to separate religion and superstition from actual medicine, and many others followed his lead.</History>
    77    <Icon>units/hele_support_healer.png</Icon>
     8    <Rank>Basic</Rank>
    89  </Identity>
     10  <Promotion>
     11    <Entity>units/hele_support_healer_a</Entity>
     12  </Promotion>
    913  <VisualActor>
    1014    <Actor>units/hellenes/healer.xml</Actor>
    1115  </VisualActor>
  • binaries/data/mods/public/simulation/templates/units/iber_support_healer_b.xml

     
    55    <SpecificName>Sacerdotisa de Ataekina</SpecificName>
    66    <History> To the best of our knowledge, only one 'temple'-like structure has been found on the Iberian Peninsula dating from the times and the Iberians worshiped their pantheon of gods at small home altars; however, a very special sculptured head and torso was found in a farmer's field around the turn of the 20th century of a personage who was obviously someone of great substance. As the two principal gods, of the many worshiped, were male Endovellikos and female Ataekina, we thought it would be nice to adopt The Lady of Elche as our priestess-healer representing Ataekina. We know from archelogy and the Romans that Ataekina was associated with spring, the changing of seasons, and nature in general. Ataekina also seems to have been associated with the cycle of birth-death-rebirth.</History>
    77    <Icon>units/iber_support_healer.png</Icon>
     8    <Rank>Basic</Rank>
    89  </Identity>
     10  <Promotion>
     11    <Entity>units/iber_support_healer_a</Entity>
     12  </Promotion>
    913  <VisualActor>
    1014    <Actor>units/iberians/healer.xml</Actor>
    1115  </VisualActor>
  • binaries/data/mods/public/simulation/templates/units/pers_support_healer_b.xml

     
    66    <SpecificName>Maguš Mada</SpecificName>
    77    <History>Under both the Medes and later the Persian the tribe of the Magi or the Magians were the masters of religious and oral tradition, comparable to the Levites of the Bible. They were connected to Zoroastrianism, but likely tended to other Iranian cults as well. Aside from religious duties the Magians also functioned as the Great King's bureaucrats and kept his administration running.</History>
    88    <Icon>units/pers_support_healer.png</Icon>
     9    <Rank>Basic</Rank>
    910  </Identity>
     11  <Promotion>
     12    <Entity>units/pers_support_healer_a</Entity>
     13  </Promotion>
    1014  <VisualActor>
    1115    <Actor>units/persians/healer.xml</Actor>
    1216  </VisualActor>
  • binaries/data/mods/public/simulation/templates/units/rome_support_healer_b.xml

     
    66    <SpecificName>Pontifex Minoris</SpecificName>
    77    <History>During the Republic, the position of priest was elevated and required a lot of responsibilities, which is why priests were by no means chosen randomly. The position of Pontifex Maximus, the high priest of the Roman religion, was occupied by such prominent figures as Julius Caesar, Marcus Aemilius Lepidus and Augustus.</History>
    88    <Icon>units/rome_support_healer.png</Icon>
     9    <Rank>Basic</Rank>
    910  </Identity>
     11  <Promotion>
     12    <Entity>units/rome_support_healer_a</Entity>
     13  </Promotion>
    1014  <VisualActor>
    1115    <Actor>units/romans/healer.xml</Actor>
    1216  </VisualActor>