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

File #999-2012-02-29.patch, 45.2 KB (added by leper, 12 years ago)

Ready for review

  • 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

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

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

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

     
    2525            "<value a:help='Remain in the world with 0 health'>remain</value>" +
    2626        "</choice>" +
    2727    "</element>" +
    28     "<element name='Healable' a:help='Indicates that the entity can be healed by healer units'>" +
     28    "<element name='Unhealable' a:help='Indicates that the entity can not be healed by healer units'>" +
    2929        "<data type='boolean'/>" +
    3030    "</element>" +
    3131    "<element name='Repairable' a:help='Indicates that the entity can be repaired by builder units'>" +
     
    7272    return (this.template.Repairable == "true");
    7373};
    7474
     75Health.prototype.IsUnhealable = function()
     76{
     77    return (this.template.Unhealable == "true");
     78};
     79
    7580Health.prototype.Kill = function()
    7681{
    7782    this.Reduce(this.hitpoints);
     
    131136{
    132137    // If we're already dead, don't allow resurrection
    133138    if (this.hitpoints == 0)
    134         return;
     139        return false;
    135140
    136141    var old = this.hitpoints;
    137142    this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints());
    138143
    139144    Engine.PostMessage(this.entity, MT_HealthChanged, { "from": old, "to": this.hitpoints });
     145    // We return the old and the actual hp
     146    return { "old": old, "new": this.hitpoints};
    140147};
    141148
    142149//// Private functions ////
  • 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");
     
    270271    GetHitpoints: function() { return 50; },
    271272    GetMaxHitpoints: function() { return 60; },
    272273    IsRepairable: function() { return false; },
     274    IsUnhealable: function() { return false; },
    273275});
    274276
    275277AddMock(10, IID_Identity, {
     
    303305    hitpoints: 50,
    304306    maxHitpoints: 60,
    305307    needsRepair: false,
     308    needsHeal: true,
    306309    buildEntities: ["test1", "test2"],
    307310    barterMarket: {
    308311        prices: { "buy": {"food":150}, "sell": {"food":25} },
  • 0ad/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");
  • 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/**
     1991 * Returns true if the target exists and is unhealable.
     1992 */
     1993UnitAI.prototype.TargetIsUnhealable = function(ent)
     1994{
     1995    var cmpHealth = Engine.QueryInterface(ent, IID_Health);
     1996    if (!cmpHealth)
     1997        return false;
     1998
     1999    return cmpHealth.IsUnhealable();
     2000};
     2001
     2002/**
    17492003 * Returns true if the target exists and needs to be killed before
    17502004 * beginning to gather resources from it.
    17512005 */
     
    22362490        case "Flee":
    22372491        case "LeaveFoundation":
    22382492        case "Attack":
     2493        case "Heal":
    22392494        case "Gather":
    22402495        case "ReturnResource":
    22412496        case "Repair":
     
    23592614    this.AddOrder("GatherNearPosition", { "type": type, "x": position[0], "z": position[1] }, queued);
    23602615}
    23612616
     2617UnitAI.prototype.Heal = function(target, queued)
     2618{
     2619    if (!this.CanHeal(target))
     2620    {
     2621        this.WalkToTarget(target, queued);
     2622        return;
     2623    }
     2624   
     2625    this.AddOrder("Heal", { "target": target, "force": true }, queued);
     2626};
     2627
    23622628UnitAI.prototype.ReturnResource = function(target, queued)
    23632629{
    23642630    if (!this.CanReturnResource(target, true))
     
    23972663        this.stance = stance;
    23982664    else
    23992665        error("UnitAI: Setting to invalid stance '"+stance+"'");
    2400 }
     2666};
    24012667
    24022668UnitAI.prototype.SwitchToStance = function(stance)
    24032669{
     
    24132679
    24142680    // Reset the range query, since the range depends on stance
    24152681    this.SetupRangeQuery();
    2416 }
     2682    // Just if we are a healer
     2683    // TODO maybe move those two to a SetupRangeQuerys()
     2684    if (this.IsHealer())
     2685        this.SetupHealRangeQuery();
     2686};
    24172687
    24182688/**
    24192689 * Resets losRangeQuery, and if there are some targets in range that we can
     
    24342704    return this.RespondToTargetedEntities(ents);
    24352705};
    24362706
    2437 UnitAI.prototype.GetQueryRange = function()
     2707/**
     2708 * Resets losHealRangeQuery, and if there are some targets in range that we can heal
     2709 * then we start healing and this returns true; otherwise, returns false.
     2710 */
     2711UnitAI.prototype.FindNewHealTargets = function()
    24382712{
     2713    if (!this.losHealRangeQuery)
     2714        return false;
     2715   
     2716    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     2717    var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery);
     2718   
     2719    for each (var ent in ents)
     2720    {
     2721        if (this.CanHeal(ent))
     2722        {
     2723            this.PushOrderFront("Heal", { "target": ent, "force": false });
     2724            return true;
     2725        }
     2726    }
     2727    // We haven't found any target to heal
     2728    return false;
     2729};
     2730
     2731UnitAI.prototype.GetQueryRange = function(healer)
     2732{
    24392733    var ret = { "min": 0, "max": 0 };
    2440     if (this.GetStance().respondStandGround)
     2734    // If we have a healer with passive stance we need to set some defaults (eg same as stand ground)
     2735    if (this.GetStance().respondStandGround || (healer && !this.GetStance().respondStandGround && !this.GetStance().respondChase && !this.GetStance().respondHoldGround))
    24412736    {
    2442         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2737        var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack);
    24432738        if (!cmpRanged)
    24442739            return ret;
    2445         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2740        var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack());
    24462741        ret.min = range.min;
    24472742        ret.max = range.max;
    24482743    }
     
    24562751    }
    24572752    else if (this.GetStance().respondHoldGround)
    24582753    {
    2459         var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
     2754        var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack);
    24602755        if (!cmpRanged)
    24612756            return ret;
    2462         var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
     2757        var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack());
    24632758        var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
    24642759        if (!cmpVision)
    24652760            return ret;
     
    25552850    return true;
    25562851};
    25572852
     2853UnitAI.prototype.CanHeal = function(target)
     2854{
     2855    // Formation controllers should always respond to commands
     2856    // (then the individual units can make up their own minds)
     2857    if (this.IsFormationController())
     2858        return true;
     2859
     2860    // Verify that we're able to respond to Heal commands
     2861    var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     2862    if (!cmpHeal)
     2863        return false;
     2864
     2865    // Verify that the target is alive
     2866    if (!this.TargetIsAlive(target))
     2867        return false;
     2868
     2869    // Verify that the target is owned by the same player as the entity or of an ally
     2870    var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     2871    if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
     2872        return false;
     2873   
     2874    // Verify that the target is not unhealable
     2875    if (this.TargetIsUnhealable(target))
     2876    {
     2877        return false;
     2878    }
     2879   
     2880    // Verify that the target has no unhealable class
     2881    // We could also use cmpIdentity.GetClassesList but this way is cleaner
     2882    var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     2883    if (!cmpIdentity)
     2884        return false;
     2885    for each (var unhealableClass in cmpHeal.GetUnhealableClasses())
     2886    {
     2887        if (cmpIdentity.HasClass(unhealableClass) != -1)
     2888        {
     2889            return false;
     2890        }
     2891    }
     2892
     2893    // Verify that the target is a healable class
     2894    // We could also use cmpIdentity.GetClassesList but this way is cleaner
     2895    var healable = false;
     2896    for each (var healableClass in cmpHeal.GetHealableClasses())
     2897    {
     2898        if (cmpIdentity.HasClass(healableClass) != -1)
     2899        {
     2900            healable = true;
     2901        }
     2902    }
     2903    if (!healable)
     2904        return false;
     2905
     2906    // Check that the target is not at MaxHealth
     2907    if (this.TargetIsAtMaxHitpoints(target))
     2908        return false;
     2909
     2910    return true;
     2911};
     2912
    25582913UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
    25592914{
    25602915    // 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        "<UnhealableClasses datatype=\"tokens\">Cavalry</UnhealableClasses>" +
     10        "<HealableClasses datatype=\"tokens\">Support Infantry</HealableClasses>" +
     11    "</a:example>" +
     12    "<element name='Range' a:help='Range (in metres) where healing is possible'>" +
     13        "<ref name='nonNegativeDecimal'/>" +
     14    "</element>" +
     15    "<element name='HP' a:help='Hitpoints healed per Rate'>" +
     16        "<ref name='nonNegativeDecimal'/>" +
     17    "</element>" +
     18    "<element name='Rate' a:help='A heal is performed every Rate ms'>" +
     19        "<ref name='nonNegativeDecimal'/>" +
     20    "</element>" +
     21    "<element name='UnhealableClasses' a:help='If the target has any of these classes it can not be healed (even if it has a class from HealableClasses)'>" +
     22        "<attribute name='datatype'>" +
     23            "<value>tokens</value>" +
     24        "</attribute>" +
     25        "<text/>" +
     26    "</element>" +
     27    "<element name='HealableClasses' a:help='The target must have one of these classes to be healable'>" +
     28        "<attribute name='datatype'>" +
     29            "<value>tokens</value>" +
     30        "</attribute>" +
     31        "<text/>" +
     32    "</element>";
     33
     34Heal.prototype.Init = function()
     35{
     36};
     37
     38Heal.prototype.Serialize = null; // we have no dynamic state to save
     39
     40Heal.prototype.GetTimers = function()
     41{
     42    var prepare = 1000;
     43    var repeat = +(this.template.Rate || 1000);
     44    return { "prepare": prepare, "repeat": repeat };
     45};
     46
     47Heal.prototype.GetRange = function()
     48{
     49    var max = +this.template.Range;
     50    var min = 0;
     51    return { "max": max, "min": min };
     52};
     53
     54Heal.prototype.GetUnhealableClasses = function()
     55{
     56    var classes = this.template.UnhealableClasses._string;
     57    // If we have no unhealable classes defined classes is undefined
     58    return classes?classes.split(/\s+/):"";
     59};
     60
     61Heal.prototype.GetHealableClasses = function()
     62{
     63    var classes = this.template.HealableClasses._string;
     64    return classes.split(/\s+/);
     65};
     66
     67/**
     68 * Heal the target entity. This should only be called after a successful range
     69 * check, and should only be called after GetTimers().repeat msec has passed
     70 * since the last call to PerformHeal.
     71 */
     72Heal.prototype.PerformHeal = function(target)
     73{
     74    this.CauseHeal({"target": target});
     75};
     76
     77/**
     78 * Heal target
     79 */
     80Heal.prototype.CauseHeal = function(data)
     81{
     82    var cmpHealth = Engine.QueryInterface(data.target, IID_Health);
     83    if (!cmpHealth)
     84        return;
     85    var targetState = cmpHealth.Increase(Math.max(0,this.template.HP));
     86
     87    // Add XP
     88    var cmpLoot = Engine.QueryInterface(data.target, IID_Loot);
     89    var cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
     90    if (targetState.old && targetState.new && cmpLoot && cmpPromotion)
     91    {
     92        // HP healed * XP per HP
     93        cmpPromotion.IncreaseXp((targetState.new-targetState.old)*(cmpLoot.GetXp()/cmpHealth.GetMaxHitpoints()));
     94    }
     95    //TODO we need a sound file
     96//  PlaySound("heal_impact", this.entity);
     97};
     98
     99Engine.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    <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>
  • 0ad/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>
  • 0ad/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>
  • 0ad/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>
  • 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/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>
  • 0ad/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>
  • 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":
     260            // The check if the target is unhealable is done by targetState.needsHeal
     261            if (isUnit(targetState) && targetState.needsHeal && (playerOwned || allyOwned))
     262            {
     263                var unhealableClasses = entState.Ability.Healer.unhealableClasses;
     264                for each (var unitClass in targetState.identity.classes)
     265                {
     266                    if (unhealableClasses.indexOf(unitClass) != -1)
     267                    {
     268                        return {"possible": false};
     269                    }
     270                }
     271               
     272                var healableClasses = entState.Ability.Healer.healableClasses;
     273                for each (var unitClass in targetState.identity.classes)
     274                {
     275                    if (healableClasses.indexOf(unitClass) != -1)
     276                    {
     277                        return {"possible": true};
     278                    }
     279                }
     280            }
     281            break;
    258282        case "gather":
    259283            if (targetState.resourceSupply && (playerOwned || gaiaOwned))
    260284            {
     
    350374            else
    351375                return {"type": "none", "cursor": "action-repair-disabled", "target": undefined};
    352376            break;
     377        case ACTION_HEAL:
     378            if (getActionInfo("heal", target).possible)
     379                return {"type": "heal", "cursor": "action-heal", "target": target};
     380            else
     381                return {"type": "none", "cursor": "action-heal-disabled", "target": undefined};
     382            break;
    353383        }
    354384    }
    355385    else if (Engine.HotkeyIsPressed("session.garrison"))
     
    9931023        Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
    9941024        return true;
    9951025
     1026    case "promote":
     1027        Engine.PostNetworkCommand({"type": "promote", "entities": selection, "target": action.target, "queued": queued});
     1028        //TODO play sound???
     1029        return true;
     1030
     1031    case "heal":
     1032        Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued});
     1033        //TODO play sound
     1034//      Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] });
     1035        return true;
     1036
    9961037    case "build": // (same command as repair)
    9971038    case "repair":
    9981039        Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
     
    12051246                inputState = INPUT_PRESELECTEDACTION;
    12061247                preSelectedAction = ACTION_REPAIR;
    12071248                break;
     1249            case "heal":
     1250                inputState = INPUT_PRESELECTEDACTION;
     1251                preSelectedAction = ACTION_HEAL;
     1252                break;
     1253            case "promote":
     1254                doAction({ "type": "promote"})
     1255                break;
    12081256            case "unload-all":
    12091257                unloadAll(entity);
    12101258                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