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

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

Added placeholder animation

  • 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() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
    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() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
    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.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 undefined;
    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
     
    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
     
    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(true);
     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()
     
    17972055};
    17982056
    17992057/**
     2058 * Returns true if the target exists and the current hitpoints are at maximum.
     2059 */
     2060UnitAI.prototype.TargetIsAtMaxHitpoints = function(ent)
     2061{
     2062    var cmpHealth = Engine.QueryInterface(ent, IID_Health);
     2063    if (!cmpHealth)
     2064        return false;
     2065
     2066    return (cmpHealth.GetHitpoints() == cmpHealth.GetMaxHitpoints());
     2067};
     2068
     2069/**
     2070 * Returns true if the target exists and is unhealable.
     2071 */
     2072UnitAI.prototype.TargetIsUnhealable = function(ent)
     2073{
     2074    var cmpHealth = Engine.QueryInterface(ent, IID_Health);
     2075    if (!cmpHealth)
     2076        return false;
     2077
     2078    return cmpHealth.IsUnhealable();
     2079};
     2080
     2081/**
    18002082 * Returns true if the target exists and needs to be killed before
    18012083 * beginning to gather resources from it.
    18022084 */
     
    22962578        case "Flee":
    22972579        case "LeaveFoundation":
    22982580        case "Attack":
     2581        case "Heal":
    22992582        case "Gather":
    23002583        case "ReturnResource":
    23012584        case "Repair":
     
    23582641{
    23592642    if (!this.CanAttack(target))
    23602643    {
    2361         this.WalkToTarget(target, queued);
     2644        // We don't want to let healers walk to the target unit so they can be easily killed.
     2645        // Instead we just let them get into healing range.
     2646        if (this.IsHealer())
     2647            this.MoveToTargetRange(target, IID_Heal);
     2648        else
     2649            this.WalkToTarget(target, queued);
    23622650        return;
    23632651    }
    23642652
     
    24192707    this.AddOrder("GatherNearPosition", { "type": type, "x": x, "z": z }, queued);
    24202708}
    24212709
     2710UnitAI.prototype.Heal = function(target, queued)
     2711{
     2712    if (!this.CanHeal(target))
     2713    {
     2714        this.WalkToTarget(target, queued);
     2715        return;
     2716    }
     2717   
     2718    this.AddOrder("Heal", { "target": target, "force": true }, queued);
     2719};
     2720
    24222721UnitAI.prototype.ReturnResource = function(target, queued)
    24232722{
    24242723    if (!this.CanReturnResource(target, true))
     
    25302829        this.stance = stance;
    25312830    else
    25322831        error("UnitAI: Setting to invalid stance '"+stance+"'");
    2533 }
     2832};
    25342833
    25352834UnitAI.prototype.SwitchToStance = function(stance)
    25362835{
     
    25482847
    25492848    // Reset the range query, since the range depends on stance
    25502849    this.SetupRangeQuery();
    2551 }
     2850    // Just if we are a healer
     2851    // TODO maybe move those two to a SetupRangeQuerys()
     2852    if (this.IsHealer())
     2853        this.SetupHealRangeQuery();
     2854};
    25522855
    25532856/**
    25542857 * Resets losRangeQuery, and if there are some targets in range that we can
     
    25692872    return this.RespondToTargetedEntities(ents);
    25702873};
    25712874
    2572 UnitAI.prototype.GetQueryRange = function()
     2875/**
     2876 * Resets losHealRangeQuery, and if there are some targets in range that we can heal
     2877 * then we start healing and this returns true; otherwise, returns false.
     2878 */
     2879UnitAI.prototype.FindNewHealTargets = function()
     2880{
     2881    if (!this.losHealRangeQuery)
     2882        return false;
     2883   
     2884    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     2885    var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery);
     2886   
     2887    for each (var ent in ents)
     2888    {
     2889        if (this.CanHeal(ent))
     2890        {
     2891            this.PushOrderFront("Heal", { "target": ent, "force": false });
     2892            return true;
     2893        }
     2894    }
     2895    // We haven't found any target to heal
     2896    return false;
     2897};
     2898
     2899UnitAI.prototype.GetQueryRange = function(healer)
    25732900{
    25742901    var ret = { "min": 0, "max": 0 };
    2575     if (this.GetStance().respondStandGround)
     2902    // If we have a healer with passive stance we need to set some defaults (eg same as stand ground)
     2903    if (this.GetStance().respondStandGround || (healer && !this.GetStance().respondStandGround && !this.GetStance().respondChase && !this.GetStance().respondHoldGround))
    25762904    {
    2577         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2905        var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack);
    25782906        if (!cmpRanged)
    25792907            return ret;
    2580         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2908        var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack());
    25812909        ret.min = range.min;
    25822910        ret.max = range.max;
    25832911    }
     
    25912919    }
    25922920    else if (this.GetStance().respondHoldGround)
    25932921    {
    2594         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2922        var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack);
    25952923        if (!cmpRanged)
    25962924            return ret;
    2597         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2925        var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack());
    25982926        var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
    25992927        if (!cmpVision)
    26002928            return ret;
     
    26903018    return true;
    26913019};
    26923020
     3021UnitAI.prototype.CanHeal = function(target)
     3022{
     3023    // Formation controllers should always respond to commands
     3024    // (then the individual units can make up their own minds)
     3025    if (this.IsFormationController())
     3026        return true;
     3027
     3028    // Verify that we're able to respond to Heal commands
     3029    var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     3030    if (!cmpHeal)
     3031        return false;
     3032
     3033    // Verify that the target is alive
     3034    if (!this.TargetIsAlive(target))
     3035        return false;
     3036
     3037    // Verify that the target is owned by the same player as the entity or of an ally
     3038    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     3039    if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
     3040        return false;
     3041   
     3042    // Verify that the target is not unhealable
     3043    if (this.TargetIsUnhealable(target))
     3044    {
     3045        return false;
     3046    }
     3047   
     3048    // Verify that the target has no unhealable class
     3049    // We could also use cmpIdentity.GetClassesList but this way is cleaner
     3050    var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     3051    if (!cmpIdentity)
     3052        return false;
     3053    for each (var unhealableClass in cmpHeal.GetUnhealableClasses())
     3054    {
     3055        if (cmpIdentity.HasClass(unhealableClass) != -1)
     3056        {
     3057            return false;
     3058        }
     3059    }
     3060
     3061    // Verify that the target is a healable class
     3062    // We could also use cmpIdentity.GetClassesList but this way is cleaner
     3063    var healable = false;
     3064    for each (var healableClass in cmpHeal.GetHealableClasses())
     3065    {
     3066        if (cmpIdentity.HasClass(healableClass) != -1)
     3067        {
     3068            healable = true;
     3069        }
     3070    }
     3071    if (!healable)
     3072        return false;
     3073
     3074    // Check that the target is not at MaxHealth
     3075    if (this.TargetIsAtMaxHitpoints(target))
     3076        return false;
     3077
     3078    return true;
     3079};
     3080
    26933081UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
    26943082{
    26953083    // 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>