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

File #999-2012-03-19.patch, 45.2 KB (added by leper, 12 years ago)
  • binaries/data/mods/public/art/actors/units/carthaginians/healer.xml

     
    88        <animation file="female/f_walk_01.dae" name="Walk" speed="30"/>
    99        <animation file="female/f_walk_01.dae" name="Run" speed="45"/>
    1010        <animation file="female/f_death_01.dae" name="Death" speed="100"/>
     11        <animation file="female/f_salute_01.dae" name="Heal" speed="30"/>
    1112      </animations>
    1213      <props>
    1314        <prop actor="props/units/heads/head_kart_priestess.xml" attachpoint="head"/>
  • binaries/data/mods/public/art/actors/units/celts/healer.xml

     
    88        <animation file="biped/inf_staff_walk_a.dae" name="Walk" speed="20"/>
    99        <animation file="infantry/general/death/inf_01.psa" name="Death" speed="400"/>
    1010        <animation file="biped/inf_staff_walk_a.dae" name="Run" speed="40"/>
     11        <animation file="female/f_salute_01.dae" name="Heal" speed="30"/>
    1112      </animations>
    1213      <mesh>skeletal/m_dress_cuffs.dae</mesh>
    1314      <props>
  • binaries/data/mods/public/art/actors/units/hellenes/healer.xml

     
    77        <animation file="biped/inf_staff_idle_a.dae" name="Idle" speed="200"/>
    88        <animation file="biped/inf_staff_walk_a.dae" name="Walk" speed="20"/>
    99        <animation file="infantry/general/death/inf_01.psa" name="Death" speed="400"/>
     10        <animation file="female/f_salute_01.dae" name="Heal" speed="30"/>
    1011      </animations>
    1112      <mesh>skeletal/m_dress_a.pmd</mesh>
    1213      <props>
  • binaries/data/mods/public/art/actors/units/iberians/healer.xml

     
    88        <animation file="female/f_walk_01.dae" name="Walk" speed="30"/>
    99        <animation file="female/f_walk_01.dae" name="Run" speed="45"/>
    1010        <animation file="female/f_death_01.dae" name="Death" speed="120"/>
     11        <animation file="female/f_salute_01.dae" name="Heal" speed="30"/>
    1112      </animations>
    1213      <props>
    1314        <prop actor="props/units/heads/head_iber_healer.xml" attachpoint="head"/>
  • binaries/data/mods/public/art/actors/units/persians/healer.xml

     
    77        <animation file="biped/inf_staff_idle_a.dae" name="Idle" speed="200"/>
    88        <animation file="biped/inf_staff_walk_a.dae" name="Walk" speed="20"/>
    99        <animation file="infantry/general/death/inf_01.psa" name="Death" speed="400"/>
     10        <animation file="female/f_salute_01.dae" name="Heal" speed="30"/>
    1011      </animations>
    1112      <mesh>skeletal/m_dress_cuffs.dae</mesh>
    1213      <props>
  • binaries/data/mods/public/art/actors/units/romans/healer.xml

     
    88        <animation file="biped/inf_staff_walk_a.dae" name="Walk" speed="20"/>
    99        <animation file="biped/inf_staff_walk_a.dae" name="Run" speed="20"/>
    1010        <animation file="infantry/general/death/inf_01.psa" name="Death" speed="400"/>
     11        <animation file="female/f_salute_01.dae" name="Heal" speed="30"/>
    1112      </animations>
    1213      <mesh>skeletal/m_dress_a.pmd</mesh>
    1314      <props>
  • binaries/data/mods/public/art/textures/cursors/action-heal.txt

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

     
    297297                return {"possible": true, "tooltip": tooltip};
    298298            }
    299299            break;
     300        case "heal":
     301            // The check if the target is unhealable is done by targetState.needsHeal
     302            if (entState.Healer && isUnit(targetState) && targetState.needsHeal && (playerOwned || allyOwned))
     303            {
     304                var unhealableClasses = entState.Healer.unhealableClasses;
     305                for each (var unitClass in targetState.identity.classes)
     306                {
     307                    if (unhealableClasses.indexOf(unitClass) != -1)
     308                    {
     309                        return {"possible": false};
     310                    }
     311                }
     312               
     313                var healableClasses = entState.Healer.healableClasses;
     314                for each (var unitClass in targetState.identity.classes)
     315                {
     316                    if (healableClasses.indexOf(unitClass) != -1)
     317                    {
     318                        return {"possible": true};
     319                    }
     320                }
     321            }
     322            break;
    300323        case "gather":
    301324            if (targetState.resourceSupply && (playerOwned || gaiaOwned))
    302325            {
     
    417440            return {"type": "build", "cursor": "action-repair", "target": target};
    418441        else if ((actionInfo = getActionInfo("set-rallypoint", target)).possible)
    419442            return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "position": actionInfo.position};
     443        else if (getActionInfo("heal", target).possible)
     444            return {"type": "heal", "cursor": "action-heal", "target": target};
    420445        else if (getActionInfo("attack", target).possible)
    421446            return {"type": "attack", "cursor": "action-attack", "target": target};
    422447        else if (getActionInfo("unset-rallypoint", target).possible)
     
    10371062        Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
    10381063        return true;
    10391064
     1065    case "heal":
     1066        Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued});
     1067        // TODO: Play a sound?
     1068//      Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] });
     1069        return true;
     1070
    10401071    case "build": // (same command as repair)
    10411072    case "repair":
    10421073        Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
  • binaries/data/mods/public/gui/session/selection_details.js

     
    7474        experienceSize.rtop = 100 - 100 * Math.max(0, Math.min(1, 1.0 * entState.promotion.curr / entState.promotion.req));
    7575        experienceBar.size = experienceSize;
    7676 
    77         var experience = "[font=\"serif-bold-13\"]Experience [/font]" + entState.promotion.curr;
     77        var experience = "[font=\"serif-bold-13\"]Experience [/font]" + Math.floor(entState.promotion.curr);
    7878        if (entState.promotion.curr < entState.promotion.req)
    7979            experience += "/" + entState.promotion.req;
    8080        getGUIObjectByName("experience").tooltip = experience;
  • binaries/data/mods/public/simulation/components/GarrisonHolder.js

     
    320320            var cmpHealth = Engine.QueryInterface(entity, IID_Health);
    321321            if (cmpHealth)
    322322            {
    323                 if (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
     323                // We do not want to heal unhealable units
     324                if (!cmpHealth.IsUnhealable())
    324325                    cmpHealth.Increase(this.healRate);
    325326            }
    326327        }
  • 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();
    158159    }
    159160
    160161    var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
     
    273274        ret.barterMarket = { "prices": cmpBarter.GetPrices() };
    274275    }
    275276
     277    var cmpHeal = Engine.QueryInterface(ent, IID_Heal);
     278    if (cmpHeal)
     279    {
     280        ret.Healer = {
     281            "unhealableClasses": cmpHeal.GetUnhealableClasses(),
     282            "healableClasses": cmpHeal.GetHealableClasses(),
     283        };
     284    }
     285
    276286    var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    277287    ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
    278288
  • 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;
     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!==undefined && 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        || this.GetHitpoints() <= 0
     79        || this.GetHitpoints() >= this.GetMaxHitpoints());
     80};
     81
    7582Health.prototype.Kill = function()
    7683{
    7784    this.Reduce(this.hitpoints);
     
    131138{
    132139    // If we're already dead, don't allow resurrection
    133140    if (this.hitpoints == 0)
    134         return;
     141        return undefined;
    135142
    136143    var old = this.hitpoints;
    137144    this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints());
    138145
    139146    Engine.PostMessage(this.entity, MT_HealthChanged, { "from": old, "to": this.hitpoints });
     147    // We return the old and the actual hp
     148    return { "old": old, "new": this.hitpoints};
    140149};
    141150
    142151//// 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
     
    408450            cmpFormation.Disband();
    409451        },
    410452
     453        "Order.Heal": function(msg) {
     454            // TODO: see notes in Order.Attack
     455            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     456            cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]);
     457            cmpFormation.Disband();
     458        },
     459
    411460        "Order.Repair": function(msg) {
    412461            // TODO: see notes in Order.Attack
    413462            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     
    546595                // So we'll set a timer here and only report the idle event if we
    547596                // remain idle
    548597                this.StartTimer(1000);
     598               
     599                // TODO: We need to set up a Query that watches own and ally units in LOS
     600                // and calls something if they loose health (or change it to simplify)
     601                // to replace the really UGLY EVIL HACK using GlobalHealthChanged as that can
     602                // have a performance impact as it gets called for each healer for every entity
     603                // in the whole map.
     604               
     605                // If a unit can heal and attack we first want to heal wounded units,
     606                // so check if we are a healer and find whether there's anybody nearby to heal.
     607                // If anyone approaches later it'll be handled via LosRangeUpdate.)
     608                if (this.IsHealer() && this.FindNewHealTargets())
     609                    return true; // (abort the FSM transition since we may have already switched state)
    549610
    550611                // If we entered the idle state we must have nothing better to do,
    551612                // so immediately check whether there's anybody nearby to attack.
    552613                // (If anyone approaches later, it'll be handled via LosRangeUpdate.)
    553614                if (this.FindNewTargets())
    554615                    return true; // (abort the FSM transition since we may have already switched state)
    555 
     616               
    556617                // Nobody to attack - stay in idle
    557618                return false;
    558619            },
     
    560621            "leave": function() {
    561622                var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    562623                rangeMan.DisableActiveQuery(this.losRangeQuery);
     624                if (this.losHealRangeQuery)
     625                    rangeMan.DisableActiveQuery(this.losHealRangeQuery);
    563626
    564627                this.StopTimer();
    565628
     
    569632                    Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
    570633                }
    571634            },
    572 
     635           
     636            // TODO: This is part of an really UGLY EVIL HACK
     637            "GlobalHealthChanged": function() {
     638                if (this.IsHealer())
     639                {
     640                    this.FindNewHealTargets();
     641                }
     642            },
     643           
    573644            "LosRangeUpdate": function(msg) {
     645                if (this.IsHealer())
     646                {
     647                    // Start healing one of the newly-seen own or ally units (if any)
     648                    this.FindNewHealTargets();
     649                }
    574650                if (this.GetStance().targetVisibleEnemies)
    575651                {
    576652                    // Start attacking one of the newly-seen enemy (if any)
     
    10001076            },
    10011077        },
    10021078
     1079        "HEAL": {
     1080            "EntityRenamed": function(msg) {
     1081                if (this.order.data.target == msg.entity)
     1082                    this.order.data.target = msg.newentity;
     1083            },
     1084
     1085            "Attacked": function(msg) {
     1086                // If we stand ground we will rather die than flee
     1087                if (!this.GetStance().respondStandGround)
     1088                    this.Flee(msg.data.attacker, false);
     1089            },
     1090
     1091            "APPROACHING": {
     1092                "enter": function () {
     1093                    this.SelectAnimation("move");
     1094                    this.StartTimer(1000, 1000);
     1095                },
     1096
     1097                "leave": function() {
     1098                    this.StopTimer();
     1099                },
     1100
     1101                "Timer": function(msg) {
     1102                    if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
     1103                    {
     1104                        this.StopMoving();
     1105                        this.FinishOrder();
     1106
     1107                        // Return to our original position
     1108                        if (this.GetStance().respondHoldGround)
     1109                            this.WalkToHeldPosition();
     1110                    }
     1111                },
     1112
     1113                "MoveCompleted": function() {
     1114                    this.SetNextState("HEALING");
     1115                },
     1116
     1117                "Attacked": function(msg) {
     1118                    // If we stand ground we will rather die than flee
     1119                    if (!this.GetStance().respondStandGround)
     1120                        this.Flee(msg.data.attacker, false);
     1121                },
     1122            },
     1123
     1124            "HEALING": {
     1125                "enter": function() {
     1126                    var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     1127                    this.healTimers = cmpHeal.GetTimers();
     1128                    this.SelectAnimation("heal", false, 1.0, "heal");
     1129                    this.SetAnimationSync(this.healTimers.prepare, this.healTimers.repeat);
     1130                    this.StartTimer(this.healTimers.prepare, this.healTimers.repeat);
     1131                    // TODO if .prepare is short, players can cheat by cycling heal/stop/heal
     1132                    // to beat the .repeat time; should enforce a minimum time
     1133                    // see comment in ATTACKING.enter
     1134                    this.FaceTowardsTarget(this.order.data.target);
     1135                },
     1136
     1137                "leave": function() {
     1138                    this.StopTimer();
     1139                },
     1140
     1141                "Timer": function(msg) {
     1142                    var target = this.order.data.target;
     1143                    // Check the target is still alive and healable
     1144                    if (this.TargetIsAlive(target) && this.CanHeal(target))
     1145                    {
     1146                        // Check if we can still reach the target
     1147                        if (this.CheckTargetRange(target, IID_Heal))
     1148                        {
     1149                            this.FaceTowardsTarget(target);
     1150                            var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     1151                            cmpHeal.PerformHeal(target);
     1152                            return;
     1153                        }
     1154                        // Can't reach it - try to chase after it
     1155                        if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
     1156                        {
     1157                            if (this.MoveToTargetRange(target, IID_Heal))
     1158                            {
     1159                                this.SetNextState("HEAL.CHASING");
     1160                                return;
     1161                            }
     1162                        }
     1163                    }
     1164                    // Can't reach it, healed to max hp or doesn't exist any more - give up
     1165                    if (this.FinishOrder())
     1166                        return;
     1167
     1168                    // Heal another one
     1169                    if (this.FindNewHealTargets())
     1170                        return;
     1171                   
     1172                    // Return to our original position
     1173                    if (this.GetStance().respondHoldGround)
     1174                        this.WalkToHeldPosition();
     1175                },
     1176                "Attacked": function(msg) {
     1177                    // If we stand ground we will rather die than flee
     1178                    if (!this.GetStance().respondStandGround)
     1179                        this.Flee(msg.data.attacker, false);
     1180                },
     1181            },
     1182            "CHASING": {
     1183                "enter": function () {
     1184                    this.SelectAnimation("move");
     1185                    this.StartTimer(1000, 1000);
     1186                },
     1187
     1188                "leave": function () {
     1189                    this.StopTimer();
     1190                },
     1191                "Timer": function(msg) {
     1192                    if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
     1193                    {
     1194                        this.StopMoving();
     1195                        this.FinishOrder();
     1196
     1197                        // Return to our original position
     1198                        if (this.GetStance().respondHoldGround)
     1199                            this.WalkToHeldPosition();
     1200                    }
     1201                },
     1202                "MoveCompleted": function () {
     1203                    this.SetNextState("HEALING");
     1204                },
     1205            }, 
     1206        },
     1207
    10031208        // Returning to dropsite
    10041209        "RETURNRESOURCE": {
    10051210            "APPROACHING": {
     
    14731678    return (this.template.NaturalBehaviour ? true : false);
    14741679};
    14751680
     1681UnitAI.prototype.IsHealer = function()
     1682{
     1683    return Engine.QueryInterface(this.entity, IID_Heal);
     1684};
     1685
    14761686UnitAI.prototype.IsIdle = function()
    14771687{
    14781688    return this.isIdle;
     
    14961706UnitAI.prototype.OnOwnershipChanged = function(msg)
    14971707{
    14981708    this.SetupRangeQuery();
     1709    if (this.IsHealer())
     1710        this.SetupHealRangeQuery();
    14991711};
    15001712
    15011713UnitAI.prototype.OnDestroy = function()
     
    15071719    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    15081720    if (this.losRangeQuery)
    15091721        rangeMan.DestroyActiveQuery(this.losRangeQuery);
     1722    if (this.losHealRangeQuery)
     1723        rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
    15101724};
    15111725
    15121726// Set up a range query for all enemy units within LOS range
     
    15391753        }
    15401754    }
    15411755
    1542     var range = this.GetQueryRange();
     1756    var range = this.GetQueryRange(IID_Attack);
    15431757
    15441758    this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver);
    15451759    rangeMan.EnableActiveQuery(this.losRangeQuery);
    15461760};
    15471761
     1762// Set up a range query for all own or ally units within LOS range
     1763// which can be healed.
     1764// This should be called whenever our ownership changes.
     1765UnitAI.prototype.SetupHealRangeQuery = function()
     1766{
     1767    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     1768    var owner = cmpOwnership.GetOwner();
     1769
     1770    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     1771    var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     1772
     1773    if (this.losHealRangeQuery)
     1774        rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
     1775
     1776    var players = [owner];
     1777
     1778    if (owner != -1)
     1779    {
     1780        // If unit not just killed, get ally players via diplomacy
     1781        var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
     1782        var numPlayers = playerMan.GetNumPlayers();
     1783        for (var i = 1; i < numPlayers; ++i)
     1784        {
     1785            // Exclude gaia and enemies
     1786            if (cmpPlayer.IsAlly(i))
     1787                players.push(i);
     1788        }
     1789    }
     1790
     1791    var range = this.GetQueryRange(IID_Heal);
     1792
     1793    this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health);
     1794    rangeMan.EnableActiveQuery(this.losHealRangeQuery);
     1795};
     1796
    15481797//// FSM linkage functions ////
    15491798
    15501799UnitAI.prototype.SetNextState = function(state)
     
    17662015
    17672016UnitAI.prototype.OnRangeUpdate = function(msg)
    17682017{
    1769     if (msg.tag == this.losRangeQuery)
     2018    if (msg.tag == this.losRangeQuery || msg.tag == this.losHealRangeQuery)
    17702019        UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
    17712020};
    17722021
     2022// really UGLY EVIL HACK
     2023// TODO This should be replaced with a c++ component that tracks all entities within a range and notifies
     2024// Healers if any of the entities receives a HealthChanged message.
     2025// If any entity changes health we call this, so this probably has a huge performance impact.
     2026UnitAI.prototype.OnGlobalHealthChanged = function(msg)
     2027{
     2028    UnitFsm.ProcessMessage(this, {"type": "GlobalHealthChanged"});
     2029}
     2030
    17732031//// Helper functions to be called by the FSM ////
    17742032
    17752033UnitAI.prototype.GetWalkSpeed = function()
     
    22962554        case "Flee":
    22972555        case "LeaveFoundation":
    22982556        case "Attack":
     2557        case "Heal":
    22992558        case "Gather":
    23002559        case "ReturnResource":
    23012560        case "Repair":
     
    23582617{
    23592618    if (!this.CanAttack(target))
    23602619    {
    2361         this.WalkToTarget(target, queued);
     2620        // We don't want to let healers walk to the target unit so they can be easily killed.
     2621        // Instead we just let them get into healing range.
     2622        if (this.IsHealer())
     2623            this.MoveToTargetRange(target, IID_Heal);
     2624        else
     2625            this.WalkToTarget(target, queued);
    23622626        return;
    23632627    }
    23642628
     
    24192683    this.AddOrder("GatherNearPosition", { "type": type, "x": x, "z": z }, queued);
    24202684}
    24212685
     2686UnitAI.prototype.Heal = function(target, queued)
     2687{
     2688    if (!this.CanHeal(target))
     2689    {
     2690        this.WalkToTarget(target, queued);
     2691        return;
     2692    }
     2693   
     2694    this.AddOrder("Heal", { "target": target, "force": true }, queued);
     2695};
     2696
    24222697UnitAI.prototype.ReturnResource = function(target, queued)
    24232698{
    24242699    if (!this.CanReturnResource(target, true))
     
    25302805        this.stance = stance;
    25312806    else
    25322807        error("UnitAI: Setting to invalid stance '"+stance+"'");
    2533 }
     2808};
    25342809
    25352810UnitAI.prototype.SwitchToStance = function(stance)
    25362811{
     
    25482823
    25492824    // Reset the range query, since the range depends on stance
    25502825    this.SetupRangeQuery();
    2551 }
     2826    // Just if we are a healer
     2827    // TODO maybe move those two to a SetupRangeQuerys()
     2828    if (this.IsHealer())
     2829        this.SetupHealRangeQuery();
     2830};
    25522831
    25532832/**
    25542833 * Resets losRangeQuery, and if there are some targets in range that we can
     
    25692848    return this.RespondToTargetedEntities(ents);
    25702849};
    25712850
    2572 UnitAI.prototype.GetQueryRange = function()
     2851/**
     2852 * Resets losHealRangeQuery, and if there are some targets in range that we can heal
     2853 * then we start healing and this returns true; otherwise, returns false.
     2854 */
     2855UnitAI.prototype.FindNewHealTargets = function()
     2856{
     2857    if (!this.losHealRangeQuery)
     2858        return false;
     2859   
     2860    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     2861    var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery);
     2862   
     2863    for each (var ent in ents)
     2864    {
     2865        if (this.CanHeal(ent))
     2866        {
     2867            this.PushOrderFront("Heal", { "target": ent, "force": false });
     2868            return true;
     2869        }
     2870    }
     2871    // We haven't found any target to heal
     2872    return false;
     2873};
     2874
     2875UnitAI.prototype.GetQueryRange = function(iid)
    25732876{
    25742877    var ret = { "min": 0, "max": 0 };
    25752878    if (this.GetStance().respondStandGround)
    25762879    {
    2577         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2880        var cmpRanged = Engine.QueryInterface(this.entity, iid);
    25782881        if (!cmpRanged)
    25792882            return ret;
    2580         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2883        var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetBestAttack();
    25812884        ret.min = range.min;
    25822885        ret.max = range.max;
    25832886    }
     
    25912894    }
    25922895    else if (this.GetStance().respondHoldGround)
    25932896    {
    2594         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2897        var cmpRanged = Engine.QueryInterface(this.entity, iid);
    25952898        if (!cmpRanged)
    25962899            return ret;
    2597         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2900        var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetBestAttack();
    25982901        var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
    25992902        if (!cmpVision)
    26002903            return ret;
    26012904        var halfvision = cmpVision.GetRange() / 2;
    26022905        ret.max = range.max + halfvision;
    26032906    }
     2907    // We probably have stance 'passive' and we wouldn't have a range,
     2908    // but as it is the default for healers we need to set it to something sane.
     2909    else if (iid === IID_Heal)
     2910    {
     2911        var cmpRanged = Engine.QueryInterface(this.entity, iid);
     2912        if (!cmpRanged)
     2913            return ret;
     2914        var range = cmpRanged.GetRange();
     2915        ret.min = range.min;
     2916        ret.max = range.max;
     2917    }
    26042918    return ret;
    26052919};
    26062920
     
    26903004    return true;
    26913005};
    26923006
     3007UnitAI.prototype.CanHeal = function(target)
     3008{
     3009    // Formation controllers should always respond to commands
     3010    // (then the individual units can make up their own minds)
     3011    if (this.IsFormationController())
     3012        return true;
     3013
     3014    // Verify that we're able to respond to Heal commands
     3015    var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     3016    if (!cmpHeal)
     3017        return false;
     3018
     3019    // Verify that the target is alive
     3020    if (!this.TargetIsAlive(target))
     3021        return false;
     3022
     3023    // Verify that the target is owned by the same player as the entity or of an ally
     3024    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     3025    if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
     3026        return false;
     3027
     3028    // Verify that the target is not unhealable (or at max health)
     3029    var cmpHealth = Engine.QueryInterface(target, IID_Health);
     3030    if (!cmpHealth || cmpHealth.IsUnhealable())
     3031        return false;
     3032
     3033    // Verify that the target has no unhealable class
     3034    var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     3035    if (!cmpIdentity)
     3036        return false;
     3037    for each (var unhealableClass in cmpHeal.GetUnhealableClasses())
     3038    {
     3039        if (cmpIdentity.HasClass(unhealableClass) != -1)
     3040        {
     3041            return false;
     3042        }
     3043    }
     3044
     3045    // Verify that the target is a healable class
     3046    var healable = false;
     3047    for each (var healableClass in cmpHeal.GetHealableClasses())
     3048    {
     3049        if (cmpIdentity.HasClass(healableClass) != -1)
     3050        {
     3051            healable = true;
     3052        }
     3053    }
     3054    if (!healable)
     3055        return false;
     3056
     3057    return true;
     3058};
     3059
    26933060UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
    26943061{
    26953062    // 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 player "+player+" or their ally: "+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>
    9     <Heal>
    10       <Radius>20</Radius>
    11       <Speed>2000</Speed>
    12     </Heal>
    13   </Auras>
     8  <Heal>
     9    <Range>30</Range>
     10    <HP>5</HP>
     11    <Rate>2000</Rate>
     12    <UnhealableClasses datatype="tokens"/>
     13    <HealableClasses datatype="tokens">Support Infantry Cavalry</HealableClasses>
     14  </Heal>
    1415  <Cost>
    1516    <Resources>
    1617      <metal>120</metal>
     
    2324  <Identity>
    2425    <Classes datatype="tokens">Healer</Classes>
    2526    <GenericName>Healer</GenericName>
    26     <Tooltip>Heal units within his Aura. (Not implemented yet)</Tooltip>
     27    <Tooltip>Heal units.</Tooltip>
    2728  </Identity>
     29  <Promotion>
     30    <RequiredXp>100</RequiredXp>
     31  </Promotion>
    2832  <Sound>
    2933    <SoundGroups>
    3034      <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>