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/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,13 @@ 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); - } + let cmpPlayer = QueryOwnerInterface(this.entity); + if (captureIndex != -1 && allowCapture && cmpPlayer) + 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", "") + ""; };