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