diff --git a/binaries/data/mods/public/art/actors/units/carthaginians/healer.xml b/binaries/data/mods/public/art/actors/units/carthaginians/healer.xml
index b7259cffab..09fd6489f4 100644
--- a/binaries/data/mods/public/art/actors/units/carthaginians/healer.xml
+++ b/binaries/data/mods/public/art/actors/units/carthaginians/healer.xml
@@ -8,6 +8,7 @@
+
diff --git a/binaries/data/mods/public/art/actors/units/celts/healer.xml b/binaries/data/mods/public/art/actors/units/celts/healer.xml
index a6d87b4c91..960017e91c 100644
--- a/binaries/data/mods/public/art/actors/units/celts/healer.xml
+++ b/binaries/data/mods/public/art/actors/units/celts/healer.xml
@@ -8,6 +8,7 @@
+ skeletal/m_dress_cuffs.dae
diff --git a/binaries/data/mods/public/art/actors/units/hellenes/healer.xml b/binaries/data/mods/public/art/actors/units/hellenes/healer.xml
index ef5a1ad9cd..1df884e6f5 100644
--- a/binaries/data/mods/public/art/actors/units/hellenes/healer.xml
+++ b/binaries/data/mods/public/art/actors/units/hellenes/healer.xml
@@ -7,6 +7,7 @@
+ skeletal/m_dress_a.pmd
diff --git a/binaries/data/mods/public/art/actors/units/iberians/healer.xml b/binaries/data/mods/public/art/actors/units/iberians/healer.xml
index 158992b249..e4d37b11fe 100644
--- a/binaries/data/mods/public/art/actors/units/iberians/healer.xml
+++ b/binaries/data/mods/public/art/actors/units/iberians/healer.xml
@@ -8,6 +8,7 @@
+
diff --git a/binaries/data/mods/public/art/actors/units/persians/healer.xml b/binaries/data/mods/public/art/actors/units/persians/healer.xml
index 94a6a71a84..85aa8bee63 100644
--- a/binaries/data/mods/public/art/actors/units/persians/healer.xml
+++ b/binaries/data/mods/public/art/actors/units/persians/healer.xml
@@ -7,6 +7,7 @@
+ skeletal/m_dress_cuffs.dae
diff --git a/binaries/data/mods/public/art/actors/units/romans/healer.xml b/binaries/data/mods/public/art/actors/units/romans/healer.xml
index 661f858867..3350363a16 100644
--- a/binaries/data/mods/public/art/actors/units/romans/healer.xml
+++ b/binaries/data/mods/public/art/actors/units/romans/healer.xml
@@ -8,6 +8,7 @@
+ skeletal/m_dress_a.pmd
diff --git a/binaries/data/mods/public/art/textures/cursors/action-heal.txt b/binaries/data/mods/public/art/textures/cursors/action-heal.txt
new file mode 100644
index 0000000000..2fb73a07ec
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/cursors/action-heal.txt
@@ -0,0 +1 @@
+1 1
diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js
index 23ed23ab8d..203f8df304 100644
--- a/binaries/data/mods/public/gui/session/input.js
+++ b/binaries/data/mods/public/gui/session/input.js
@@ -299,6 +299,29 @@ function getActionInfo(action, target)
return {"possible": true, "tooltip": tooltip};
}
break;
+ case "heal":
+ // The check if the target is unhealable is done by targetState.needsHeal
+ if (entState.Healer && hasClass(targetState, "Unit") && targetState.needsHeal && (playerOwned || allyOwned))
+ {
+ var unhealableClasses = entState.Healer.unhealableClasses;
+ for each (var unitClass in targetState.identity.classes)
+ {
+ if (unhealableClasses.indexOf(unitClass) != -1)
+ {
+ return {"possible": false};
+ }
+ }
+
+ var healableClasses = entState.Healer.healableClasses;
+ for each (var unitClass in targetState.identity.classes)
+ {
+ if (healableClasses.indexOf(unitClass) != -1)
+ {
+ return {"possible": true};
+ }
+ }
+ }
+ break;
case "gather":
if (targetState.resourceSupply)
{
@@ -419,6 +442,8 @@ function determineAction(x, y, fromMinimap)
return {"type": "build", "cursor": "action-repair", "target": target};
else if ((actionInfo = getActionInfo("set-rallypoint", target)).possible)
return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "position": actionInfo.position};
+ else if (getActionInfo("heal", target).possible)
+ return {"type": "heal", "cursor": "action-heal", "target": target};
else if (getActionInfo("attack", target).possible)
return {"type": "attack", "cursor": "action-attack", "target": target};
else if (getActionInfo("unset-rallypoint", target).possible)
@@ -1039,6 +1064,12 @@ function doAction(action, ev)
Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
return true;
+ case "heal":
+ Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued});
+ // TODO: Play a sound?
+// Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] });
+ return true;
+
case "build": // (same command as repair)
case "repair":
Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
diff --git a/binaries/data/mods/public/gui/session/selection_details.js b/binaries/data/mods/public/gui/session/selection_details.js
index a0a309e1b0..813f79bc41 100644
--- a/binaries/data/mods/public/gui/session/selection_details.js
+++ b/binaries/data/mods/public/gui/session/selection_details.js
@@ -75,7 +75,7 @@ function displaySingle(entState, template)
experienceSize.rtop = 100 - 100 * Math.max(0, Math.min(1, 1.0 * entState.promotion.curr / entState.promotion.req));
experienceBar.size = experienceSize;
- var experience = "[font=\"serif-bold-13\"]Experience [/font]" + entState.promotion.curr;
+ var experience = "[font=\"serif-bold-13\"]Experience [/font]" + Math.floor(entState.promotion.curr);
if (entState.promotion.curr < entState.promotion.req)
experience += "/" + entState.promotion.req;
getGUIObjectByName("experience").tooltip = experience;
diff --git a/binaries/data/mods/public/simulation/components/GarrisonHolder.js b/binaries/data/mods/public/simulation/components/GarrisonHolder.js
index 4455b03f88..1abcd64c9e 100644
--- a/binaries/data/mods/public/simulation/components/GarrisonHolder.js
+++ b/binaries/data/mods/public/simulation/components/GarrisonHolder.js
@@ -320,7 +320,8 @@ GarrisonHolder.prototype.HealTimeout = function(data)
var cmpHealth = Engine.QueryInterface(entity, IID_Health);
if (cmpHealth)
{
- if (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
+ // We do not want to heal unhealable units
+ if (!cmpHealth.IsUnhealable())
cmpHealth.Increase(this.healRate);
}
}
diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js
index 2c4a82f5f3..03228c0882 100644
--- a/binaries/data/mods/public/simulation/components/GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -155,6 +155,7 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
+ ret.needsHeal = !cmpHealth.IsUnhealable();
}
var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
@@ -273,6 +274,15 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
ret.barterMarket = { "prices": cmpBarter.GetPrices() };
}
+ var cmpHeal = Engine.QueryInterface(ent, IID_Heal);
+ if (cmpHeal)
+ {
+ ret.Healer = {
+ "unhealableClasses": cmpHeal.GetUnhealableClasses(),
+ "healableClasses": cmpHeal.GetHealableClasses(),
+ };
+ }
+
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
diff --git a/binaries/data/mods/public/simulation/components/Heal.js b/binaries/data/mods/public/simulation/components/Heal.js
new file mode 100644
index 0000000000..74b7568658
--- /dev/null
+++ b/binaries/data/mods/public/simulation/components/Heal.js
@@ -0,0 +1,91 @@
+function Heal() {}
+
+Heal.prototype.Schema =
+ "Controls the healing abilities of the unit." +
+ "" +
+ "20" +
+ "5" +
+ "2000" +
+ "Cavalry" +
+ "Support Infantry" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "tokens" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "tokens" +
+ "" +
+ "" +
+ "";
+
+Heal.prototype.Init = function()
+{
+};
+
+Heal.prototype.Serialize = null; // we have no dynamic state to save
+
+Heal.prototype.GetTimers = function()
+{
+ var prepare = 1000;
+ var repeat = +this.template.Rate;
+ return { "prepare": prepare, "repeat": repeat };
+};
+
+Heal.prototype.GetRange = function()
+{
+ var max = +this.template.Range;
+ var min = 0;
+ return { "max": max, "min": min };
+};
+
+Heal.prototype.GetUnhealableClasses = function()
+{
+ var classes = this.template.UnhealableClasses._string;
+ // If we have no unhealable classes defined classes is undefined
+ return classes ? classes.split(/\s+/) : [];
+};
+
+Heal.prototype.GetHealableClasses = function()
+{
+ var classes = this.template.HealableClasses._string;
+ return classes.split(/\s+/);
+};
+
+/**
+ * Heal the target entity. This should only be called after a successful range
+ * check, and should only be called after GetTimers().repeat msec has passed
+ * since the last call to PerformHeal.
+ */
+Heal.prototype.PerformHeal = function(target)
+{
+ var cmpHealth = Engine.QueryInterface(target, IID_Health);
+ if (!cmpHealth)
+ return;
+ var targetState = cmpHealth.Increase(Math.max(0,this.template.HP));
+
+ // Add XP
+ var cmpLoot = Engine.QueryInterface(target, IID_Loot);
+ var cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
+ if (targetState!==undefined && cmpLoot && cmpPromotion)
+ {
+ // HP healed * XP per HP
+ cmpPromotion.IncreaseXp((targetState.new-targetState.old)*(cmpLoot.GetXp()/cmpHealth.GetMaxHitpoints()));
+ }
+ //TODO we need a sound file
+// PlaySound("heal_impact", this.entity);
+};
+
+Engine.RegisterComponentType(IID_Heal, "Heal", Heal);
diff --git a/binaries/data/mods/public/simulation/components/Health.js b/binaries/data/mods/public/simulation/components/Health.js
index f65b3becbd..1e002954dc 100644
--- a/binaries/data/mods/public/simulation/components/Health.js
+++ b/binaries/data/mods/public/simulation/components/Health.js
@@ -25,7 +25,7 @@ Health.prototype.Schema =
"remain" +
"" +
"" +
- "" +
+ "" +
"" +
"" +
"" +
@@ -63,6 +63,15 @@ Health.prototype.SetHitpoints = function(value)
var old = this.hitpoints;
this.hitpoints = Math.max(1, Math.min(this.GetMaxHitpoints(), value));
+
+ var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ if (cmpRangeManager)
+ {
+ if (this.hitpoints < this.GetMaxHitpoints())
+ cmpRangeManager.SetEntityFlag(this.entity, "injured", true);
+ else
+ cmpRangeManager.SetEntityFlag(this.entity, "injured", false);
+ }
Engine.PostMessage(this.entity, MT_HealthChanged, { "from": old, "to": this.hitpoints });
};
@@ -72,6 +81,13 @@ Health.prototype.IsRepairable = function()
return (this.template.Repairable == "true");
};
+Health.prototype.IsUnhealable = function()
+{
+ return (this.template.Unhealable == "true"
+ || this.GetHitpoints() <= 0
+ || this.GetHitpoints() >= this.GetMaxHitpoints());
+};
+
Health.prototype.Kill = function()
{
this.Reduce(this.hitpoints);
@@ -80,6 +96,12 @@ Health.prototype.Kill = function()
Health.prototype.Reduce = function(amount)
{
var state = { "killed": false };
+ if (amount >= 0 && this.hitpoints == this.GetMaxHitpoints())
+ {
+ var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ if (cmpRangeManager)
+ cmpRangeManager.SetEntityFlag(this.entity, "injured", true);
+ }
if (amount >= this.hitpoints)
{
// If this is the first time we reached 0, then die.
@@ -131,12 +153,21 @@ Health.prototype.Increase = function(amount)
{
// If we're already dead, don't allow resurrection
if (this.hitpoints == 0)
- return;
+ return undefined;
var old = this.hitpoints;
this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints());
+
+ if (this.hitpoints == this.GetMaxHitpoints())
+ {
+ var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ if (cmpRangeManager)
+ cmpRangeManager.SetEntityFlag(this.entity, "injured", false);
+ }
Engine.PostMessage(this.entity, MT_HealthChanged, { "from": old, "to": this.hitpoints });
+ // We return the old and the actual hp
+ return { "old": old, "new": this.hitpoints};
};
//// Private functions ////
diff --git a/binaries/data/mods/public/simulation/components/Loot.js b/binaries/data/mods/public/simulation/components/Loot.js
index 6ae5242b7d..3161340845 100644
--- a/binaries/data/mods/public/simulation/components/Loot.js
+++ b/binaries/data/mods/public/simulation/components/Loot.js
@@ -21,7 +21,7 @@ Loot.prototype.Serialize = null; // we have no dynamic state to save
Loot.prototype.GetXp = function()
{
- return this.template.xp;
+ return +(this.template.xp || 0);
};
Loot.prototype.GetResources = function()
diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js
index 23ca9cd31c..008f5bc331 100644
--- a/binaries/data/mods/public/simulation/components/UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/UnitAI.js
@@ -131,6 +131,10 @@ var UnitFsmSpec = {
"LosRangeUpdate": function(msg) {
// ignore newly-seen units by default
},
+
+ "LosHealRangeUpdate": function(msg) {
+ // ignore newly-seen injured units by default
+ },
"Attacked": function(msg) {
// ignore attacker
@@ -289,6 +293,43 @@ var UnitFsmSpec = {
this.FinishOrder();
},
+ "Order.Heal": function(msg) {
+ // Check the target is alive
+ if (!this.TargetIsAlive(this.order.data.target))
+ {
+ this.FinishOrder();
+ return;
+ }
+
+ // Check if the target is in range
+ if (this.CheckTargetRange(this.order.data.target, IID_Heal))
+ {
+ this.StopMoving();
+ this.SetNextState("INDIVIDUAL.HEAL.HEALING");
+ return;
+ }
+
+ // If we can't reach the target, but are standing ground,
+ // then abandon this heal order
+ if (this.GetStance().respondStandGround && !this.order.data.force)
+ {
+ this.FinishOrder();
+ return;
+ }
+
+ // Try to move within heal range
+ if (this.MoveToTargetRange(this.order.data.target, IID_Heal))
+ {
+ // We've started walking to the given point
+ this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
+ return;
+ }
+
+ // We can't reach the target, and can't move towards it,
+ // so abandon this heal order
+ this.FinishOrder();
+ },
+
"Order.Gather": function(msg) {
// If the target is still alive, we need to kill it first
@@ -414,6 +455,13 @@ var UnitFsmSpec = {
cmpFormation.Disband();
},
+ "Order.Heal": function(msg) {
+ // TODO: see notes in Order.Attack
+ var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+ cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]);
+ cmpFormation.Disband();
+ },
+
"Order.Repair": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
@@ -556,6 +604,13 @@ var UnitFsmSpec = {
// remain idle
this.StartTimer(1000);
+ // If a unit can heal and attack we first want to heal wounded units,
+ // so check if we are a healer and find whether there's anybody nearby to heal.
+ // (If anyone approaches later it'll be handled via LosHealRangeUpdate.)
+ // If anyone in sight gets hurt that will be handled via LosHealRangeUpdate.
+ if (this.IsHealer() && this.FindNewHealTargets())
+ return true; // (abort the FSM transition since we may have already switched state)
+
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
@@ -569,6 +624,8 @@ var UnitFsmSpec = {
"leave": function() {
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
rangeMan.DisableActiveQuery(this.losRangeQuery);
+ if (this.losHealRangeQuery)
+ rangeMan.DisableActiveQuery(this.losHealRangeQuery);
this.StopTimer();
@@ -586,6 +643,10 @@ var UnitFsmSpec = {
this.RespondToTargetedEntities(msg.data.added);
}
},
+
+ "LosHealRangeUpdate": function(msg) {
+ this.RespondToHealableEntities(msg.data.added);
+ },
"Timer": function(msg) {
if (!this.isIdle)
@@ -1009,6 +1070,124 @@ var UnitFsmSpec = {
},
},
+ "HEAL": {
+ "EntityRenamed": function(msg) {
+ if (this.order.data.target == msg.entity)
+ this.order.data.target = msg.newentity;
+ },
+
+ "Attacked": function(msg) {
+ // If we stand ground we will rather die than flee
+ if (!this.GetStance().respondStandGround)
+ this.Flee(msg.data.attacker, false);
+ },
+
+ "APPROACHING": {
+ "enter": function () {
+ this.SelectAnimation("move");
+ this.StartTimer(1000, 1000);
+ },
+
+ "leave": function() {
+ this.StopTimer();
+ },
+
+ "Timer": function(msg) {
+ if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
+ {
+ this.StopMoving();
+ this.FinishOrder();
+
+ // Return to our original position
+ if (this.GetStance().respondHoldGround)
+ this.WalkToHeldPosition();
+ }
+ },
+
+ "MoveCompleted": function() {
+ this.SetNextState("HEALING");
+ },
+ },
+
+ "HEALING": {
+ "enter": function() {
+ var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
+ this.healTimers = cmpHeal.GetTimers();
+ this.SelectAnimation("heal", false, 1.0, "heal");
+ this.SetAnimationSync(this.healTimers.prepare, this.healTimers.repeat);
+ this.StartTimer(this.healTimers.prepare, this.healTimers.repeat);
+ // TODO if .prepare is short, players can cheat by cycling heal/stop/heal
+ // to beat the .repeat time; should enforce a minimum time
+ // see comment in ATTACKING.enter
+ this.FaceTowardsTarget(this.order.data.target);
+ },
+
+ "leave": function() {
+ this.StopTimer();
+ },
+
+ "Timer": function(msg) {
+ var target = this.order.data.target;
+ // Check the target is still alive and healable
+ if (this.TargetIsAlive(target) && this.CanHeal(target))
+ {
+ // Check if we can still reach the target
+ if (this.CheckTargetRange(target, IID_Heal))
+ {
+ this.FaceTowardsTarget(target);
+ var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
+ cmpHeal.PerformHeal(target);
+ return;
+ }
+ // Can't reach it - try to chase after it
+ if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
+ {
+ if (this.MoveToTargetRange(target, IID_Heal))
+ {
+ this.SetNextState("HEAL.CHASING");
+ return;
+ }
+ }
+ }
+ // Can't reach it, healed to max hp or doesn't exist any more - give up
+ if (this.FinishOrder())
+ return;
+
+ // Heal another one
+ if (this.FindNewHealTargets())
+ return;
+
+ // Return to our original position
+ if (this.GetStance().respondHoldGround)
+ this.WalkToHeldPosition();
+ },
+ },
+ "CHASING": {
+ "enter": function () {
+ this.SelectAnimation("move");
+ this.StartTimer(1000, 1000);
+ },
+
+ "leave": function () {
+ this.StopTimer();
+ },
+ "Timer": function(msg) {
+ if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
+ {
+ this.StopMoving();
+ this.FinishOrder();
+
+ // Return to our original position
+ if (this.GetStance().respondHoldGround)
+ this.WalkToHeldPosition();
+ }
+ },
+ "MoveCompleted": function () {
+ this.SetNextState("HEALING");
+ },
+ },
+ },
+
// Returning to dropsite
"RETURNRESOURCE": {
"APPROACHING": {
@@ -1493,6 +1672,11 @@ UnitAI.prototype.IsDomestic = function()
return cmpIdentity.HasClass("Domestic");
};
+UnitAI.prototype.IsHealer = function()
+{
+ return Engine.QueryInterface(this.entity, IID_Heal);
+};
+
UnitAI.prototype.IsIdle = function()
{
return this.isIdle;
@@ -1521,6 +1705,8 @@ UnitAI.prototype.StateChanged = function()
UnitAI.prototype.OnOwnershipChanged = function(msg)
{
this.SetupRangeQuery();
+ if (this.IsHealer())
+ this.SetupHealRangeQuery();
};
UnitAI.prototype.OnDestroy = function()
@@ -1532,6 +1718,8 @@ UnitAI.prototype.OnDestroy = function()
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
rangeMan.DestroyActiveQuery(this.losRangeQuery);
+ if (this.losHealRangeQuery)
+ rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
};
// Set up a range query for all enemy units within LOS range
@@ -1564,12 +1752,47 @@ UnitAI.prototype.SetupRangeQuery = function()
}
}
- var range = this.GetQueryRange();
+ var range = this.GetQueryRange(IID_Attack);
- this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver);
+ this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, rangeMan.GetEntityFlagMask("normal"));
rangeMan.EnableActiveQuery(this.losRangeQuery);
};
+// Set up a range query for all own or ally units within LOS range
+// which can be healed.
+// This should be called whenever our ownership changes.
+UnitAI.prototype.SetupHealRangeQuery = function()
+{
+ var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ var owner = cmpOwnership.GetOwner();
+
+ var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
+
+ if (this.losHealRangeQuery)
+ rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
+
+ var players = [owner];
+
+ if (owner != -1)
+ {
+ // If unit not just killed, get ally players via diplomacy
+ var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
+ var numPlayers = playerMan.GetNumPlayers();
+ for (var i = 1; i < numPlayers; ++i)
+ {
+ // Exclude gaia and enemies
+ if (cmpPlayer.IsAlly(i))
+ players.push(i);
+ }
+ }
+
+ var range = this.GetQueryRange(IID_Heal);
+
+ this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, rangeMan.GetEntityFlagMask("injured"));
+ rangeMan.EnableActiveQuery(this.losHealRangeQuery);
+};
+
//// FSM linkage functions ////
UnitAI.prototype.SetNextState = function(state)
@@ -1814,6 +2037,8 @@ UnitAI.prototype.OnRangeUpdate = function(msg)
{
if (msg.tag == this.losRangeQuery)
UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
+ else if (msg.tag == this.losHealRangeQuery)
+ UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg});
};
//// Helper functions to be called by the FSM ////
@@ -2226,6 +2451,27 @@ UnitAI.prototype.RespondToTargetedEntities = function(ents)
return false;
};
+/**
+ * Try to respond to healable entities.
+ * Returns true if it responded.
+ */
+UnitAI.prototype.RespondToHealableEntities = function(ents)
+{
+ if (!ents.length)
+ return false;
+
+ for each (var ent in ents)
+ {
+ if (this.CanHeal(ent))
+ {
+ this.PushOrderFront("Heal", { "target": ent, "force": false });
+ return true;
+ }
+ }
+
+ return false;
+};
+
/**
* Returns true if we should stop following the target entity.
*/
@@ -2341,6 +2587,7 @@ UnitAI.prototype.ComputeWalkingDistance = function()
case "Flee":
case "LeaveFoundation":
case "Attack":
+ case "Heal":
case "Gather":
case "ReturnResource":
case "Repair":
@@ -2403,7 +2650,12 @@ UnitAI.prototype.Attack = function(target, queued)
{
if (!this.CanAttack(target))
{
- this.WalkToTarget(target, queued);
+ // We don't want to let healers walk to the target unit so they can be easily killed.
+ // Instead we just let them get into healing range.
+ if (this.IsHealer())
+ this.MoveToTargetRange(target, IID_Heal);
+ else
+ this.WalkToTarget(target, queued);
return;
}
@@ -2464,6 +2716,17 @@ UnitAI.prototype.GatherNearPosition = function(x, z, type, queued)
this.AddOrder("GatherNearPosition", { "type": type, "x": x, "z": z }, queued);
}
+UnitAI.prototype.Heal = function(target, queued)
+{
+ if (!this.CanHeal(target))
+ {
+ this.WalkToTarget(target, queued);
+ return;
+ }
+
+ this.AddOrder("Heal", { "target": target, "force": true }, queued);
+};
+
UnitAI.prototype.ReturnResource = function(target, queued)
{
if (!this.CanReturnResource(target, true))
@@ -2575,7 +2838,7 @@ UnitAI.prototype.SetStance = function(stance)
this.stance = stance;
else
error("UnitAI: Setting to invalid stance '"+stance+"'");
-}
+};
UnitAI.prototype.SwitchToStance = function(stance)
{
@@ -2593,7 +2856,11 @@ UnitAI.prototype.SwitchToStance = function(stance)
// Reset the range query, since the range depends on stance
this.SetupRangeQuery();
-}
+ // Just if we are a healer
+ // TODO maybe move those two to a SetupRangeQuerys()
+ if (this.IsHealer())
+ this.SetupHealRangeQuery();
+};
/**
* Resets losRangeQuery, and if there are some targets in range that we can
@@ -2614,15 +2881,39 @@ UnitAI.prototype.FindNewTargets = function()
return this.RespondToTargetedEntities(ents);
};
-UnitAI.prototype.GetQueryRange = function()
+/**
+ * Resets losHealRangeQuery, and if there are some targets in range that we can heal
+ * then we start healing and this returns true; otherwise, returns false.
+ */
+UnitAI.prototype.FindNewHealTargets = function()
+{
+ if (!this.losHealRangeQuery)
+ return false;
+
+ var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery);
+
+ for each (var ent in ents)
+ {
+ if (this.CanHeal(ent))
+ {
+ this.PushOrderFront("Heal", { "target": ent, "force": false });
+ return true;
+ }
+ }
+ // We haven't found any target to heal
+ return false;
+};
+
+UnitAI.prototype.GetQueryRange = function(iid)
{
var ret = { "min": 0, "max": 0 };
if (this.GetStance().respondStandGround)
{
- var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
+ var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
- var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
+ var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack());
ret.min = range.min;
ret.max = range.max;
}
@@ -2636,16 +2927,27 @@ UnitAI.prototype.GetQueryRange = function()
}
else if (this.GetStance().respondHoldGround)
{
- var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
+ var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
- var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
+ var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack());
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var halfvision = cmpVision.GetRange() / 2;
ret.max = range.max + halfvision;
}
+ // We probably have stance 'passive' and we wouldn't have a range,
+ // but as it is the default for healers we need to set it to something sane.
+ else if (iid === IID_Heal)
+ {
+ var cmpRanged = Engine.QueryInterface(this.entity, iid);
+ if (!cmpRanged)
+ return ret;
+ var range = cmpRanged.GetRange();
+ ret.min = range.min;
+ ret.max = range.max;
+ }
return ret;
};
@@ -2733,6 +3035,59 @@ UnitAI.prototype.CanGather = function(target)
return true;
};
+UnitAI.prototype.CanHeal = function(target)
+{
+ // Formation controllers should always respond to commands
+ // (then the individual units can make up their own minds)
+ if (this.IsFormationController())
+ return true;
+
+ // Verify that we're able to respond to Heal commands
+ var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
+ if (!cmpHeal)
+ return false;
+
+ // Verify that the target is alive
+ if (!this.TargetIsAlive(target))
+ return false;
+
+ // Verify that the target is owned by the same player as the entity or of an ally
+ var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
+ return false;
+
+ // Verify that the target is not unhealable (or at max health)
+ var cmpHealth = Engine.QueryInterface(target, IID_Health);
+ if (!cmpHealth || cmpHealth.IsUnhealable())
+ return false;
+
+ // Verify that the target has no unhealable class
+ var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
+ if (!cmpIdentity)
+ return false;
+ for each (var unhealableClass in cmpHeal.GetUnhealableClasses())
+ {
+ if (cmpIdentity.HasClass(unhealableClass) != -1)
+ {
+ return false;
+ }
+ }
+
+ // Verify that the target is a healable class
+ var healable = false;
+ for each (var healableClass in cmpHeal.GetHealableClasses())
+ {
+ if (cmpIdentity.HasClass(healableClass) != -1)
+ {
+ healable = true;
+ }
+ }
+ if (!healable)
+ return false;
+
+ return true;
+};
+
UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
{
// Formation controllers should always respond to commands
diff --git a/binaries/data/mods/public/simulation/components/interfaces/Heal.js b/binaries/data/mods/public/simulation/components/interfaces/Heal.js
new file mode 100644
index 0000000000..5f88a9f8f5
--- /dev/null
+++ b/binaries/data/mods/public/simulation/components/interfaces/Heal.js
@@ -0,0 +1 @@
+Engine.RegisterInterface("Heal");
diff --git a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
index 5e5ecec2ad..ac7c1ef299 100644
--- a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
@@ -5,6 +5,7 @@ Engine.LoadComponentScript("interfaces/BuildLimits.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
+Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/RallyPoint.js");
@@ -271,6 +272,7 @@ AddMock(10, IID_Health, {
GetHitpoints: function() { return 50; },
GetMaxHitpoints: function() { return 60; },
IsRepairable: function() { return false; },
+ IsUnhealable: function() { return false; },
});
AddMock(10, IID_Identity, {
@@ -304,6 +306,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
hitpoints: 50,
maxHitpoints: 60,
needsRepair: false,
+ needsHeal: true,
buildEntities: ["test1", "test2"],
barterMarket: {
prices: { "buy": {"food":150}, "sell": {"food":25} },
diff --git a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
index 0a22c0f32e..16580b1ae6 100644
--- a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
@@ -4,6 +4,7 @@ Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/Formation.js");
+Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/Timer.js");
@@ -33,12 +34,13 @@ function TestFormationExiting(mode)
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
- CreateActiveQuery: function(ent, minRange, maxRange, players, iid) {
+ CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) {
return 1;
},
EnableActiveQuery: function(id) { },
ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; },
DisableActiveQuery: function(id) { },
+ GetEntityFlagMask: function(identifier) { },
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js
index 4d07c2e3a4..78cacff275 100644
--- a/binaries/data/mods/public/simulation/helpers/Commands.js
+++ b/binaries/data/mods/public/simulation/helpers/Commands.js
@@ -66,6 +66,20 @@ function ProcessCommand(player, cmd)
});
break;
+ case "heal":
+ if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
+ {
+ // This check is for debugging only!
+ warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
+ }
+
+ // See UnitAI.CanHeal for target checks
+ var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
+ GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
+ cmpUnitAI.Heal(cmd.target, cmd.queued);
+ });
+ break;
+
case "repair":
// This covers both repairing damaged buildings, and constructing unfinished foundations
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
diff --git a/binaries/data/mods/public/simulation/templates/template_structure.xml b/binaries/data/mods/public/simulation/templates/template_structure.xml
index d87661387a..d27bdd5714 100644
--- a/binaries/data/mods/public/simulation/templates/template_structure.xml
+++ b/binaries/data/mods/public/simulation/templates/template_structure.xml
@@ -33,7 +33,7 @@
corpse0
- false
+ truetrue
diff --git a/binaries/data/mods/public/simulation/templates/template_unit.xml b/binaries/data/mods/public/simulation/templates/template_unit.xml
index c2ecdeed7e..d539d87364 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit.xml
@@ -30,7 +30,7 @@
corpse1000
- true
+ falsefalse
diff --git a/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml b/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml
index dca5794316..100b8158d4 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml
@@ -4,7 +4,7 @@
100remain1
- false
+ truefalse
diff --git a/binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml b/binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml
index cd9ca9c00a..3a73cc7dbc 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml
@@ -7,7 +7,7 @@
2.0
- false
+ truetrue
diff --git a/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml b/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml
index 2c891a00d3..c01e829989 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml
@@ -5,12 +5,13 @@
2.02.0
-
-
- 20
- 2000
-
-
+
+ 30
+ 5
+ 2000
+
+ Organic
+ 120
@@ -23,8 +24,11 @@
HealerHealer
- Heal units within his Aura. (Not implemented yet)
+ Heals units.
+
+ 100
+
diff --git a/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml b/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml
index c90ad5f5be..67bc476862 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml
@@ -36,7 +36,7 @@
- false
+ trueSlave
diff --git a/binaries/data/mods/public/simulation/templates/units/cart_support_healer_b.xml b/binaries/data/mods/public/simulation/templates/units/cart_support_healer_b.xml
index 89ff0c149f..803837bea5 100644
--- a/binaries/data/mods/public/simulation/templates/units/cart_support_healer_b.xml
+++ b/binaries/data/mods/public/simulation/templates/units/cart_support_healer_b.xml
@@ -6,7 +6,11 @@
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. Heal units within her aura. (Not implemented yet)units/cart_support_healer.png
+ Basic
+
+ units/cart_support_healer_a
+ units/carthaginians/healer.xml
diff --git a/binaries/data/mods/public/simulation/templates/units/celt_support_healer_b.xml b/binaries/data/mods/public/simulation/templates/units/celt_support_healer_b.xml
index d8be2d2dcf..c1543e101a 100644
--- a/binaries/data/mods/public/simulation/templates/units/celt_support_healer_b.xml
+++ b/binaries/data/mods/public/simulation/templates/units/celt_support_healer_b.xml
@@ -5,7 +5,11 @@
Druides 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.units/celt_support_healer.png
+ Basic
+
+ units/celt_support_healer_a
+ units/celts/healer.xml
diff --git a/binaries/data/mods/public/simulation/templates/units/hele_support_healer_b.xml b/binaries/data/mods/public/simulation/templates/units/hele_support_healer_b.xml
index da383f9464..4512c66839 100644
--- a/binaries/data/mods/public/simulation/templates/units/hele_support_healer_b.xml
+++ b/binaries/data/mods/public/simulation/templates/units/hele_support_healer_b.xml
@@ -5,7 +5,11 @@
HiereúsThe 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.units/hele_support_healer.png
+ Basic
+
+ units/hele_support_healer_a
+ units/hellenes/healer.xml
diff --git a/binaries/data/mods/public/simulation/templates/units/iber_support_healer_b.xml b/binaries/data/mods/public/simulation/templates/units/iber_support_healer_b.xml
index d9f7199105..6fd88fbfe2 100644
--- a/binaries/data/mods/public/simulation/templates/units/iber_support_healer_b.xml
+++ b/binaries/data/mods/public/simulation/templates/units/iber_support_healer_b.xml
@@ -5,7 +5,11 @@
Sacerdotisa de Ataekina 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.units/iber_support_healer.png
+ Basic
+
+ units/iber_support_healer_a
+ units/iberians/healer.xml
diff --git a/binaries/data/mods/public/simulation/templates/units/pers_support_healer_b.xml b/binaries/data/mods/public/simulation/templates/units/pers_support_healer_b.xml
index 125bc2119a..efc9d90011 100644
--- a/binaries/data/mods/public/simulation/templates/units/pers_support_healer_b.xml
+++ b/binaries/data/mods/public/simulation/templates/units/pers_support_healer_b.xml
@@ -6,7 +6,11 @@
Maguš MadaUnder 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.units/pers_support_healer.png
+ Basic
+
+ units/pers_support_healer_a
+ units/persians/healer.xml
diff --git a/binaries/data/mods/public/simulation/templates/units/rome_support_healer_b.xml b/binaries/data/mods/public/simulation/templates/units/rome_support_healer_b.xml
index bf44fdd3e0..a2a4889ba8 100644
--- a/binaries/data/mods/public/simulation/templates/units/rome_support_healer_b.xml
+++ b/binaries/data/mods/public/simulation/templates/units/rome_support_healer_b.xml
@@ -6,7 +6,11 @@
Pontifex MinorisDuring 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.units/rome_support_healer.png
+ Basic
+
+ units/rome_support_healer_a
+ units/romans/healer.xml
diff --git a/source/scriptinterface/ScriptConversions.cpp b/source/scriptinterface/ScriptConversions.cpp
index 818bc721fd..b7a22840f7 100644
--- a/source/scriptinterface/ScriptConversions.cpp
+++ b/source/scriptinterface/ScriptConversions.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2011 Wildfire Games.
+/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -90,6 +90,16 @@ template<> bool ScriptInterface::FromJSVal(JSContext* cx, jsval v, u16& out
return true;
}
+template<> bool ScriptInterface::FromJSVal(JSContext* cx, jsval v, u8& out)
+{
+ uint16 ret;
+ WARN_IF_NOT(JSVAL_IS_NUMBER(v), v);
+ if (!JS_ValueToUint16(cx, v, &ret))
+ return false;
+ out = (u8)ret;
+ return true;
+}
+
// NOTE: we can't define a jsval specialisation, because that conflicts with integer types
template<> bool ScriptInterface::FromJSVal(JSContext* UNUSED(cx), jsval v, CScriptVal& out)
{
@@ -196,6 +206,11 @@ template<> jsval ScriptInterface::ToJSVal(JSContext* UNUSED(cx), const u16&
return INT_TO_JSVAL(val);
}
+template<> jsval ScriptInterface::ToJSVal(JSContext* UNUSED(cx), const u8& val)
+{
+ return INT_TO_JSVAL(val);
+}
+
template<> jsval ScriptInterface::ToJSVal(JSContext* cx, const u32& val)
{
if (val <= JSVAL_INT_MAX)
diff --git a/source/scriptinterface/ScriptInterface.h b/source/scriptinterface/ScriptInterface.h
index 1d10f88037..4ebced7df5 100644
--- a/source/scriptinterface/ScriptInterface.h
+++ b/source/scriptinterface/ScriptInterface.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2011 Wildfire Games.
+/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -38,7 +38,7 @@ class AutoGCRooter;
// Set the maximum number of function arguments that can be handled
// (This should be as small as possible (for compiler efficiency),
// but as large as necessary for all wrapped functions)
-#define SCRIPT_INTERFACE_MAX_ARGS 6
+#define SCRIPT_INTERFACE_MAX_ARGS 7
// TODO: what's a good default?
#define DEFAULT_RUNTIME_SIZE 16 * 1024 * 1024
diff --git a/source/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp
index efaf69c23f..5de5d34c00 100644
--- a/source/simulation2/components/CCmpRangeManager.cpp
+++ b/source/simulation2/components/CCmpRangeManager.cpp
@@ -50,6 +50,7 @@ struct Query
u32 ownersMask;
i32 interface;
std::vector lastMatch;
+ u8 flagsMask;
};
/**
@@ -69,12 +70,13 @@ static u32 CalcOwnerMask(i32 owner)
*/
struct EntityData
{
- EntityData() : retainInFog(0), owner(-1), inWorld(0) { }
+ EntityData() : retainInFog(0), owner(-1), inWorld(0), flags(1) { }
entity_pos_t x, z;
entity_pos_t visionRange;
u8 retainInFog; // boolean
i8 owner;
u8 inWorld; // boolean
+ u8 flags; // See GetEntityFlagMask
};
cassert(sizeof(EntityData) == 16);
@@ -95,6 +97,7 @@ struct SerializeQuery
serialize.NumberU32_Unbounded("owners mask", value.ownersMask);
serialize.NumberI32_Unbounded("interface", value.interface);
SerializeVector()(serialize, "last match", value.lastMatch);
+ serialize.NumberU8("flagsMask", value.flagsMask, 0, -1);
}
};
@@ -112,6 +115,7 @@ struct SerializeEntityData
serialize.NumberU8("retain in fog", value.retainInFog, 0, 1);
serialize.NumberI8_Unbounded("owner", value.owner);
serialize.NumberU8("in world", value.inWorld, 0, 1);
+ serialize.NumberU8("flags", value.flags, 0, -1);
}
};
@@ -514,10 +518,10 @@ public:
virtual tag_t CreateActiveQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange,
- std::vector owners, int requiredInterface)
+ std::vector owners, int requiredInterface, u8 flags)
{
tag_t id = m_QueryNext++;
- m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface);
+ m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags);
return id;
}
@@ -565,7 +569,7 @@ public:
{
PROFILE("ExecuteQuery");
- Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface);
+ Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"));
std::vector r;
@@ -709,6 +713,10 @@ public:
if (!entity.inWorld)
return false;
+ // Ignore entities that don't match the current flags
+ if (!(entity.flags & q.flagsMask))
+ return false;
+
// Ignore self
if (id == q.source)
return false;
@@ -773,7 +781,7 @@ public:
Query ConstructQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange,
- const std::vector& owners, int requiredInterface)
+ const std::vector& owners, int requiredInterface, u8 flagsMask)
{
// Min range must be non-negative
if (minRange < entity_pos_t::Zero())
@@ -794,6 +802,7 @@ public:
q.ownersMask |= CalcOwnerMask(owners[i]);
q.interface = requiredInterface;
+ q.flagsMask = flagsMask;
return q;
}
@@ -859,6 +868,42 @@ public:
for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i)
collector.Submit(&m_DebugOverlayLines[i]);
}
+
+ virtual u8 GetEntityFlagMask(std::string identifier)
+ {
+ CStr id = CStr(identifier);
+
+ if (id == "normal")
+ return 1;
+ if (id == "injured")
+ return 2;
+
+ LOGWARNING(L"CCmpRangeManager: Invalid flag identifier %s", id.c_str());
+ return 0;
+ }
+
+ virtual void SetEntityFlag(entity_id_t ent, std::string identifier, bool value)
+ {
+ std::map::iterator it = m_EntityData.find(ent);
+
+ // We don't have this entity
+ if (it == m_EntityData.end())
+ return;
+
+ u8 flag = GetEntityFlagMask(identifier);
+
+ // We don't have a flag set
+ if (flag == 0)
+ {
+ LOGWARNING(L"CCmpRangeManager: Invalid flag identifier %s for entity %u", identifier.c_str(), ent);
+ return;
+ }
+
+ if (value)
+ it->second.flags |= flag;
+ else
+ it->second.flags &= !flag;
+ }
// ****************************************************************
diff --git a/source/simulation2/components/ICmpRangeManager.cpp b/source/simulation2/components/ICmpRangeManager.cpp
index 29f0bafe61..4503ef2596 100644
--- a/source/simulation2/components/ICmpRangeManager.cpp
+++ b/source/simulation2/components/ICmpRangeManager.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2010 Wildfire Games.
+/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -35,11 +35,13 @@ std::string ICmpRangeManager::GetLosVisibility_wrapper(entity_id_t ent, int play
BEGIN_INTERFACE_WRAPPER(RangeManager)
DEFINE_INTERFACE_METHOD_5("ExecuteQuery", std::vector, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int)
-DEFINE_INTERFACE_METHOD_5("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int)
+DEFINE_INTERFACE_METHOD_6("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int, u8)
DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyActiveQuery, ICmpRangeManager::tag_t)
DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t)
DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t)
DEFINE_INTERFACE_METHOD_1("ResetActiveQuery", std::vector, ICmpRangeManager, ResetActiveQuery, ICmpRangeManager::tag_t)
+DEFINE_INTERFACE_METHOD_3("SetEntityFlag", void, ICmpRangeManager, SetEntityFlag, entity_id_t, std::string, bool)
+DEFINE_INTERFACE_METHOD_1("GetEntityFlagMask", u8, ICmpRangeManager, GetEntityFlagMask, std::string)
DEFINE_INTERFACE_METHOD_1("GetEntitiesByPlayer", std::vector, ICmpRangeManager, GetEntitiesByPlayer, player_id_t)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool)
DEFINE_INTERFACE_METHOD_2("SetLosRevealAll", void, ICmpRangeManager, SetLosRevealAll, player_id_t, bool)
diff --git a/source/simulation2/components/ICmpRangeManager.h b/source/simulation2/components/ICmpRangeManager.h
index 2f77c7ec2e..142936e016 100644
--- a/source/simulation2/components/ICmpRangeManager.h
+++ b/source/simulation2/components/ICmpRangeManager.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2010 Wildfire Games.
+/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -96,10 +96,11 @@ public:
* @param maxRange non-negative maximum distance in metres (inclusive); or -1.0 to ignore distance.
* @param owners list of player IDs that matching entities may have; -1 matches entities with no owner.
* @param requiredInterface if non-zero, an interface ID that matching entities must implement.
+ * @param flags if a entity in range has one of the flags set it will show up.
* @return unique non-zero identifier of query.
*/
virtual tag_t CreateActiveQuery(entity_id_t source,
- entity_pos_t minRange, entity_pos_t maxRange, std::vector owners, int requiredInterface) = 0;
+ entity_pos_t minRange, entity_pos_t maxRange, std::vector owners, int requiredInterface, u8 flags) = 0;
/**
* Destroy a query and clean up resources. This must be called when an entity no longer needs its
@@ -142,6 +143,19 @@ public:
*/
virtual void SetDebugOverlay(bool enabled) = 0;
+ /**
+ * Returns the mask for the specified identifier.
+ */
+ virtual u8 GetEntityFlagMask(std::string identifier) = 0;
+
+ /**
+ * Set the flag specified by the identifier to the supplied value for the entity
+ * @param ent the entity whose flags will be modified.
+ * @param identifier the flag to be modified.
+ * @param value to which the flag will be set.
+ */
+ virtual void SetEntityFlag(entity_id_t ent, std::string identifier, bool value) = 0;
+
// LOS interface:
enum ELosState
diff --git a/source/simulation2/system/InterfaceScripted.h b/source/simulation2/system/InterfaceScripted.h
index f487701121..f111586542 100644
--- a/source/simulation2/system/InterfaceScripted.h
+++ b/source/simulation2/system/InterfaceScripted.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2010 Wildfire Games.
+/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -79,4 +79,10 @@
5, \
JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT },
+#define DEFINE_INTERFACE_METHOD_6(scriptname, rettype, classname, methodname, arg1, arg2, arg3, arg4, arg5, arg6) \
+ { scriptname, \
+ ScriptInterface::callMethod, \
+ 6, \
+ JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT },
+
#endif // INCLUDED_INTERFACE_SCRIPTED