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 @@ -231,8 +231,8 @@ if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) return false; - const cmpResistance = QueryMiragedInterface(target, IID_Resistance); - if (!cmpResistance) + const cmpTargetResistance = QueryMiragedInterface(target, IID_Resistance); + if (!cmpTargetResistance) return false; const cmpIdentity = QueryMiragedInterface(target, IID_Identity); @@ -265,6 +265,9 @@ if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints())) continue; + if (!cmpTargetResistance.IsVulnerableTo(this.template[type])) + continue; + if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) continue; Index: binaries/data/mods/public/simulation/components/Resistance.js =================================================================== --- binaries/data/mods/public/simulation/components/Resistance.js +++ binaries/data/mods/public/simulation/components/Resistance.js @@ -117,9 +117,9 @@ */ Resistance.prototype.GetEffectiveResistanceAgainst = function(effectType) { - let ret = {}; + const ret = {}; - let template = this.GetResistanceOfForm(Engine.QueryInterface(this.entity, IID_Foundation) ? "Foundation" : "Entity"); + const template = this.GetResistanceOfForm(Engine.QueryInterface(this.entity, IID_Foundation) ? "Foundation" : "Entity"); if (template[effectType]) ret[effectType] = template[effectType]; @@ -174,6 +174,20 @@ return ret; }; +/** + * This is a fast resistance-check, validating that the attackData + * may potentially damage us, but not actually computing any numbers. + * @aparam AttackData - an attack schema object. + */ +Resistance.prototype.IsVulnerableTo = function(attackData) +{ + const form = Engine.QueryInterface(this.entity, IID_Foundation) ? "Foundation" : "Entity"; + for (const type in this.template[form]) + if (attackData[type] !== undefined) + return true; + return false; +}; + Resistance.prototype.OnOwnershipChanged = function(msg) { if (msg.to === INVALID_PLAYER) @@ -209,6 +223,15 @@ return this.resistanceOfForm; }; +ResistanceMirage.prototype.IsVulnerableTo = function(attackData) +{ + const form = this.isFoundation ? "Foundation" : "Entity"; + for (const type in this.resistanceOfForm[form]) + if (attackData[type] !== undefined) + return true; + return false; +}; + Engine.RegisterGlobal("ResistanceMirage", ResistanceMirage); Resistance.prototype.Mirage = function() 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 @@ -175,6 +175,8 @@ }); AddMock(defender, IID_Resistance, { + "GetEffectiveResistanceAgainst": (x) => ({ [x]: 0 }), + "IsVulnerableTo": () => true }); test_function(attacker, cmpAttack, defender); Index: binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -106,6 +106,11 @@ }, }); + AddMock(target, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Crush": 0 } }), + "IsInvulnerable": () => false, + }); + AddMock(SYSTEM_ENTITY, IID_DelayedDamage, { "Hit": () => { damageTaken = true; @@ -232,6 +237,21 @@ "GetPosition2D": () => new Vector2D(5, 2), }); + AddMock(60, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Hack": 0 } }), + "IsInvulnerable": () => false, + }); + + AddMock(61, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Hack": 0 } }), + "IsInvulnerable": () => false, + }); + + AddMock(62, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Hack": 0 } }), + "IsInvulnerable": () => false, + }); + AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(60); @@ -345,6 +365,13 @@ "GetPosition2D": () => new Vector2D(23, 4), }); + for (let i = 60; i <= 65; ++i) + AddMock(i, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Hack": 0 } }), + "IsInvulnerable": () => false, + }); + + AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 100 * fallOff(0)); @@ -464,6 +491,11 @@ "GetShape": () => ({ "type": "circle", "radius": 20 }), }); + AddMock(60, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0 } }), + "IsInvulnerable": () => false, + }); + AddMock(70, IID_Ownership, { "GetOwner": () => 1, }); @@ -510,6 +542,11 @@ "GetShape": () => ({ "type": "circle", "radius": 20 }) }); + AddMock(61, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0 } }), + "IsInvulnerable": () => false, + }); + cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); hitEnts.clear(); @@ -570,6 +607,11 @@ } }); + AddMock(61, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0, "Crush": 0 } }), + "IsInvulnerable": () => false, + }); + AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), @@ -589,6 +631,11 @@ "GetShape": () => ({ "type": "circle", "radius": 20 }), }); + AddMock(62, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0, "Crush": 0 } }), + "IsInvulnerable": () => false, + }); + cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); @@ -677,6 +724,11 @@ } }); + AddMock(61, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0, "Crush": 0 } }), + "IsInvulnerable": () => false, + }); + AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), @@ -696,6 +748,11 @@ "GetShape": () => ({ "type": "circle", "radius": 20 }), }); + AddMock(62, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0, "Crush": 0 } }), + "IsInvulnerable": () => false, + }); + cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); Index: binaries/data/mods/public/simulation/components/tests/test_Resistance.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Resistance.js +++ binaries/data/mods/public/simulation/components/tests/test_Resistance.js @@ -58,22 +58,22 @@ TestInvulnerability() { - this.Reset(); + this.Reset({ "Entity": { "Damage": { "Name": 0 } } }); - let damage = 5; - let attackData = { "Damage": { "Name": damage } }; - let attackType = "Test"; + const damage = 5; + const attackData = { "Damage": { "Name": damage } }; + const attackType = "Test"; TS_ASSERT(!this.cmpResistance.IsInvulnerable()); - let cmpHealth = AddMock(this.EntityID, IID_Health, { + const cmpHealth = AddMock(this.EntityID, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage); return { "healthChange": -amount }; } }); - let spy = new Spy(cmpHealth, "TakeDamage"); - let data = { + const spy = new Spy(cmpHealth, "TakeDamage"); + const data = { "type": attackType, "attackData": attackData, "attacker": this.AttackerID, @@ -90,14 +90,42 @@ TS_ASSERT_EQUALS(spy._called, 1); } + TestInvulnerabilityThroughUnspecifiedResistance() + { + this.Reset({ "Entity": { "Damage": {} } }); + + const damage = 5; + const attackData = { "Damage": { "Name": damage } }; + const attackType = "Test"; + + TS_ASSERT(!this.cmpResistance.IsInvulnerable()); + + const cmpHealth = AddMock(this.EntityID, IID_Health, { + "TakeDamage": (amount, __, ___) => { + TS_ASSERT_EQUALS(amount, 0); + return { "healthChange": -amount }; + } + }); + const spy = new Spy(cmpHealth, "TakeDamage"); + const data = { + "type": attackType, + "attackData": attackData, + "attacker": this.AttackerID, + "attackerOwner": this.EnemyID + }; + + AttackHelper.HandleAttackEffects(this.EntityID, data); + TS_ASSERT_EQUALS(spy._called, 1); + } + TestBonus() { - this.Reset(); + this.Reset({ "Entity": { "Damage": { "Name": 0 } } }); - let damage = 5; - let bonus = 2; - let classes = "Entity"; - let attackData = { + const damage = 5; + const bonus = 2; + const classes = "Entity"; + const attackData = { "Damage": { "Name": damage }, "Bonuses": { "bonus": { @@ -112,13 +140,13 @@ "GetCiv": () => "civ" }); - let cmpHealth = AddMock(this.EntityID, IID_Health, { + const cmpHealth = AddMock(this.EntityID, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * bonus); return { "healthChange": -amount }; } }); - let spy = new Spy(cmpHealth, "TakeDamage"); + const spy = new Spy(cmpHealth, "TakeDamage"); AttackHelper.HandleAttackEffects(this.EntityID, { "type": "Test", @@ -131,8 +159,8 @@ TestDamageResistanceApplies() { - let resistanceValue = 2; - let damageType = "Name"; + const resistanceValue = 2; + const damageType = "Name"; this.Reset({ "Entity": { "Damage": { @@ -141,18 +169,18 @@ } }); - let damage = 5; - let attackData = { + const damage = 5; + const attackData = { "Damage": { "Name": damage } }; - let cmpHealth = AddMock(this.EntityID, IID_Health, { + const cmpHealth = AddMock(this.EntityID, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * Math.pow(0.9, resistanceValue)); return { "healthChange": -amount }; } }); - let spy = new Spy(cmpHealth, "TakeDamage"); + const spy = new Spy(cmpHealth, "TakeDamage"); AttackHelper.HandleAttackEffects(this.EntityID, { "type": "Test", @@ -165,25 +193,25 @@ TestCaptureResistanceApplies() { - let resistanceValue = 2; + const resistanceValue = 2; this.Reset({ "Entity": { "Capture": resistanceValue } }); - let damage = 5; - let attackData = { + const damage = 5; + const attackData = { "Capture": damage }; - let cmpCapturable = AddMock(this.EntityID, IID_Capturable, { + const cmpCapturable = AddMock(this.EntityID, IID_Capturable, { "Capture": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * Math.pow(0.9, resistanceValue)); return { "captureChange": amount }; } }); - let spy = new Spy(cmpCapturable, "Capture"); + const spy = new Spy(cmpCapturable, "Capture"); AttackHelper.HandleAttackEffects(this.EntityID, { "type": "Test", @@ -197,8 +225,8 @@ TestStatusEffectsResistancesApplies() { // Test duration reduction. - let durationFactor = 0.5; - let statusName = "statusName"; + const durationFactor = 0.5; + const statusName = "statusName"; this.Reset({ "Entity": { "ApplyStatus": { @@ -209,7 +237,7 @@ } }); - let duration = 10; + const duration = 10; let attackData = { "ApplyStatus": { [statusName]: { @@ -262,8 +290,8 @@ TS_ASSERT_EQUALS(spy._called, 1); // Test multiple resistances. - let reducedStatusName = "reducedStatus"; - let blockedStatusName = "blockedStatus"; + const reducedStatusName = "reducedStatus"; + const blockedStatusName = "blockedStatus"; this.Reset({ "Entity": { "ApplyStatus": { @@ -308,8 +336,8 @@ TestResistanceAndBonus() { - let resistanceValue = 2; - let damageType = "Name"; + const resistanceValue = 2; + const damageType = "Name"; this.Reset({ "Entity": { "Damage": { @@ -318,10 +346,10 @@ } }); - let damage = 5; - let bonus = 2; - let classes = "Entity"; - let attackData = { + const damage = 5; + const bonus = 2; + const classes = "Entity"; + const attackData = { "Damage": { "Name": damage }, "Bonuses": { "bonus": { @@ -336,13 +364,13 @@ "GetCiv": () => "civ" }); - let cmpHealth = AddMock(this.EntityID, IID_Health, { + const cmpHealth = AddMock(this.EntityID, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * bonus * Math.pow(0.9, resistanceValue)); return { "healthChange": -amount }; } }); - let spy = new Spy(cmpHealth, "TakeDamage"); + const spy = new Spy(cmpHealth, "TakeDamage"); AttackHelper.HandleAttackEffects(this.EntityID, { "type": "Test", @@ -355,17 +383,18 @@ TestMultipleEffects() { - let captureResistanceValue = 2; + const captureResistanceValue = 2; this.Reset({ "Entity": { + "Damage": { "Name": 0 }, "Capture": captureResistanceValue } }); - let damage = 5; - let bonus = 2; - let classes = "Entity"; - let attackData = { + const damage = 5; + const bonus = 2; + const classes = "Entity"; + const attackData = { "Damage": { "Name": damage }, "Capture": damage, "Bonuses": { @@ -381,13 +410,13 @@ "GetCiv": () => "civ" }); - let cmpCapturable = AddMock(this.EntityID, IID_Capturable, { + const cmpCapturable = AddMock(this.EntityID, IID_Capturable, { "Capture": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * bonus * Math.pow(0.9, captureResistanceValue)); return { "captureChange": amount }; } }); - let cmpHealth = AddMock(this.EntityID, IID_Health, { + const cmpHealth = AddMock(this.EntityID, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * bonus); return { "healthChange": -amount }; @@ -395,8 +424,8 @@ "GetHitpoints": () => 1, "GetMaxHitpoints": () => 1 }); - let healthSpy = new Spy(cmpHealth, "TakeDamage"); - let captureSpy = new Spy(cmpCapturable, "Capture"); + const healthSpy = new Spy(cmpHealth, "TakeDamage"); + const captureSpy = new Spy(cmpCapturable, "Capture"); AttackHelper.HandleAttackEffects(this.EntityID, { "type": "Test", @@ -409,7 +438,7 @@ } } -let cmp = new testResistance(); +const cmp = new testResistance(); cmp.TestInvulnerability(); cmp.TestBonus(); cmp.TestDamageResistanceApplies(); Index: binaries/data/mods/public/simulation/helpers/Attack.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Attack.js +++ binaries/data/mods/public/simulation/helpers/Attack.js @@ -167,28 +167,34 @@ if (!cmpResistance) cmpResistance = Engine.QueryInterface(target, IID_Resistance); - let resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {}; + const resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {}; - if (effectType == "Damage") - for (let type in effectData.Damage) - total += effectData.Damage[type] * Math.pow(0.9, resistanceStrengths.Damage ? resistanceStrengths.Damage[type] || 0 : 0); - else if (effectType == "Capture") + // Having no damage/capture resistance to a given type specified is assumed to mean invulnerability. + if (effectType === "Damage") { - total = effectData.Capture * Math.pow(0.9, resistanceStrengths.Capture || 0); + for (const type in resistanceStrengths?.Damage) + if (effectData.Damage[type] !== undefined) + total += effectData.Damage[type] * Math.pow(0.9, resistanceStrengths.Damage[type]); + } + else if (effectType === "Capture" && resistanceStrengths.Capture !== undefined && + effectData.Capture !== undefined) + { + total = effectData.Capture * Math.pow(0.9, resistanceStrengths.Capture); // If Health is lower we are more susceptible to capture attacks. - let cmpHealth = Engine.QueryInterface(target, IID_Health); + const cmpHealth = Engine.QueryInterface(target, IID_Health); if (cmpHealth) total /= 0.1 + 0.9 * cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints(); } - if (effectType != "ApplyStatus") + if (effectType !== "ApplyStatus") return total * bonusMultiplier; + // TODO: treat undefined as invulnerable. if (!resistanceStrengths.ApplyStatus) return effectData[effectType]; - let result = {}; - for (let statusEffect in effectData[effectType]) + const result = {}; + for (const statusEffect in effectData[effectType]) { if (!resistanceStrengths.ApplyStatus[statusEffect]) { Index: binaries/data/mods/public/simulation/helpers/tests/test_Attack.js =================================================================== --- binaries/data/mods/public/simulation/helpers/tests/test_Attack.js +++ binaries/data/mods/public/simulation/helpers/tests/test_Attack.js @@ -36,7 +36,7 @@ this.TESTED_ENTITY_ID = 5; this.attackData = { - "Damage": "1", + "Damage": { "Name": "1" }, "Capture": "2", "ApplyStatus": { "statusName": {} @@ -44,20 +44,40 @@ }; } + setupTarget(id, hasHealth, hasCapture) + { + DeleteMock(id, IID_Health); + DeleteMock(id, IID_Capturable); + + // Resistance is always needed. + AddMock(id, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ + "Damage": { + "Name": 0, + }, + "Capture": 0 + }), + "IsInvulnerable": () => false, + }); + + if (hasHealth) + AddMock(id, IID_Health, { + "TakeDamage": x => { this.resultString += x; return { "Damage": x }; }, + "GetHitpoints": () => 1, + "GetMaxHitpoints": () => 1, + }); + + if (hasCapture) + AddMock(id, IID_Capturable, { + "Capture": x => { this.resultString += x; return { "Capture": x }; }, + }); + } + /** * This tests that we inflict multiple effect types. */ testMultipleEffects() { - AddMock(this.TESTED_ENTITY_ID, IID_Health, { - "TakeDamage": x => { this.resultString += x; }, - "GetHitpoints": () => 1, - "GetMaxHitpoints": () => 1, - }); - - AddMock(this.TESTED_ENTITY_ID, IID_Capturable, { - "Capture": x => { this.resultString += x; }, - }); - + this.setupTarget(this.TESTED_ENTITY_ID, true, true); AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { "type": "Test", "attackData": this.attackData, @@ -65,7 +85,7 @@ "attackerOwner": INVALID_PLAYER }); - TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) !== -1); + TS_ASSERT(this.resultString.indexOf(this.attackData.Damage.Name) !== -1); TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) !== -1); } @@ -73,9 +93,7 @@ * This tests that we correctly handle effect types if one is not received. */ testSkippedEffect() { - AddMock(this.TESTED_ENTITY_ID, IID_Capturable, { - "Capture": x => { this.resultString += x; }, - }); + this.setupTarget(this.TESTED_ENTITY_ID, false, true); AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { "type": "Test", @@ -83,16 +101,11 @@ "attacker": INVALID_ENTITY, "attackerOwner": INVALID_PLAYER }); - TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) === -1); + TS_ASSERT(this.resultString.indexOf(this.attackData.Damage.Name) === -1); TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) !== -1); this.resultString = ""; - DeleteMock(this.TESTED_ENTITY_ID, IID_Capturable); - AddMock(this.TESTED_ENTITY_ID, IID_Health, { - "TakeDamage": x => { this.resultString += x; }, - "GetHitpoints": () => 1, - "GetMaxHitpoints": () => 1, - }); + this.setupTarget(this.TESTED_ENTITY_ID, true, false); AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { "type": "Test", @@ -100,7 +113,7 @@ "attacker": INVALID_ENTITY, "attackerOwner": INVALID_PLAYER }); - TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) !== -1); + TS_ASSERT(this.resultString.indexOf(this.attackData.Damage.Name) !== -1); TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) === -1); } @@ -109,6 +122,9 @@ */ testAttackedMessage() { Engine.PostMessage = () => TS_ASSERT(false); + + this.setupTarget(this.TESTED_ENTITY_ID, false, false); + AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { "type": "Test", "attackData": this.attackData, @@ -116,9 +132,8 @@ "attackerOwner": INVALID_PLAYER }); - AddMock(this.TESTED_ENTITY_ID, IID_Capturable, { - "Capture": () => ({ "captureChange": 0 }), - }); + this.setupTarget(this.TESTED_ENTITY_ID, false, true); + let count = 0; Engine.PostMessage = () => count++; AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { @@ -129,11 +144,8 @@ }); TS_ASSERT_EQUALS(count, 1); - AddMock(this.TESTED_ENTITY_ID, IID_Health, { - "TakeDamage": () => ({ "healthChange": 0 }), - "GetHitpoints": () => 1, - "GetMaxHitpoints": () => 1, - }); + this.setupTarget(this.TESTED_ENTITY_ID, true, true); + count = 0; Engine.PostMessage = () => count++; AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { @@ -169,9 +181,10 @@ * Regression test that bonus multiplier is handled correctly. */ testBonusMultiplier() { + this.setupTarget(this.TESTED_ENTITY_ID, false, false); AddMock(this.TESTED_ENTITY_ID, IID_Health, { "TakeDamage": (amount, __, ___) => { - TS_ASSERT_EQUALS(amount, this.attackData.Damage * 2); + TS_ASSERT_EQUALS(amount, this.attackData.Damage.Name * 2); }, "GetHitpoints": () => 1, "GetMaxHitpoints": () => 1, Index: binaries/data/mods/public/simulation/templates/template_structure.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure.xml +++ binaries/data/mods/public/simulation/templates/template_structure.xml @@ -115,7 +115,9 @@ 1 10 1 + 0 + 0 @@ -128,6 +130,7 @@ 1 10 1 + 0 Index: binaries/data/mods/public/simulation/templates/template_unit.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit.xml +++ binaries/data/mods/public/simulation/templates/template_unit.xml @@ -82,6 +82,8 @@ 1 1 1 + 0 + 0