Index: binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- binaries/data/mods/public/gui/common/tooltips.js
+++ binaries/data/mods/public/gui/common/tooltips.js
@@ -207,7 +207,7 @@
function armorLevelToPercentageString(level)
{
return sprintf(translate("%(percentage)s%%"), {
- "percentage": (100 - Math.round(Math.pow(0.9, level) * 100))
+ "percentage": level == "Infinity" ? 100 : (100 - Math.round(Math.pow(0.9, level) * 100))
});
}
@@ -221,7 +221,7 @@
"details":
Object.keys(template.armour).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
- "damage": template.armour[dmgType].toFixed(1),
+ "damage": template.armour[dmgType] == "Infinity" ? "∞" : template.armour[dmgType].toFixed(1),
"damageType": unitFont(translateWithContext("damage type", g_DamageTypes.GetNames()[dmgType])),
"armorPercentage":
'[font="sans-10"]' +
Index: binaries/data/mods/public/simulation/ai/common-api/entity.js
===================================================================
--- binaries/data/mods/public/simulation/ai/common-api/entity.js
+++ binaries/data/mods/public/simulation/ai/common-api/entity.js
@@ -741,6 +741,54 @@
return false;
},
+ /**
+ * Returns true when an entity can attack a target.
+ */
+ "canAttackTarget": function(target)
+ {
+ let attackTypes = this.get("Attack");
+ if (!attackTypes)
+ return false;
+
+ let canCapture = this.canCapture(target);
+ let armourStrengths = target.get("Armour");
+ if (!armourStrengths)
+ return canCapture;
+
+ for (let type in attackTypes)
+ {
+ if (type != "Capture" && target.isInvulnerable())
+ continue;
+
+ if (type == "Capture" && !canCapture)
+ continue;
+
+ // Check if the target is immune to some damage types.
+ if (type != "Capture")
+ {
+ let attackStrengths = this.attackStrengths(type);
+ let canDamage = false;
+ for (let damageType in attackStrengths)
+ if (attackStrengths[damageType] != 0 && armourStrengths[damageType] != "Infinity")
+ {
+ canDamage = true;
+ break;
+ }
+
+ if (!canDamage)
+ continue;
+ }
+
+ let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
+ if (!restrictedClasses)
+ return true;
+ if (!MatchesClassList(target.classes(), restrictedClasses))
+ return true;
+ };
+
+ return false;
+ },
+
"move": function(x, z, queued = false) {
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued });
return this;
Index: binaries/data/mods/public/simulation/ai/petra/attackManager.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/attackManager.js
+++ binaries/data/mods/public/simulation/ai/petra/attackManager.js
@@ -182,6 +182,9 @@
let access = m.getLandAccess(gameState, ent);
for (let struct of gameState.getEnemyStructures().values())
{
+ if (!ent.canAttackTarget(struct.id()))
+ continue;
+
let structPos = struct.position();
let x;
let z;
Index: binaries/data/mods/public/simulation/ai/petra/attackPlan.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/attackPlan.js
+++ binaries/data/mods/public/simulation/ai/petra/attackPlan.js
@@ -1334,11 +1334,13 @@
{
if (m.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out
continue;
+ if (!ent.canAttackTarget(attacker.id()))
+ continue;
ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker));
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
// And if this attacker is a non-ranged siege unit and our unit also, attack it
- if (m.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee"))
+ if (m.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker.id()))
{
ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker));
ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
@@ -1357,6 +1359,8 @@
let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5);
for (let ent of collec.values())
{
+ if (!ent.canAttackTarget(attacker.id()))
+ continue;
ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker));
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
@@ -1367,7 +1371,7 @@
let collec = this.unitCollection.filterNearest(ourUnit.position(), 2);
for (let ent of collec.values())
{
- if (m.isSiegeUnit(ent))
+ if (m.isSiegeUnit(ent) || !ent.canAttackTarget(attacker.id()))
continue;
let orderData = ent.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
@@ -1395,8 +1399,11 @@
continue;
}
}
- ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker));
- ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
+ if (ourUnit.canAttackTarget(attacker.id()))
+ {
+ ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker));
+ ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
+ }
}
}
}
@@ -1547,7 +1554,7 @@
if (siegeUnit)
{
let mStruct = enemyStructures.filter(enemy => {
- if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall"))
+ if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall") || !ent.canAttackTarget(enemy.id()))
return false;
if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
@@ -1599,7 +1606,7 @@
{
let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged");
let mUnit = enemyUnits.filter(enemy => {
- if (!enemy.position())
+ if (!enemy.position() || !ent.canAttackTarget(enemy.id()))
return false;
if (enemy.hasClass("Animal"))
return false;
@@ -1641,7 +1648,7 @@
let rand = randIntExclusive(0, mUnit.length * 0.1);
ent.attack(mUnit[rand].id(), m.allowCapture(gameState, ent, mUnit[rand]));
}
- else if (this.isBlocked)
+ else if (this.isBlocked && ent.canAttackTarget(this.target.id()))
ent.attack(this.target.id(), false);
else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500)
{
@@ -1662,7 +1669,7 @@
let mStruct = enemyStructures.filter(enemy => {
if (this.isBlocked && enemy.id() != this.target.id())
return false;
- if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall"))
+ if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall") || !ent.canAttackTarget(enemy.id()))
return false;
if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
@@ -1710,6 +1717,8 @@
return;
distmin = dist;
attacker = gameState.getEntityById(unit.unitAIOrderData()[0].target);
+ if (!ent.canAttackTarget(attacker.id()))
+ return;
});
if (attacker)
ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker));
@@ -1763,7 +1772,7 @@
{
if (ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
- if (!ent.isIdle())
+ if (!ent.isIdle() || !ent.canAttackTarget(attacker.id()))
continue;
ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker));
}
Index: binaries/data/mods/public/simulation/ai/petra/defenseArmy.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/defenseArmy.js
+++ binaries/data/mods/public/simulation/ai/petra/defenseArmy.js
@@ -314,6 +314,9 @@
if (!eEnt || !eEnt.position()) // probably can't happen.
continue;
+ if (!ent.canAttackTarget(eEnt))
+ continue;
+
if (eEnt.hasClass("Unit") && eEnt.unitAIOrderData() && eEnt.unitAIOrderData().length &&
eEnt.unitAIOrderData()[0].target && eEnt.unitAIOrderData()[0].target == entID)
{ // being attacked >>> target the unit
Index: binaries/data/mods/public/simulation/ai/petra/defenseManager.js
===================================================================
--- binaries/data/mods/public/simulation/ai/petra/defenseManager.js
+++ binaries/data/mods/public/simulation/ai/petra/defenseManager.js
@@ -440,6 +440,18 @@
return;
if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()))
return;
+
+ // Do not assign defender if it cannot attack at least part of the attacking army.
+ for (let i = 0; i < armiesNeeding.length; ++i)
+ {
+ let canAttack = armiesNeeding[i].army.foeEntities.some(eEnt => {
+ let eEntID = gameState.getEntityById(eEnt);
+ return ent.canAttackTarget(eEntID);
+ });
+ if (!canAttack)
+ return;
+ };
+
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1)
{
let subrole = ent.getMetadata(PlayerID, "subrole");
@@ -705,7 +717,7 @@
if (allAttacked[entId])
continue;
let ent = gameState.getEntityById(entId);
- if (!ent || !ent.position())
+ if (!ent || !ent.position() || !ent.canAttackTarget(attacker.id()))
continue;
// Check that the unit is still attacking the structure (since the last played turn)
let state = ent.unitAIState();
@@ -728,7 +740,8 @@
}
}
}
- target.attack(attacker.id(), m.allowCapture(gameState, target, attacker));
+ if (target.canAttackTarget(attacker))
+ target.attack(attacker.id(), m.allowCapture(gameState, target, attacker));
}
}
};
Index: binaries/data/mods/public/simulation/components/Armour.js
===================================================================
--- binaries/data/mods/public/simulation/components/Armour.js
+++ binaries/data/mods/public/simulation/components/Armour.js
@@ -34,46 +34,52 @@
/**
* Take damage according to the entity's armor.
- * @param {Object} strengths - { "hack": number, "pierce": number, "crush": number } or something like that.
+ * @param {Object} attackStrengths - { "hack": number, "pierce": number, "crush": number } or something like that.
* @param {number} multiplier - the damage multiplier.
* Returns object of the form { "killed": false, "change": -12 }.
*/
-Armour.prototype.TakeDamage = function(strengths, multiplier = 1)
+Armour.prototype.TakeDamage = function(attackStrengths, multiplier = 1)
{
if (this.invulnerable)
return { "killed": false, "change": 0 };
// Adjust damage values based on armour; exponential armour: damage = attack * 0.9^armour
- var armourStrengths = this.GetArmourStrengths();
+ let armourStrengths = this.GetArmourStrengths();
// Total is sum of individual damages
// Don't bother rounding, since HP is no longer integral.
- var total = 0;
- for (let type in strengths)
- total += strengths[type] * multiplier * Math.pow(0.9, armourStrengths[type] || 0);
+ let total = 0;
+ for (let type in attackStrengths)
+ {
+ if (armourStrengths[type] == "Infinity")
+ continue;
+
+ total += attackStrengths[type] * multiplier * Math.pow(0.9, armourStrengths[type] || 0);
+ }
// Reduce health
- var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
+ let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
return cmpHealth.Reduce(total);
};
Armour.prototype.GetArmourStrengths = function()
{
// Work out the armour values with technology effects
- var applyMods = (type, foundation) => {
- var strength;
+ let applyMods = (type, foundation) => {
+ let strength;
if (foundation)
{
- strength = +this.template.Foundation[type];
+ strength = this.template.Foundation[type];
type = "Foundation/" + type;
}
else
- strength = +this.template[type];
+ strength = this.template[type];
- return ApplyValueModificationsToEntity("Armour/" + type, strength, this.entity);
+ return strength == "Infinity" ? "Infinity" :
+ ApplyValueModificationsToEntity("Armour/" + type, +strength, this.entity);
};
- var foundation = Engine.QueryInterface(this.entity, IID_Foundation) && this.template.Foundation;
+ let foundation = Engine.QueryInterface(this.entity, IID_Foundation) && this.template.Foundation;
let ret = {};
for (let damageType of DamageTypes.GetTypes())
Index: binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- binaries/data/mods/public/simulation/components/Attack.js
+++ binaries/data/mods/public/simulation/components/Attack.js
@@ -323,6 +323,11 @@
// reach each other, no matter how close they come.
let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
+ let armourStrengths;
+ let cmpTargetArmour = QueryMiragedInterface(target, IID_DamageReceiver);
+ if (cmpTargetArmour)
+ armourStrengths = cmpTargetArmour.GetArmourStrengths();
+
for (let type of types)
{
if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints()))
@@ -334,6 +339,22 @@
if (heightDiff > this.GetRange(type).max)
continue;
+ // Check if the target is immune to some damage types.
+ if (type != "Capture")
+ {
+ let attackStrengths = this.GetAttackStrengths(type);
+ let canDamage = false;
+ for (let damageType in attackStrengths)
+ if (attackStrengths[damageType] != 0 && armourStrengths[damageType] != "Infinity")
+ {
+ canDamage = true;
+ break;
+ }
+
+ if (!canDamage)
+ continue;
+ }
+
let restrictedClasses = this.GetRestrictedClasses(type);
if (!restrictedClasses.length)
return true;
@@ -408,20 +429,12 @@
if (isTargetClass("Domestic") && this.template.Slaughter)
return "Slaughter";
- let types = this.GetAttackTypes().filter(type => !this.GetRestrictedClasses(type).some(isTargetClass));
+ let types = this.GetAttackTypes().filter(type => this.CanAttack(target, [type]));
// check if the target is capturable
let captureIndex = types.indexOf("Capture");
- if (captureIndex != -1)
- {
- let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
-
- let cmpPlayer = QueryOwnerInterface(this.entity);
- if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
- return "Capture";
- // not capturable, so remove this attack
- types.splice(captureIndex, 1);
- }
+ if (captureIndex != -1 && allowCapture)
+ return "Capture";
let isPreferred = className => this.GetPreferredClasses(className).some(isTargetClass);
Index: binaries/data/mods/public/simulation/components/Fogging.js
===================================================================
--- binaries/data/mods/public/simulation/components/Fogging.js
+++ binaries/data/mods/public/simulation/components/Fogging.js
@@ -140,6 +140,10 @@
if (cmpMarket)
cmpMirage.CopyMarket(cmpMarket);
+ let cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
+ if (cmpDamageReceiver)
+ cmpMirage.CopyDamageReceiver(cmpDamageReceiver);
+
// Notify the GUI the entity has been replaced by a mirage, in case it is selected at this moment
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.AddMiragedEntity(player, this.entity, this.mirages[player]);
Index: binaries/data/mods/public/simulation/components/Mirage.js
===================================================================
--- binaries/data/mods/public/simulation/components/Mirage.js
+++ binaries/data/mods/public/simulation/components/Mirage.js
@@ -40,6 +40,8 @@
this.traders = null;
this.marketType = null;
this.internationalBonus = null;
+
+ this.armourStrengths = null;
};
Mirage.prototype.SetParent = function(ent)
@@ -207,6 +209,16 @@
}
};
+// Armour data
+
+Mirage.prototype.CopyDamageReceiver = function(cmpDamageReceiver)
+{
+ this.miragedIids.add(IID_DamageReceiver);
+ this.armourStrengths = clone(cmpDamageReceiver.GetArmourStrengths());
+};
+
+Mirage.prototype.GetArmourStrengths = function() { return this.armourStrengths; };
+
// ============================
Mirage.prototype.OnVisibilityChanged = function(msg)
Index: binaries/data/mods/public/simulation/components/tests/test_Attack.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_Attack.js
+++ binaries/data/mods/public/simulation/components/tests/test_Attack.js
@@ -3,6 +3,7 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Attack.js");
+Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/AuraManager.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
@@ -130,6 +131,14 @@
"GetHitpoints": () => 100
});
+ AddMock(defender, IID_DamageReceiver, {
+ "GetArmourStrengths": () => ({
+ "Hack": 0,
+ "Pierce": 0,
+ "Crush": 0
+ })
+ });
+
test_function(attacker, cmpAttack, defender);
}
@@ -205,7 +214,7 @@
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false);
});
-function testGetBestAttackAgainst(defenderClass, bestAttack, isBuilding = false)
+function testGetBestAttackAgainst(defenderClass, bestAttack, isBuilding = false, hasSomeImmunity = false)
{
attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => {
@@ -235,6 +244,35 @@
TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack);
});
+ // CanAttack rejects "Ranged" due to the fact that "Ranged" only has "Pierce"
+ // damage, to which the target is immune.
+ attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => {
+
+ if (hasSomeImmunity)
+ AddMock(defender, IID_DamageReceiver, {
+ "GetArmourStrengths": () => ({
+ "Hack": 0,
+ "Pierce": "Infinity",
+ "Crush": 0
+ })
+ });
+
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), !hasSomeImmunity);
+
+ if (hasSomeImmunity)
+ AddMock(defender, IID_DamageReceiver, {
+ "GetArmourStrengths": () => ({
+ "Hack": "Infinity",
+ "Pierce": 0,
+ "Crush": "Infinity"
+ })
+ });
+
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true);
+ TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), true);
+ });
+
attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => {
if (isBuilding)
@@ -266,7 +304,7 @@
attack = "Capture";
for (let ac of allowCapturing)
- TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack);
+ TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), attack);
});
}
@@ -274,6 +312,7 @@
testGetBestAttackAgainst("Archer", "Ranged");
testGetBestAttackAgainst("Domestic", "Slaughter");
testGetBestAttackAgainst("Structure", "Capture", true);
+testGetBestAttackAgainst("Hoplite", "Ranged", false, true);
function testPredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity)
{
Index: binaries/data/mods/public/simulation/helpers/DamageTypes.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/DamageTypes.js
+++ binaries/data/mods/public/simulation/helpers/DamageTypes.js
@@ -1,7 +1,7 @@
DamageTypes.prototype.BuildSchema = function(helptext = "")
{
return "" + this.GetTypes().reduce((schema, type) =>
- schema + "",
+ schema + "Infinity",
"") + "";
};
Index: binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
@@ -19,6 +19,7 @@
0.0
2
+ !Domestic
Index: binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
@@ -18,6 +18,7 @@
0.0
2
+ !Domestic