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

File #999-2012-03-08.patch, 46.9 KB (added by leper, 12 years ago)

Fixes conflicts caused by 11279 (#30).

  • 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;
     
    297298                return {"possible": true, "tooltip": tooltip};
    298299            }
    299300            break;
     301        case "heal":
     302            // The check if the target is unhealable is done by targetState.needsHeal
     303            if (entState.Ability && isUnit(targetState) && targetState.needsHeal && (playerOwned || allyOwned))
     304            {
     305                var unhealableClasses = entState.Ability.Healer.unhealableClasses;
     306                for each (var unitClass in targetState.identity.classes)
     307                {
     308                    if (unhealableClasses.indexOf(unitClass) != -1)
     309                    {
     310                        return {"possible": false};
     311                    }
     312                }
     313               
     314                var healableClasses = entState.Ability.Healer.healableClasses;
     315                for each (var unitClass in targetState.identity.classes)
     316                {
     317                    if (healableClasses.indexOf(unitClass) != -1)
     318                    {
     319                        return {"possible": true};
     320                    }
     321                }
     322            }
     323            break;
    300324        case "gather":
    301325            if (targetState.resourceSupply && (playerOwned || gaiaOwned))
    302326            {
     
    393417            else
    394418                return {"type": "none", "cursor": "action-repair-disabled", "target": undefined};
    395419            break;
     420        case ACTION_HEAL:
     421            if (getActionInfo("heal", target).possible)
     422                return {"type": "heal", "cursor": "action-heal", "target": target};
     423            else
     424                return {"type": "none", "cursor": "action-heal-disabled", "target": undefined};
     425            break;
    396426        }
    397427    }
    398428    else if (Engine.HotkeyIsPressed("session.garrison"))
     
    417447            return {"type": "build", "cursor": "action-repair", "target": target};
    418448        else if ((actionInfo = getActionInfo("set-rallypoint", target)).possible)
    419449            return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "position": actionInfo.position};
     450        else if (getActionInfo("heal", target).possible)
     451            return {"type": "heal", "cursor": "action-heal", "target": target};
    420452        else if (getActionInfo("attack", target).possible)
    421453            return {"type": "attack", "cursor": "action-attack", "target": target};
    422454        else if (getActionInfo("unset-rallypoint", target).possible)
     
    10371069        Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
    10381070        return true;
    10391071
     1072    case "promote":
     1073        Engine.PostNetworkCommand({"type": "promote", "entities": selection, "target": action.target, "queued": queued});
     1074        // TODO:Play a sound?
     1075        return true;
     1076
     1077    case "heal":
     1078        Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued});
     1079        // TODO: Play a sound?
     1080//      Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] });
     1081        return true;
     1082
    10401083    case "build": // (same command as repair)
    10411084    case "repair":
    10421085        Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
     
    12581301                inputState = INPUT_PRESELECTEDACTION;
    12591302                preSelectedAction = ACTION_REPAIR;
    12601303                break;
     1304            case "heal":
     1305                inputState = INPUT_PRESELECTEDACTION;
     1306                preSelectedAction = ACTION_HEAL;
     1307                break;
     1308            case "promote":
     1309                doAction({ "type": "promote"})
     1310                break;
    12611311            case "unload-all":
    12621312                unloadAll(entity);
    12631313                break;
  • binaries/data/mods/public/gui/session/session.xml

     
    729729                    </object>
    730730                </object>
    731731
     732                <object name="unitAbilityPanel"
     733                    size="14 12 100% 100%"
     734                >
     735                    <object size="0 0 100% 100%">
     736                        <repeat count="24">
     737                            <object name="unitAbilityButton[n]" hidden="true" style="iconButton" type="button" size="0 0 46 46" tooltip_style="sessionToolTipBottom">
     738                                <object name="unitAbilityIcon[n]" type="image" ghost="true" size="3 3 43 43"/>
     739                            </object>
     740                        </repeat>
     741                    </object>
     742                </object>
     743
    732744                <object name="unitResearchPanel"
    733745                    style="TranslucentPanelThinBorder"
    734746                    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
     
    2223const BARTER_RESOURCES = ["food", "wood", "stone", "metal"];
    2324const BARTER_ACTIONS = ["Sell", "Buy"];
    2425
    25 // The number of currently visible buttons (used to optimise showing/hiding)
    26 var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Barter": 0, "Trading": 0, "Construction": 0, "Command": 0, "Stance": 0};
     26// The number of currently visible buttons (used to optimise showing/hiding)
     27var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Barter": 0, "Trading": 0, "Construction": 0, "Ability": 0, "Command": 0, "Stance": 0};
    2728
    2829// Unit panels are panels with row(s) of buttons
    29 var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Training", "Barter", "Trading", "Construction", "Research", "Stance", "Command"];
     30var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Training", "Barter", "Trading", "Construction", "Ability", "Research", "Stance", "Command"];
    3031
    3132// Indexes of resources to sell and buy on barter panel
    3233var g_barterSell = 0;
     
    178179                numberOfItems =  24;
    179180            break;
    180181
     182        case ABILITY:
     183            if (numberOfItems > 24)
     184                numberOfItems = 24;
     185            break;
     186
    181187        case COMMAND:
    182188            if (numberOfItems > 6)
    183189                numberOfItems = 6;
     
    194200        var item = items[i];
    195201        var entType = ((guiName == "Queue")? item.template : item);
    196202        var template;
    197         if (guiName != "Formation" && guiName != "Command" && guiName != "Stance")
     203        if (guiName != "Formation" && guiName != "Command" && guiName != "Stance" && guiName != "Ability")
    198204        {
    199205            template = GetTemplateData(entType);
    200206            if (!template)
     
    273279
    274280                break;
    275281
     282            case ABILITY:
     283                // TODO read tooltips from some file or template based on 'item'
     284                var tooltip;
     285                switch(item)
     286                {
     287                    case "heal":
     288                        tooltip = "Heal units";
     289                        break;
     290                    case "promote":
     291                        tooltip = "Promote this unit";
     292                        break;
     293                    default:
     294                        tooltip = "No tooltip defined";
     295                    break;
     296                }
     297                break;
     298
    276299            case COMMAND:
    277300                // here, "item" is an object with properties .name (command name), .tooltip and .icon (relative to session/icons/single)
    278301                if (item.name == "unload-all")
     
    342365            icon.sprite = "stretched:session/icons/single/" + item.icon;
    343366
    344367        }
     368        else if (guiName == "Ability")
     369        {
     370            icon.sprite = "stretched:session/icons/single/"+item+".png";
     371        }
    345372        else if (template.icon)
    346373        {
    347374            icon.sprite = "stretched:session/portraits/" + template.icon;
     
    534561            setupUnitBarterPanel(entState);
    535562        }
    536563
     564        if (entState.Ability)
     565        {
     566            var abilities = [];
     567            if (entState.Ability.Healer)
     568                abilities.push("heal");
     569            if (entState.Ability.Promote)
     570                abilities.push("promote");
     571            setupUnitPanel("Ability", usedPanels, entState, abilities, function (item) { performCommand(entState.id, item); });
     572        }
     573
    537574        if (entState.buildEntities && entState.buildEntities.length)
    538575        {
    539576            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);
     
    273274        ret.barterMarket = { "prices": cmpBarter.GetPrices() };
    274275    }
    275276
     277    // Abilities
     278    var cmpHeal = Engine.QueryInterface(ent, IID_Heal);
     279    // Check if we have abilities
     280    if (cmpHeal || (cmpHeal && cmpPromotion))
     281        ret.Ability = [];
     282    if (cmpHeal)
     283    {
     284        ret.Ability.Healer = {
     285            "unhealableClasses": cmpHeal.GetUnhealableClasses(),
     286            "healableClasses": cmpHeal.GetHealableClasses(),
     287        };
     288    }
     289
     290    // TODO remove this; This is just used to test/demonstrate the extensibility
     291    // of the Ability system
     292    // promoteAbility (just for healers)
     293    if (cmpPromotion && cmpHeal)
     294    {
     295        ret.Ability.Promote = true ;
     296    }
     297
    276298    var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    277299    ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
    278300
  • 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
     
    410452            cmpFormation.Disband();
    411453        },
    412454
     455        "Order.Heal": function(msg) {
     456            // TODO: see notes in Order.Attack
     457            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     458            cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]);
     459            cmpFormation.Disband();
     460        },
     461
    413462        "Order.Repair": function(msg) {
    414463            // TODO: see notes in Order.Attack
    415464            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     
    548597                // So we'll set a timer here and only report the idle event if we
    549598                // remain idle
    550599                this.StartTimer(1000);
     600               
     601                // TODO: We need to set up a Query that watches own and ally units in LOS
     602                // and calls something if they loose health (or change it to simplify)
     603                // to replace the really UGLY EVIL HACK using GlobalHealthChanged as that can
     604                // have a performance impact as it gets called for each healer for every entity
     605                // in the whole map.
     606               
     607                // If a unit can heal and attack we first want to heal wounded units,
     608                // so check if we are a healer and find whether there's anybody nearby to heal.
     609                // If anyone approaches later it'll be handled via LosRangeUpdate.)
     610                if (this.IsHealer() && this.FindNewHealTargets())
     611                    return true; // (abort the FSM transition since we may have already switched state)
    551612
    552613                // If we entered the idle state we must have nothing better to do,
    553614                // so immediately check whether there's anybody nearby to attack.
    554615                // (If anyone approaches later, it'll be handled via LosRangeUpdate.)
    555616                if (this.FindNewTargets())
    556617                    return true; // (abort the FSM transition since we may have already switched state)
    557 
     618               
    558619                // Nobody to attack - stay in idle
    559620                return false;
    560621            },
     
    562623            "leave": function() {
    563624                var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    564625                rangeMan.DisableActiveQuery(this.losRangeQuery);
     626                if (this.losHealRangeQuery)
     627                    rangeMan.DisableActiveQuery(this.losHealRangeQuery);
    565628
    566629                this.StopTimer();
    567630
     
    571634                    Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
    572635                }
    573636            },
    574 
     637           
     638            // TODO: This is part of an really UGLY EVIL HACK
     639            "GlobalHealthChanged": function() {
     640                if (this.IsHealer())
     641                {
     642                    this.FindNewHealTargets();
     643                }
     644            },
     645           
    575646            "LosRangeUpdate": function(msg) {
     647                if (this.IsHealer())
     648                {
     649                    // Start healing one of the newly-seen own or ally units (if any)
     650                    this.FindNewHealTargets();
     651                }
    576652                if (this.GetStance().targetVisibleEnemies)
    577653                {
    578654                    // Start attacking one of the newly-seen enemy (if any)
     
    10021078            },
    10031079        },
    10041080
     1081        "HEAL": {
     1082            "EntityRenamed": function(msg) {
     1083                if (this.order.data.target == msg.entity)
     1084                    this.order.data.target = msg.newentity;
     1085            },
     1086
     1087            "Attacked": function(msg) {
     1088                // If we stand ground we will rather die than flee
     1089                if (!this.GetStance().respondStandGround)
     1090                    this.Flee(msg.data.attacker, false);
     1091            },
     1092
     1093            "APPROACHING": {
     1094                "enter": function () {
     1095                    this.SelectAnimation("move");
     1096                    this.StartTimer(1000, 1000);
     1097                },
     1098
     1099                "leave": function() {
     1100                    this.StopTimer();
     1101                },
     1102
     1103                "Timer": function(msg) {
     1104                    if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
     1105                    {
     1106                        this.StopMoving();
     1107                        this.FinishOrder();
     1108
     1109                        // Return to our original position
     1110                        if (this.GetStance().respondHoldGround)
     1111                            this.WalkToHeldPosition();
     1112                    }
     1113                },
     1114
     1115                "MoveCompleted": function() {
     1116                    this.SetNextState("HEALING");
     1117                },
     1118
     1119                "Attacked": function(msg) {
     1120                    // If we stand ground we will rather die than flee
     1121                    if (!this.GetStance().respondStandGround)
     1122                        this.Flee(msg.data.attacker, false);
     1123                },
     1124            },
     1125
     1126            "HEALING": {
     1127                "enter": function() {
     1128                    var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     1129                    this.healTimers = cmpHeal.GetTimers();
     1130//                  this.SelectAnimation("heal", false, 1.0, "heal"); // TODO needs animation
     1131//                  this.SetAnimationSync(this.healTimers.prepare, this.healTimers.repeat);
     1132                    this.StartTimer(this.healTimers.prepare, this.healTimers.repeat);
     1133                    // TODO if .prepare is short, players can cheat by cycling heal/stop/heal
     1134                    // to beat the .repeat time; should enforce a minimum time
     1135                    // see comment in ATTACKING.enter
     1136                    this.FaceTowardsTarget(this.order.data.target);
     1137                },
     1138
     1139                "leave": function() {
     1140                    this.StopTimer();
     1141                },
     1142
     1143                "Timer": function(msg) {
     1144                    var target = this.order.data.target;
     1145                    // Check the target is still alive and healable
     1146                    if (this.TargetIsAlive(target) && this.CanHeal(target))
     1147                    {
     1148                        // Check if we can still reach the target
     1149                        if (this.CheckTargetRange(target, IID_Heal))
     1150                        {
     1151                            this.FaceTowardsTarget(target);
     1152                            var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     1153                            cmpHeal.PerformHeal(target);
     1154                            return;
     1155                        }
     1156                        // Can't reach it - try to chase after it
     1157                        if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
     1158                        {
     1159                            if (this.MoveToTargetRange(target, IID_Heal))
     1160                            {
     1161                                this.SetNextState("HEAL.CHASING");
     1162                                return;
     1163                            }
     1164                        }
     1165                    }
     1166                    // Can't reach it, healed to max hp or doesn't exist any more - give up
     1167                    if (this.FinishOrder())
     1168                        return;
     1169
     1170                    // Heal another one
     1171                    if (this.FindNewHealTargets())
     1172                        return;
     1173                   
     1174                    // Return to our original position
     1175                    if (this.GetStance().respondHoldGround)
     1176                        this.WalkToHeldPosition();
     1177                },
     1178                "Attacked": function(msg) {
     1179                    // If we stand ground we will rather die than flee
     1180                    if (!this.GetStance().respondStandGround)
     1181                        this.Flee(msg.data.attacker, false);
     1182                },
     1183            },
     1184            "CHASING": {
     1185                "enter": function () {
     1186                    this.SelectAnimation("move");
     1187                    this.StartTimer(1000, 1000);
     1188                },
     1189
     1190                "leave": function () {
     1191                    this.StopTimer();
     1192                },
     1193                "Timer": function(msg) {
     1194                    if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
     1195                    {
     1196                        this.StopMoving();
     1197                        this.FinishOrder();
     1198
     1199                        // Return to our original position
     1200                        if (this.GetStance().respondHoldGround)
     1201                            this.WalkToHeldPosition();
     1202                    }
     1203                },
     1204                "MoveCompleted": function () {
     1205                    this.SetNextState("HEALING");
     1206                },
     1207            }, 
     1208        },
     1209
    10051210        // Returning to dropsite
    10061211        "RETURNRESOURCE": {
    10071212            "APPROACHING": {
     
    14571662    return (this.template.NaturalBehaviour ? true : false);
    14581663};
    14591664
     1665UnitAI.prototype.IsHealer = function()
     1666{
     1667    return Engine.QueryInterface(this.entity, IID_Heal);
     1668};
     1669
    14601670UnitAI.prototype.IsIdle = function()
    14611671{
    14621672    return this.isIdle;
     
    14801690UnitAI.prototype.OnOwnershipChanged = function(msg)
    14811691{
    14821692    this.SetupRangeQuery();
     1693    if (this.IsHealer())
     1694        this.SetupHealRangeQuery();
    14831695};
    14841696
    14851697UnitAI.prototype.OnDestroy = function()
     
    14911703    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    14921704    if (this.losRangeQuery)
    14931705        rangeMan.DestroyActiveQuery(this.losRangeQuery);
     1706    if (this.losHealRangeQuery)
     1707        rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
    14941708};
    14951709
    14961710// Set up a range query for all enemy units within LOS range
     
    15291743    rangeMan.EnableActiveQuery(this.losRangeQuery);
    15301744};
    15311745
     1746// Set up a range query for all own or ally units within LOS range
     1747// which can be healed.
     1748// This should be called whenever our ownership changes.
     1749UnitAI.prototype.SetupHealRangeQuery = function()
     1750{
     1751    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     1752    var owner = cmpOwnership.GetOwner();
     1753
     1754    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     1755    var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     1756
     1757    if (this.losHealRangeQuery)
     1758        rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
     1759
     1760    var players = [owner];
     1761
     1762    if (owner != -1)
     1763    {
     1764        // If unit not just killed, get ally players via diplomacy
     1765        var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
     1766        var numPlayers = playerMan.GetNumPlayers();
     1767        for (var i = 1; i < numPlayers; ++i)
     1768        {
     1769            // Exclude gaia and enemies
     1770            if (cmpPlayer.IsAlly(i))
     1771                players.push(i);
     1772        }
     1773    }
     1774
     1775    var range = this.GetQueryRange(true);
     1776
     1777    this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health);
     1778    rangeMan.EnableActiveQuery(this.losHealRangeQuery);
     1779};
     1780
    15321781//// FSM linkage functions ////
    15331782
    15341783UnitAI.prototype.SetNextState = function(state)
     
    17501999
    17512000UnitAI.prototype.OnRangeUpdate = function(msg)
    17522001{
    1753     if (msg.tag == this.losRangeQuery)
     2002    if (msg.tag == this.losRangeQuery || msg.tag == this.losHealRangeQuery)
    17542003        UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
    17552004};
    17562005
     2006// really UGLY EVIL HACK
     2007// TODO This should be replaced with a c++ component that tracks all entities within a range and notifies
     2008// Healers if any of the entities receives a HealthChanged message.
     2009// If any entity changes health we call this, so this probably has a huge performance impact.
     2010UnitAI.prototype.OnGlobalHealthChanged = function(msg)
     2011{
     2012    if (this.IsHealer())
     2013    {
     2014        UnitFsm.ProcessMessage(this, {"type": "GlobalHealthChanged"});
     2015    }
     2016}
     2017
    17572018//// Helper functions to be called by the FSM ////
    17582019
    17592020UnitAI.prototype.GetWalkSpeed = function()
     
    17812042};
    17822043
    17832044/**
     2045 * Returns true if the target exists and the current hitpoints are at maximum.
     2046 */
     2047UnitAI.prototype.TargetIsAtMaxHitpoints = function(ent)
     2048{
     2049    var cmpHealth = Engine.QueryInterface(ent, IID_Health);
     2050    if (!cmpHealth)
     2051        return false;
     2052
     2053    return (cmpHealth.GetHitpoints() == cmpHealth.GetMaxHitpoints());
     2054};
     2055
     2056/**
     2057 * Returns true if the target exists and is unhealable.
     2058 */
     2059UnitAI.prototype.TargetIsUnhealable = function(ent)
     2060{
     2061    var cmpHealth = Engine.QueryInterface(ent, IID_Health);
     2062    if (!cmpHealth)
     2063        return false;
     2064
     2065    return cmpHealth.IsUnhealable();
     2066};
     2067
     2068/**
    17842069 * Returns true if the target exists and needs to be killed before
    17852070 * beginning to gather resources from it.
    17862071 */
     
    22712556        case "Flee":
    22722557        case "LeaveFoundation":
    22732558        case "Attack":
     2559        case "Heal":
    22742560        case "Gather":
    22752561        case "ReturnResource":
    22762562        case "Repair":
     
    23942680    this.AddOrder("GatherNearPosition", { "type": type, "x": x, "z": z }, queued);
    23952681}
    23962682
     2683UnitAI.prototype.Heal = function(target, queued)
     2684{
     2685    if (!this.CanHeal(target))
     2686    {
     2687        this.WalkToTarget(target, queued);
     2688        return;
     2689    }
     2690   
     2691    this.AddOrder("Heal", { "target": target, "force": true }, queued);
     2692};
     2693
    23972694UnitAI.prototype.ReturnResource = function(target, queued)
    23982695{
    23992696    if (!this.CanReturnResource(target, true))
     
    25052802        this.stance = stance;
    25062803    else
    25072804        error("UnitAI: Setting to invalid stance '"+stance+"'");
    2508 }
     2805};
    25092806
    25102807UnitAI.prototype.SwitchToStance = function(stance)
    25112808{
     
    25212818
    25222819    // Reset the range query, since the range depends on stance
    25232820    this.SetupRangeQuery();
    2524 }
     2821    // Just if we are a healer
     2822    // TODO maybe move those two to a SetupRangeQuerys()
     2823    if (this.IsHealer())
     2824        this.SetupHealRangeQuery();
     2825};
    25252826
    25262827/**
    25272828 * Resets losRangeQuery, and if there are some targets in range that we can
     
    25422843    return this.RespondToTargetedEntities(ents);
    25432844};
    25442845
    2545 UnitAI.prototype.GetQueryRange = function()
     2846/**
     2847 * Resets losHealRangeQuery, and if there are some targets in range that we can heal
     2848 * then we start healing and this returns true; otherwise, returns false.
     2849 */
     2850UnitAI.prototype.FindNewHealTargets = function()
     2851{
     2852    if (!this.losHealRangeQuery)
     2853        return false;
     2854   
     2855    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     2856    var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery);
     2857   
     2858    for each (var ent in ents)
     2859    {
     2860        if (this.CanHeal(ent))
     2861        {
     2862            this.PushOrderFront("Heal", { "target": ent, "force": false });
     2863            return true;
     2864        }
     2865    }
     2866    // We haven't found any target to heal
     2867    return false;
     2868};
     2869
     2870UnitAI.prototype.GetQueryRange = function(healer)
    25462871{
    25472872    var ret = { "min": 0, "max": 0 };
    2548     if (this.GetStance().respondStandGround)
     2873    // If we have a healer with passive stance we need to set some defaults (eg same as stand ground)
     2874    if (this.GetStance().respondStandGround || (healer && !this.GetStance().respondStandGround && !this.GetStance().respondChase && !this.GetStance().respondHoldGround))
    25492875    {
    2550         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2876        var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack);
    25512877        if (!cmpRanged)
    25522878            return ret;
    2553         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2879        var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack());
    25542880        ret.min = range.min;
    25552881        ret.max = range.max;
    25562882    }
     
    25642890    }
    25652891    else if (this.GetStance().respondHoldGround)
    25662892    {
    2567         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2893        var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack);
    25682894        if (!cmpRanged)
    25692895            return ret;
    2570         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2896        var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack());
    25712897        var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
    25722898        if (!cmpVision)
    25732899            return ret;
     
    26632989    return true;
    26642990};
    26652991
     2992UnitAI.prototype.CanHeal = function(target)
     2993{
     2994    // Formation controllers should always respond to commands
     2995    // (then the individual units can make up their own minds)
     2996    if (this.IsFormationController())
     2997        return true;
     2998
     2999    // Verify that we're able to respond to Heal commands
     3000    var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     3001    if (!cmpHeal)
     3002        return false;
     3003
     3004    // Verify that the target is alive
     3005    if (!this.TargetIsAlive(target))
     3006        return false;
     3007
     3008    // Verify that the target is owned by the same player as the entity or of an ally
     3009    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     3010    if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
     3011        return false;
     3012   
     3013    // Verify that the target is not unhealable
     3014    if (this.TargetIsUnhealable(target))
     3015    {
     3016        return false;
     3017    }
     3018   
     3019    // Verify that the target has no unhealable class
     3020    // We could also use cmpIdentity.GetClassesList but this way is cleaner
     3021    var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     3022    if (!cmpIdentity)
     3023        return false;
     3024    for each (var unhealableClass in cmpHeal.GetUnhealableClasses())
     3025    {
     3026        if (cmpIdentity.HasClass(unhealableClass) != -1)
     3027        {
     3028            return false;
     3029        }
     3030    }
     3031
     3032    // Verify that the target is a healable class
     3033    // We could also use cmpIdentity.GetClassesList but this way is cleaner
     3034    var healable = false;
     3035    for each (var healableClass in cmpHeal.GetHealableClasses())
     3036    {
     3037        if (cmpIdentity.HasClass(healableClass) != -1)
     3038        {
     3039            healable = true;
     3040        }
     3041    }
     3042    if (!healable)
     3043        return false;
     3044
     3045    // Check that the target is not at MaxHealth
     3046    if (this.TargetIsAtMaxHitpoints(target))
     3047        return false;
     3048
     3049    return true;
     3050};
     3051
    26663052UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
    26673053{
    26683054    // 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");
     
    271272    GetHitpoints: function() { return 50; },
    272273    GetMaxHitpoints: function() { return 60; },
    273274    IsRepairable: function() { return false; },
     275    IsUnhealable: function() { return false; },
    274276});
    275277
    276278AddMock(10, IID_Identity, {
     
    304306    hitpoints: 50,
    305307    maxHitpoints: 60,
    306308    needsRepair: false,
     309    needsHeal: true,
    307310    buildEntities: ["test1", "test2"],
    308311    barterMarket: {
    309312        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>