Ticket #999: #999-2012-02-26.patch

File #999-2012-02-26.patch, 37.9 KB (added by leper, 12 years ago)
  • 0ad/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))
  • 0ad/binaries/data/mods/public/simulation/components/GuiInterface.js

     
    264264        ret.barterMarket = { "prices": cmpBarter.GetPrices() };
    265265    }
    266266
     267    // Abilities
     268    var cmpHeal = Engine.QueryInterface(ent, IID_Heal);
     269    // Check if we have abilities
     270    if (cmpHeal || (cmpHeal && cmpPromotion))
     271        ret.Ability = [];
     272    if (cmpHeal)
     273    {
     274        ret.Ability.Healer = { "healableClasses": cmpHeal.GetHealableClasses() };
     275    }
     276
     277    // TODO remove this; This is just used to test/demonstrate the extensibility
     278    // of the Ability system
     279    // promoteAbility (just for healers)
     280    if (cmpPromotion && cmpHeal)
     281    {
     282        ret.Ability.Promote = true ;
     283    }
     284
    267285    var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    268286    ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
    269287
  • 0ad/binaries/data/mods/public/simulation/components/interfaces/Heal.js

     
     1Engine.RegisterInterface("Heal");
  • 0ad/binaries/data/mods/public/simulation/components/Health.js

     
    131131{
    132132    // If we're already dead, don't allow resurrection
    133133    if (this.hitpoints == 0)
    134         return;
     134        return false;
    135135
    136136    var old = this.hitpoints;
    137137    this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints());
    138138
    139139    Engine.PostMessage(this.entity, MT_HealthChanged, { "from": old, "to": this.hitpoints });
     140    // We return the old and the actual hp
     141    return { "old": old, "new": this.hitpoints};
    140142};
    141143
    142144//// Private functions ////
  • 0ad/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()
  • 0ad/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");
  • 0ad/binaries/data/mods/public/simulation/components/UnitAI.js

     
    283283        this.FinishOrder();
    284284    },
    285285
     286    "Order.Heal": function(msg) {
     287        // Check the target is alive
     288        if (!this.TargetIsAlive(this.order.data.target))
     289        {
     290            this.FinishOrder();
     291            return;
     292        }
     293
     294        // Check if the target is in range
     295        if (this.CheckTargetRange(this.order.data.target, IID_Heal))
     296        {
     297            this.StopMoving();
     298            this.SetNextState("INDIVIDUAL.HEAL.HEALING");
     299            return;
     300        }
     301
     302        // If we can't reach the target, but are standing ground,
     303        // then abandon this heal order
     304        if (this.GetStance().respondStandGround && !this.order.data.force)
     305        {
     306            this.FinishOrder();
     307            return;
     308        }
     309
     310        // Try to move within heal range
     311        if (this.MoveToTargetRange(this.order.data.target, IID_Heal))
     312        {
     313            // We've started walking to the given point
     314            this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
     315            return;
     316        }
     317
     318        // We can't reach the target, and can't move towards it,
     319        // so abandon this heal order
     320        this.FinishOrder();
     321    },
     322
    286323    "Order.Gather": function(msg) {
    287324       
    288325        // If the target is still alive, we need to kill it first
     
    402439            cmpFormation.Disband();
    403440        },
    404441
     442        "Order.Heal": function(msg) {
     443            // TODO: see notes in Order.Attack
     444            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     445            cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]);
     446            cmpFormation.Disband();
     447        },
     448
    405449        "Order.Repair": function(msg) {
    406450            // TODO: see notes in Order.Attack
    407451            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     
    540584                // So we'll set a timer here and only report the idle event if we
    541585                // remain idle
    542586                this.StartTimer(1000);
     587               
     588                // If a unit can heal and attack we first want to heal wounded units,
     589                // so check if we are a healer and find whether there's anybody nearby to heal.
     590                // If anyone approaches later it'll be handled via LosRangeUpdate.)
     591                if (this.IsHealer() && this.FindNewHealTargets())
     592                    return true; // (abort the FSM transition since we may have already switched state)
    543593
    544594                // If we entered the idle state we must have nothing better to do,
    545595                // so immediately check whether there's anybody nearby to attack.
    546596                // (If anyone approaches later, it'll be handled via LosRangeUpdate.)
    547597                if (this.FindNewTargets())
    548598                    return true; // (abort the FSM transition since we may have already switched state)
    549 
     599               
    550600                // Nobody to attack - stay in idle
    551601                return false;
    552602            },
     
    554604            "leave": function() {
    555605                var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    556606                rangeMan.DisableActiveQuery(this.losRangeQuery);
     607                if (this.losHealRangeQuery)
     608                    rangeMan.DisableActiveQuery(this.losHealRangeQuery);
    557609
    558610                this.StopTimer();
    559611
     
    563615                    Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
    564616                }
    565617            },
    566 
     618           
    567619            "LosRangeUpdate": function(msg) {
     620                if (this.IsHealer())
     621                {
     622                    // Start healing one of the newly-seen own or ally units (if any)
     623                    this.FindNewHealTargets();
     624                }
    568625                if (this.GetStance().targetVisibleEnemies)
    569626                {
    570627                    // Start attacking one of the newly-seen enemy (if any)
     
    9951052            },
    9961053        },
    9971054
     1055        "HEAL": {
     1056            "EntityRenamed": function(msg) {
     1057                if (this.order.data.target == msg.entity)
     1058                    this.order.data.target = msg.newentity;
     1059            },
     1060
     1061            "Attacked": function(msg) {
     1062                // If we stand ground we will rather die than flee
     1063                if (!this.GetStance().respondStandGround)
     1064                    this.Flee(msg.data.attacker, false);
     1065            },
     1066
     1067            "APPROACHING": {
     1068                "enter": function () {
     1069                    this.SelectAnimation("move");
     1070                    this.StartTimer(1000, 1000);
     1071                },
     1072
     1073                "leave": function() {
     1074                    this.StopTimer();
     1075                },
     1076
     1077                "Timer": function(msg) {
     1078                    if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
     1079                    {
     1080                        this.StopMoving();
     1081                        this.FinishOrder();
     1082
     1083                        // Return to our original position
     1084                        if (this.GetStance().respondHoldGround)
     1085                            this.WalkToHeldPosition();
     1086                    }
     1087                },
     1088
     1089                "MoveCompleted": function() {
     1090                    this.SetNextState("HEALING");
     1091                },
     1092
     1093                "Attacked": function(msg) {
     1094                    // If we stand ground we will rather die than flee
     1095                    if (!this.GetStance().respondStandGround)
     1096                        this.Flee(msg.data.attacker, false);
     1097                },
     1098            },
     1099
     1100            "HEALING": {
     1101                "enter": function() {
     1102                    var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     1103                    this.healTimers = cmpHeal.GetTimers();
     1104//                  this.SelectAnimation("heal", false, 1.0, "heal"); // TODO needs animation
     1105//                  this.SetAnimationSync(this.healTimers.prepare, this.healTimers.repeat);
     1106                    this.StartTimer(this.healTimers.prepare, this.healTimers.repeat);
     1107                    // TODO if .prepare is short, players can cheat by cycling heal/stop/heal
     1108                    // to beat the .repeat time; should enforce a minimum time
     1109                    // see comment in ATTACKING.enter
     1110                    this.FaceTowardsTarget(this.order.data.target);
     1111                },
     1112
     1113                "leave": function() {
     1114                    this.StopTimer();
     1115                },
     1116
     1117                "Timer": function(msg) {
     1118                    var target = this.order.data.target;
     1119                    // Check the target is still alive and healable
     1120                    if (this.TargetIsAlive(target) && this.CanHeal(target))
     1121                    {
     1122                        // Check if we can still reach the target
     1123                        if (this.CheckTargetRange(target, IID_Heal))
     1124                        {
     1125                            this.FaceTowardsTarget(target);
     1126                            var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     1127                            cmpHeal.PerformHeal(target);
     1128                            return;
     1129                        }
     1130                        // Can't reach it - try to chase after it
     1131                        if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
     1132                        {
     1133                            if (this.MoveToTargetRange(target, IID_Heal))
     1134                            {
     1135                                this.SetNextState("HEAL.CHASING");
     1136                                return;
     1137                            }
     1138                        }
     1139                    }
     1140                    // Can't reach it, healed to max hp or doesn't exist any more - give up
     1141                    if (this.FinishOrder())
     1142                        return;
     1143
     1144                    // Heal another one
     1145                    if (this.FindNewHealTargets())
     1146                        return;
     1147                   
     1148                    // Return to our original position
     1149                    if (this.GetStance().respondHoldGround)
     1150                        this.WalkToHeldPosition();
     1151                },
     1152                "Attacked": function(msg) {
     1153                    // If we stand ground we will rather die than flee
     1154                    if (!this.GetStance().respondStandGround)
     1155                        this.Flee(msg.data.attacker, false);
     1156                },
     1157            },
     1158            "CHASING": {
     1159                "enter": function () {
     1160                    this.SelectAnimation("move");
     1161                    this.StartTimer(1000, 1000);
     1162                },
     1163
     1164                "leave": function () {
     1165                    this.StopTimer();
     1166                },
     1167                "Timer": function(msg) {
     1168                    if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
     1169                    {
     1170                        this.StopMoving();
     1171                        this.FinishOrder();
     1172
     1173                        // Return to our original position
     1174                        if (this.GetStance().respondHoldGround)
     1175                            this.WalkToHeldPosition();
     1176                    }
     1177                },
     1178                "MoveCompleted": function () {
     1179                    this.SetNextState("HEALING");
     1180                },
     1181            }, 
     1182        },
     1183
    9981184        // Returning to dropsite
    9991185        "RETURNRESOURCE": {
    10001186            "APPROACHING": {
     
    14221608    return (this.template.NaturalBehaviour ? true : false);
    14231609};
    14241610
     1611UnitAI.prototype.IsHealer = function()
     1612{
     1613    return Engine.QueryInterface(this.entity, IID_Heal);
     1614};
     1615
    14251616UnitAI.prototype.IsIdle = function()
    14261617{
    14271618    return this.isIdle;
     
    14451636UnitAI.prototype.OnOwnershipChanged = function(msg)
    14461637{
    14471638    this.SetupRangeQuery();
     1639    if (this.IsHealer())
     1640        this.SetupHealRangeQuery();
    14481641};
    14491642
    14501643UnitAI.prototype.OnDestroy = function()
     
    14561649    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    14571650    if (this.losRangeQuery)
    14581651        rangeMan.DestroyActiveQuery(this.losRangeQuery);
     1652    if (this.losHealRangeQuery)
     1653        rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
    14591654};
    14601655
    14611656// Set up a range query for all enemy units within LOS range
     
    14941689    rangeMan.EnableActiveQuery(this.losRangeQuery);
    14951690};
    14961691
     1692// Set up a range query for all own or ally units within LOS range
     1693// which can be healed.
     1694// This should be called whenever our ownership changes.
     1695UnitAI.prototype.SetupHealRangeQuery = function()
     1696{
     1697    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     1698    var owner = cmpOwnership.GetOwner();
     1699
     1700    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     1701    var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     1702
     1703    if (this.losHealRangeQuery)
     1704        rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
     1705
     1706    var players = [owner];
     1707
     1708    if (owner != -1)
     1709    {
     1710        // If unit not just killed, get ally players via diplomacy
     1711        var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
     1712        var numPlayers = playerMan.GetNumPlayers();
     1713        for (var i = 1; i < numPlayers; ++i)
     1714        {
     1715            // Exclude gaia and enemies
     1716            if (cmpPlayer.IsAlly(i))
     1717                players.push(i);
     1718        }
     1719    }
     1720
     1721    var range = this.GetQueryRange(true);
     1722
     1723    this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health);
     1724    rangeMan.EnableActiveQuery(this.losHealRangeQuery);
     1725};
     1726
    14971727//// FSM linkage functions ////
    14981728
    14991729UnitAI.prototype.SetNextState = function(state)
     
    17151945
    17161946UnitAI.prototype.OnRangeUpdate = function(msg)
    17171947{
    1718     if (msg.tag == this.losRangeQuery)
     1948    if (msg.tag == this.losRangeQuery || msg.tag == this.losHealRangeQuery)
    17191949        UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
    17201950};
    17211951
     
    17461976};
    17471977
    17481978/**
     1979 * Returns true if the target exists and the current hitpoints are at maximum.
     1980 */
     1981UnitAI.prototype.TargetIsAtMaxHitpoints = function(ent)
     1982{
     1983    var cmpHealth = Engine.QueryInterface(ent, IID_Health);
     1984    if(!cmpHealth)
     1985        return false;
     1986
     1987    return (cmpHealth.GetHitpoints() == cmpHealth.GetMaxHitpoints());
     1988};
     1989
     1990/**
    17491991 * Returns true if the target exists and needs to be killed before
    17501992 * beginning to gather resources from it.
    17511993 */
     
    22362478        case "Flee":
    22372479        case "LeaveFoundation":
    22382480        case "Attack":
     2481        case "Heal":
    22392482        case "Gather":
    22402483        case "ReturnResource":
    22412484        case "Repair":
     
    23592602    this.AddOrder("GatherNearPosition", { "type": type, "x": position[0], "z": position[1] }, queued);
    23602603}
    23612604
     2605UnitAI.prototype.Heal = function(target, queued)
     2606{
     2607    if (!this.CanHeal(target))
     2608    {
     2609        this.WalkToTarget(target, queued);
     2610        return;
     2611    }
     2612   
     2613    this.AddOrder("Heal", { "target": target, "force": true }, queued);
     2614};
     2615
    23622616UnitAI.prototype.ReturnResource = function(target, queued)
    23632617{
    23642618    if (!this.CanReturnResource(target, true))
     
    23972651        this.stance = stance;
    23982652    else
    23992653        error("UnitAI: Setting to invalid stance '"+stance+"'");
    2400 }
     2654};
    24012655
    24022656UnitAI.prototype.SwitchToStance = function(stance)
    24032657{
     
    24132667
    24142668    // Reset the range query, since the range depends on stance
    24152669    this.SetupRangeQuery();
    2416 }
     2670    // Just if we are a healer
     2671    // TODO maybe move those two to a SetupRangeQuerys()
     2672    if (this.IsHealer())
     2673        this.SetupHealRangeQuery();
     2674};
    24172675
    24182676/**
    24192677 * Resets losRangeQuery, and if there are some targets in range that we can
     
    24342692    return this.RespondToTargetedEntities(ents);
    24352693};
    24362694
    2437 UnitAI.prototype.GetQueryRange = function()
     2695/**
     2696 * Resets losHealRangeQuery, and if there are some targets in range that we can heal
     2697 * then we start healing and this returns true; otherwise, returns false.
     2698 */
     2699UnitAI.prototype.FindNewHealTargets = function()
    24382700{
     2701    if (!this.losHealRangeQuery)
     2702        return false;
     2703   
     2704    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     2705    var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery);
     2706   
     2707    for each (var ent in ents)
     2708    {
     2709        if (this.CanHeal(ent))
     2710        {
     2711            this.PushOrderFront("Heal", { "target": ent, "force": false });
     2712            return true;
     2713        }
     2714    }
     2715    // We haven't found any target to heal
     2716    return false;
     2717};
     2718
     2719UnitAI.prototype.GetQueryRange = function(healer)
     2720{
    24392721    var ret = { "min": 0, "max": 0 };
    2440     if (this.GetStance().respondStandGround)
     2722    // If we have a healer with passive stance we need to set some defaults (eg same as stand ground)
     2723    if (this.GetStance().respondStandGround || (healer && !this.GetStance().respondStandGround && !this.GetStance().respondChase && !this.GetStance().respondHoldGround))
    24412724    {
    2442         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2725        var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack);
    24432726        if (!cmpRanged)
    24442727            return ret;
    2445         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2728        var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack());
    24462729        ret.min = range.min;
    24472730        ret.max = range.max;
    24482731    }
     
    24562739    }
    24572740    else if (this.GetStance().respondHoldGround)
    24582741    {
    2459         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2742        var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack);
    24602743        if (!cmpRanged)
    24612744            return ret;
    2462         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2745        var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack());
    24632746        var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
    24642747        if (!cmpVision)
    24652748            return ret;
     
    25552838    return true;
    25562839};
    25572840
     2841// TODO should we adhere to the Healable element (Health.js?)
     2842UnitAI.prototype.CanHeal = function(target)
     2843{
     2844    // Formation controllers should always respond to commands
     2845    // (then the individual units can make up their own minds)
     2846    if (this.IsFormationController())
     2847        return true;
     2848
     2849    // Verify that we're able to respond to Heal commands
     2850    var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     2851    if (!cmpHeal)
     2852        return false;
     2853
     2854    // Verify that the target is alive
     2855    if (!this.TargetIsAlive(target))
     2856        return false;
     2857
     2858    // Verify that the target is owned by the same player as the entity or of an ally
     2859    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     2860    if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
     2861        return false;
     2862
     2863    // Verify that the target is a healable class
     2864    // We could also use cmpIdentity.GetClassesList but this way is cleaner
     2865    var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     2866    if (!cmpIdentity)
     2867        return false;
     2868    var healable = false;
     2869    for each (var healableClass in cmpHeal.GetHealableClasses())
     2870    {
     2871        if (cmpIdentity.HasClass(healableClass) != -1)
     2872        {
     2873            healable = true;
     2874        }
     2875    }
     2876    if (!healable)
     2877        return false;
     2878
     2879    // Check that the target is not at MaxHealth
     2880    if (this.TargetIsAtMaxHitpoints(target))
     2881        return false;
     2882
     2883    return true;
     2884};
     2885
    25582886UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
    25592887{
    25602888    // Formation controllers should always respond to commands
  • 0ad/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        "<HealableClasses datatype=\"tokens\">Support Infantry</HealableClasses>" +
     10    "</a:example>" +
     11    "<element name='Range' a:help='Range (in metres) where healing is possible'>" +
     12        "<ref name='nonNegativeDecimal'/>" +
     13    "</element>" +
     14    "<element name='HP' a:help='Hitpoints healed per Rate'>" +
     15        "<ref name='nonNegativeDecimal'/>" +
     16    "</element>" +
     17    "<element name='Rate' a:help='A heal is performed every Rate ms'>" +
     18        "<ref name='nonNegativeDecimal'/>" +
     19    "</element>" +
     20    "<element name='HealableClasses'>" +
     21        "<attribute name='datatype'>" +
     22            "<value>tokens</value>" +
     23        "</attribute>" +
     24        "<text/>" +
     25    "</element>";
     26
     27Heal.prototype.Init = function()
     28{
     29};
     30
     31Heal.prototype.Serialize = null; // we have no dynamic state to save
     32
     33Heal.prototype.GetTimers = function()
     34{
     35    var prepare = 1000;
     36    var repeat = +(this.template.Rate || 1000);
     37    return { "prepare": prepare, "repeat": repeat };
     38}
     39
     40Heal.prototype.GetRange = function()
     41{
     42    var max = +this.template.Range;
     43    var min = 0;
     44    return { "max": max, "min": min };
     45};
     46
     47Heal.prototype.GetHealableClasses = function()
     48{
     49    var classes = this.template.HealableClasses._string;
     50    return classes.split(/\s+/);
     51};
     52
     53/**
     54 * Heal the target entity. This should only be called after a successful range
     55 * check, and should only be called after GetTimers().repeat msec has passed
     56 * since the last call to PerformHeal.
     57 */
     58Heal.prototype.PerformHeal = function(target)
     59{
     60    this.CauseHeal({"target": target});
     61};
     62
     63/**
     64 * Heal target
     65 */
     66Heal.prototype.CauseHeal = function(data)
     67{
     68    var cmpHealth = Engine.QueryInterface(data.target, IID_Health);
     69    if (!cmpHealth)
     70        return;
     71    var targetState = cmpHealth.Increase(Math.max(0,this.template.HP));
     72
     73    // Add XP
     74    var cmpLoot = Engine.QueryInterface(data.target, IID_Loot);
     75    var cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
     76    if (targetState.old && targetState.new && cmpLoot && cmpPromotion)
     77    {
     78        // HP healed * XP per HP
     79        cmpPromotion.IncreaseXp((targetState.new-targetState.old)*(cmpLoot.GetXp()/cmpHealth.GetMaxHitpoints()));
     80    }
     81    //TODO we need a sound file
     82//  PlaySound("heal_impact", this.entity);
     83};
     84
     85Engine.RegisterComponentType(IID_Heal, "Heal", Heal);
  • 0ad/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    <HealableClasses datatype="tokens">Support Infantry Cavalry</HealableClasses>
     19  </Heal>
    1420  <Cost>
    1521    <Resources>
    1622      <metal>120</metal>
     
    2329  <Identity>
    2430    <Classes datatype="tokens">Healer</Classes>
    2531    <GenericName>Healer</GenericName>
    26     <Tooltip>Heal units within his Aura. (Not implemented yet)</Tooltip>
     32    <Tooltip>Heal units.</Tooltip>
    2733  </Identity>
     34  <Promotion>
     35    <RequiredXp>100</RequiredXp>
     36  </Promotion>
    2837  <Sound>
    2938    <SoundGroups>
    3039      <select>voice/hellenes/civ/civ_male_select.xml</select>
  • 0ad/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>
  • 0ad/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>
  • 0ad/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>
  • 0ad/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>
  • 0ad/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>
  • 0ad/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>
  • 0ad/binaries/data/mods/public/gui/session/session.xml

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

     
    1515const ACTION_NONE = 0;
    1616const ACTION_GARRISON = 1;
    1717const ACTION_REPAIR = 2;
     18const ACTION_HEAL = 3;
    1819var preSelectedAction = ACTION_NONE;
    1920
    2021var INPUT_NORMAL = 0;
     
    255256                }
    256257            }
    257258            break;
     259        case "heal": //TODO add something like && targetState.needsheal ?
     260            if (isUnit(targetState) && (playerOwned || allyOwned))
     261            {
     262                var healableClasses = entState.Ability.Healer.healableClasses;
     263                for each (var unitClass in targetState.identity.classes)
     264                {
     265                    if (healableClasses.indexOf(unitClass) != -1)
     266                    {
     267                        return {"possible": true};
     268                    }
     269                }
     270            }
     271            break;
    258272        case "gather":
    259273            if (targetState.resourceSupply && (playerOwned || gaiaOwned))
    260274            {
     
    350364            else
    351365                return {"type": "none", "cursor": "action-repair-disabled", "target": undefined};
    352366            break;
     367        case ACTION_HEAL:
     368            if (getActionInfo("heal", target).possible)
     369                return {"type": "heal", "cursor": "action-heal", "target": target};
     370            else
     371                return {"type": "none", "cursor": "action-heal-disabled", "target": undefined};
     372            break;
    353373        }
    354374    }
    355375    else if (Engine.HotkeyIsPressed("session.garrison"))
     
    9921012        Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
    9931013        return true;
    9941014
     1015    case "promote":
     1016        Engine.PostNetworkCommand({"type": "promote", "entities": selection, "target": action.target, "queued": queued});
     1017        //TODO play sound???
     1018        return true;
     1019
     1020    case "heal":
     1021        Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued});
     1022        //TODO play sound
     1023//      Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] });
     1024        return true;
     1025
    9951026    case "build": // (same command as repair)
    9961027    case "repair":
    9971028        Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
     
    12041235                inputState = INPUT_PRESELECTEDACTION;
    12051236                preSelectedAction = ACTION_REPAIR;
    12061237                break;
     1238            case "heal":
     1239                inputState = INPUT_PRESELECTEDACTION;
     1240                preSelectedAction = ACTION_HEAL;
     1241                break;
     1242            case "promote":
     1243                doAction({ "type": "promote"})
     1244                break;
    12071245            case "unload-all":
    12081246                unloadAll(entity);
    12091247                break;
  • 0ad/binaries/data/mods/public/gui/session/unit_commands.js

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

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

     
     11 1