From ff90bb84901f15cfc302235bba0977a745ebc22f Mon Sep 17 00:00:00 2001 From: fatherbushido Date: Mon, 21 Aug 2017 06:52:37 +0000 Subject: [PATCH] Fix issues with civ and classes bonuses for splash damage. Even if those bonuses were in splash schema they was not taken into account. Update, fix, clean and complete unit tests for those logic. Reviewed by Mate-86. Differential Revision: https://code.wildfiregames.com/D792 This was SVN commit r20015. --- .../simulation/ai/petra/entityExtend.js | 1 - .../public/simulation/components/Attack.js | 36 +-- .../public/simulation/components/Damage.js | 17 +- .../simulation/components/DeathDamage.js | 8 + .../public/simulation/components/UnitAI.js | 8 - .../components/tests/test_Attack.js | 23 +- .../components/tests/test_Damage.js | 281 ++++++++++++++++-- .../components/tests/test_DeathDamage.js | 8 + .../public/simulation/helpers/DamageBonus.js | 26 ++ 9 files changed, 336 insertions(+), 72 deletions(-) create mode 100644 binaries/data/mods/public/simulation/helpers/DamageBonus.js diff --git a/binaries/data/mods/public/simulation/ai/petra/entityExtend.js b/binaries/data/mods/public/simulation/ai/petra/entityExtend.js index 304730b885..455b21648c 100644 --- a/binaries/data/mods/public/simulation/ai/petra/entityExtend.js +++ b/binaries/data/mods/public/simulation/ai/petra/entityExtend.js @@ -164,7 +164,6 @@ m.allowCapture = function(gameState, ent, target) return capture > antiCapture + sumCapturePoints/80; }; -/** copy of GetAttackBonus from Attack.js */ m.getAttackBonus = function(ent, target, type) { let attackBonus = 1; diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index 38029eec80..c4f465a1da 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -432,33 +432,14 @@ Attack.prototype.GetRange = function(type) return { "max": max, "min": min, "elevationBonus": elevationBonus }; }; -// Calculate the attack damage multiplier against a target -Attack.prototype.GetAttackBonus = function(type, target) +Attack.prototype.GetBonusTemplate = function(type) { - let attackBonus = 1; let template = this.template[type]; if (!template) template = this.template[type.split(".")[0]].Splash; - if (template.Bonuses) - { - let cmpIdentity = Engine.QueryInterface(target, IID_Identity); - if (!cmpIdentity) - return 1; - - // Multiply the bonuses for all matching classes - for (let key in template.Bonuses) - { - let bonus = template.Bonuses[key]; - if (bonus.Civ && bonus.Civ !== cmpIdentity.GetCiv()) - continue; - if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !cmpIdentity.HasClass(cls))) - continue; - attackBonus *= bonus.Multiplier; - } - } - - return attackBonus; + return clone(template.Bonuses); + return null; }; /** @@ -528,7 +509,7 @@ Attack.prototype.PerformAttack = function(type, target) "position": realTargetPosition, "direction": missileDirection, "projectileId": id, - "multiplier": this.GetAttackBonus(type, target), + "bonus": this.GetBonusTemplate(type), "isSplash": false, "attackerOwner": attackerOwner }; @@ -538,7 +519,8 @@ Attack.prototype.PerformAttack = function(type, target) data.radius = +this.template.Ranged.Splash.Range; data.shape = this.template.Ranged.Splash.Shape; data.isSplash = true; - data.splashStrengths = this.GetAttackStrengths(type+".Splash"); + data.splashStrengths = this.GetAttackStrengths(type + ".Splash"); + data.splashBonus = this.GetBonusTemplate(type + ".Splash"); } cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000, data); } @@ -547,7 +529,7 @@ Attack.prototype.PerformAttack = function(type, target) if (attackerOwner == -1) return; - let multiplier = this.GetAttackBonus(type, target); + let multiplier = GetDamageBonus(target, this.GetBonusTemplate(type)); let cmpHealth = Engine.QueryInterface(target, IID_Health); if (!cmpHealth || cmpHealth.GetHitpoints() == 0) return; @@ -574,7 +556,7 @@ Attack.prototype.PerformAttack = function(type, target) "strengths": this.GetAttackStrengths(type), "target": target, "attacker": this.entity, - "multiplier": this.GetAttackBonus(type, target), + "multiplier": GetDamageBonus(target, this.GetBonusTemplate(type)), "type": type, "attackerOwner": attackerOwner }); @@ -609,7 +591,7 @@ Attack.prototype.PredictTimeToTarget = function(selfPosition, horizSpeed, target return c / (Math.sqrt(disc) - b); return false; -} +}; Attack.prototype.OnValueModification = function(msg) { diff --git a/binaries/data/mods/public/simulation/components/Damage.js b/binaries/data/mods/public/simulation/components/Damage.js index cc386de9d5..ff8e954cd3 100644 --- a/binaries/data/mods/public/simulation/components/Damage.js +++ b/binaries/data/mods/public/simulation/components/Damage.js @@ -97,11 +97,14 @@ Damage.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire) * @param {boolean} data.isSplash - a flag indicating if it's splash damage. * @param {Vector3D} data.position - the expected position of the target. * @param {number} data.projectileId - the id of the projectile. - * @param {Vector3D} data.direction - The unit vector defining the direction + * @param {Vector3D} data.direction - the unit vector defining the direction. + * @param {Object} data.bonus - the attack bonus template from the attacker. * ***When splash damage*** * @param {boolean} data.friendlyFire - a flag indicating if allied entities are also damaged. * @param {number} data.radius - the radius of the splash damage. * @param {string} data.shape - the shape of the splash range. + * @param {Object} data.splashBonus - the attack bonus template from the attacker. + * @param {Object} data.splashStrengths - data of the form { 'hack': number, 'pierce': number, 'crush': number }. */ Damage.prototype.MissileHit = function(data, lateness) { @@ -117,6 +120,7 @@ Damage.prototype.MissileHit = function(data, lateness) "radius": data.radius, "shape": data.shape, "strengths": data.splashStrengths, + "splashBonus": data.splashBonus, "direction": data.direction, "playersToDamage": this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire), "type": data.type, @@ -131,6 +135,7 @@ Damage.prototype.MissileHit = function(data, lateness) let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); if (cmpDamageReceiver && this.TestCollision(data.target, data.position, lateness)) { + data.multiplier = GetDamageBonus(data.target, data.bonus); this.CauseDamage(data); cmpProjectileManager.RemoveProjectile(data.projectileId); return; @@ -147,11 +152,12 @@ Damage.prototype.MissileHit = function(data, lateness) { if (!this.TestCollision(ent, data.position, lateness)) continue; + this.CauseDamage({ "strengths": data.strengths, "target": ent, "attacker": data.attacker, - "multiplier": data.multiplier, + "multiplier": GetDamageBonus(ent, data.bonus), "type": data.type, "attackerOwner": data.attackerOwner }); @@ -170,7 +176,8 @@ Damage.prototype.MissileHit = function(data, lateness) * @param {Object} data.strengths - data of the form { 'hack': number, 'pierce': number, 'crush': number }. * @param {string} data.type - the type of damage. * @param {number} data.attackerOwner - the player id of the attacker. - * @param {Vector3D} [data.direction] - the unit vector defining the direction. + * @param {Vector3D} [data.direction] - the unit vector defining the direction. Needed for linear splash damage. + * @param {Object} data.splashBonus - the attack bonus template from the attacker. * @param {number[]} data.playersToDamage - the array of player id's to damage. */ Damage.prototype.CauseSplashDamage = function(data) @@ -210,6 +217,10 @@ Damage.prototype.CauseSplashDamage = function(data) { warn("The " + data.shape + " splash damage shape is not implemented!"); } + + if (data.splashBonus) + damageMultiplier *= GetDamageBonus(ent, data.splashBonus); + // Call CauseDamage which reduces the hitpoints, posts network command, plays sounds.... this.CauseDamage({ "strengths": data.strengths, diff --git a/binaries/data/mods/public/simulation/components/DeathDamage.js b/binaries/data/mods/public/simulation/components/DeathDamage.js index 37da68cd47..b0bb1d33a0 100644 --- a/binaries/data/mods/public/simulation/components/DeathDamage.js +++ b/binaries/data/mods/public/simulation/components/DeathDamage.js @@ -55,6 +55,13 @@ DeathDamage.prototype.GetDeathDamageStrengths = function(type) }; }; +DeathDamage.prototype.GetBonusTemplate = function() +{ + if (this.template.Bonuses) + return clone(this.template.Bonuses); + return null; +}; + DeathDamage.prototype.CauseDeathDamage = function() { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); @@ -78,6 +85,7 @@ DeathDamage.prototype.CauseDeathDamage = function() "radius": radius, "shape": this.template.Shape, "strengths": this.GetDeathDamageStrengths("Death"), + "splashBonus": this.GetBonusTemplate(), "playersToDamage": playersToDamage, "type": "Death", "attackerOwner": owner diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 2cd5a0226a..8b519ac3ce 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -4684,14 +4684,6 @@ UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture) return cmpAttack.GetBestAttackAgainst(target, allowCapture); }; -UnitAI.prototype.GetAttackBonus = function(type, target) -{ - var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - if (!cmpAttack) - return 1; - return cmpAttack.GetAttackBonus(type, target); -}; - /** * Try to find one of the given entities which can be attacked, * and start attacking it. diff --git a/binaries/data/mods/public/simulation/components/tests/test_Attack.js b/binaries/data/mods/public/simulation/components/tests/test_Attack.js index 1a130514e2..d8c34fff36 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -1,3 +1,4 @@ +Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Auras.js"); @@ -86,7 +87,7 @@ function attackComponentTest(defenderClass, isEnemy, test_function) "Bonuses": { "BonusCav": { "Classes": "Cavalry", - "Multiplier": 2 + "Multiplier": 3 } } } @@ -163,11 +164,21 @@ attackComponentTest(undefined, true ,(attacker, cmpAttack, defender) => { for (let className of ["Infantry", "Cavalry"]) attackComponentTest(className, true, (attacker, cmpAttack, defender) => { - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Melee", defender), className == "Cavalry" ? 2 : 1); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Ranged", defender), 1); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Ranged.Splash", defender), className == "Cavalry" ? 2 : 1); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Capture", defender), 1); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Slaughter", defender), 1); + + TS_ASSERT_EQUALS(cmpAttack.GetBonusTemplate("Melee").BonusCav.Multiplier, 2); + // Check that we don't leak data + let bonus = cmpAttack.GetBonusTemplate("Melee"); + bonus.BonusCav.Multiplier = 2.7; + TS_ASSERT_EQUALS(cmpAttack.GetBonusTemplate("Melee").BonusCav.Multiplier, 2); + + TS_ASSERT(cmpAttack.GetBonusTemplate("Capture") === null); + + let getAttackBonus = (t, e) => GetDamageBonus(e, cmpAttack.GetBonusTemplate(t)); + TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Melee", defender), className == "Cavalry" ? 2 : 1); + TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Ranged", defender), 1); + TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Ranged.Splash", defender), className == "Cavalry" ? 3 : 1); + TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Capture", defender), 1); + TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Slaughter", defender), 1); }); // CanAttack rejects elephant attack due to RestrictedClasses diff --git a/binaries/data/mods/public/simulation/components/tests/test_Damage.js b/binaries/data/mods/public/simulation/components/tests/test_Damage.js index 74cd11f22e..a60db79982 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -1,3 +1,4 @@ +Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadHelperScript("ValueModification.js"); @@ -36,8 +37,7 @@ function Test_Generic() let type = "Melee"; let damageTaken = false; - cmpAttack.GetAttackStrengths = (type) => ({ "hack": 0, "pierce": 0, "crush": damage }); - cmpAttack.GetAttackBonus = (type, target) => 1.0; + cmpAttack.GetAttackStrengths = attackType => ({ "hack": 0, "pierce": 0, "crush": damage }); let data = { "attacker": attacker, @@ -57,24 +57,19 @@ function Test_Generic() }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - "GetPlayerByID": (id) => atkPlayerEntity, + "GetPlayerByID": id => atkPlayerEntity, "GetNumPlayers": () => 5 }); - AddMock(SYSTEM_ENTITY, IID_RangeManager, { - "ExecuteQueryAroundPos": () => [target], - "GetElevationAdaptedRange": (pos, rot, max, bonus, a) => max, - }); - AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { - RemoveProjectile: () => {}, - LaunchProjectileAtPoint: (ent, pos, speed, gravity) => {}, + "RemoveProjectile": () => {}, + "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(target, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, - "GetPosition2D": () => new Vector2D(3, 3), + "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, }); @@ -117,13 +112,6 @@ function Test_Generic() cmpDamage.CauseDamage(data); TestDamage(); - data.friendlyFire = false; - data.range = 10; - data.shape = "Circular"; - data.isSplash = true; - cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", 1000, data); - TestDamage(); - // Check for damage still being dealt if the attacker dies cmpAttack.PerformAttack("Ranged", target); Engine.DestroyEntity(attacker); @@ -164,7 +152,9 @@ function TestLinearSplashDamage() let fallOff = function(x,y) { return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius)); - } + }; + + let hitEnts = new Set(); let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); @@ -186,6 +176,7 @@ function TestLinearSplashDamage() AddMock(60, IID_DamageReceiver, { "TakeDamage": (hack, pierce, crush) => { + hitEnts.add(60); TS_ASSERT_EQUALS(hack + pierce + crush, 100 * fallOff(2.2, -0.4)); return { "killed": false, "change": -(hack + pierce + crush) }; } @@ -193,6 +184,7 @@ function TestLinearSplashDamage() AddMock(61, IID_DamageReceiver, { TakeDamage: (hack, pierce, crush) => { + hitEnts.add(61); TS_ASSERT_EQUALS(hack + pierce + crush, 100 * fallOff(0, 0)); return { "killed": false, "change": -(hack + pierce + crush) }; } @@ -200,24 +192,34 @@ function TestLinearSplashDamage() AddMock(62, IID_DamageReceiver, { "TakeDamage": (hack, pierce, crush) => { + hitEnts.add(62); TS_ASSERT_EQUALS(hack + pierce + crush, 0); return { "killed": false, "change": -(hack + pierce + crush) }; } }); cmpDamage.CauseSplashDamage(data); + TS_ASSERT(hitEnts.has(60)); + TS_ASSERT(hitEnts.has(61)); + TS_ASSERT(hitEnts.has(62)); + hitEnts.clear(); data.direction = new Vector3D(0.6, 747, 0.8); AddMock(60, IID_DamageReceiver, { "TakeDamage": (hack, pierce, crush) => { + hitEnts.add(60); TS_ASSERT_EQUALS(hack + pierce + crush, 100 * fallOff(1, 2)); return { "killed": false, "change": -(hack + pierce + crush) }; } }); cmpDamage.CauseSplashDamage(data); -}; + TS_ASSERT(hitEnts.has(60)); + TS_ASSERT(hitEnts.has(61)); + TS_ASSERT(hitEnts.has(62)); + hitEnts.clear(); +} TestLinearSplashDamage(); @@ -231,12 +233,12 @@ function TestCircularSplashDamage() let fallOff = function(r) { return 1 - r * r / (radius * radius); - } + }; let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); AddMock(SYSTEM_ENTITY, IID_RangeManager, { - "ExecuteQueryAroundPos": () => [60, 61, 62], + "ExecuteQueryAroundPos": () => [60, 61, 62, 64], }); AddMock(60, IID_Position, { @@ -252,12 +254,12 @@ function TestCircularSplashDamage() }); AddMock(63, IID_Position, { - "GetPosition2D": () => new Vector2D(5, -3), + "GetPosition2D": () => new Vector2D(10, -10), }); // Target on the frontier of the shape AddMock(64, IID_Position, { - "GetPosition2D": () => new Vector2D(4, -2), + "GetPosition2D": () => new Vector2D(9, -4), }); AddMock(60, IID_DamageReceiver, { @@ -283,8 +285,7 @@ function TestCircularSplashDamage() AddMock(63, IID_DamageReceiver, { "TakeDamage": (hack, pierce, crush) => { - TS_ASSERT_EQUALS(hack + pierce + crush, 0); - return { "killed": false, "change": -(hack + pierce + crush) }; + TS_ASSERT(false); } }); @@ -305,6 +306,232 @@ function TestCircularSplashDamage() "type": "Ranged", "attackerOwner": 1 }); -}; +} TestCircularSplashDamage(); + +function Test_MissileHit() +{ + ResetState(); + Engine.PostMessage = (ent, iid, message) => {}; + + let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); + + let target = 60; + let targetOwner = 1; + let targetPos = new Vector3D(3, 10, 0); + let hitEnts = new Set(); + + AddMock(SYSTEM_ENTITY, IID_Timer, { + "GetLatestTurnLength": () => 500 + }); + + const radius = 10; + + let data = { + "type": "Ranged", + "attacker": 70, + "target": 60, + "strengths": { "hack": 0, "pierce": 100, "crush": 0 }, + "position": targetPos, + "direction": new Vector3D(1, 0, 0), + "projectileId": 9, + "bonus": undefined, + "isSplash": false, + "attackerOwner": 1 + }; + + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": id => id == 1 ? 10 : 11, + "GetNumPlayers": () => 2 + }); + + AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { + "RemoveProjectile": () => {}, + "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, + }); + + AddMock(60, IID_Position, { + "GetPosition": () => targetPos, + "GetPreviousPosition": () => targetPos, + "GetPosition2D": () => Vector2D.From(targetPos), + "IsInWorld": () => true, + }); + + AddMock(60, IID_Health, {}); + + AddMock(60, IID_DamageReceiver, { + "TakeDamage": (hack, pierce, crush) => { + TS_ASSERT_EQUALS(hack + pierce + crush, 100); + hitEnts.add(60); + return { "killed": false, "change": -(hack + pierce + crush) }; + } + }); + + AddMock(60, IID_Footprint, { + "GetShape": () => ({ "type": "circle", "radius": 20 }), + }); + + AddMock(70, IID_Ownership, { + "GetOwner": () => 1, + }); + + AddMock(70, IID_Position, { + "GetPosition": () => new Vector3D(0, 0, 0), + "GetRotation": () => new Vector3D(0, 0, 0), + "IsInWorld": () => true, + }); + + AddMock(10, IID_Player, { + "GetEnemies": () => [2] + }); + + cmpDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(60)); + hitEnts.clear(); + + // The main target is not hit but another one is hit. + + AddMock(60, IID_Position, { + "GetPosition": () => new Vector3D(900, 10, 0), + "GetPreviousPosition": () => new Vector3D(900, 10, 0), + "GetPosition2D": () => new Vector2D(900, 0), + "IsInWorld": () => true, + }); + + AddMock(60, IID_DamageReceiver, { + "TakeDamage": (hack, pierce, crush) => { + TS_ASSERT_EQUALS(false); + return { "killed": false, "change": -(hack + pierce + crush) }; + } + }); + + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "ExecuteQueryAroundPos": () => [61] + }); + + AddMock(61, IID_Position, { + "GetPosition": () => targetPos, + "GetPreviousPosition": () => targetPos, + "GetPosition2D": () => Vector2D.from3D(targetPos), + "IsInWorld": () => true, + }); + + AddMock(61, IID_Health, {}); + + AddMock(61, IID_DamageReceiver, { + "TakeDamage": (hack, pierce, crush) => { + TS_ASSERT_EQUALS(hack + pierce + crush, 100); + hitEnts.add(61); + return { "killed": false, "change": -(hack + pierce + crush) }; + } + }); + + AddMock(61, IID_Footprint, { + "GetShape": () => ({ "type": "circle", "radius": 20 }), + }); + + cmpDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(61)); + hitEnts.clear(); + + // Add a splash damage. + + data.friendlyFire = false; + data.radius = 10; + data.shape = "Circular"; + data.isSplash = true; + data.splashStrengths = { "hack": 0, "pierce": 0, "crush": 200 }; + + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "ExecuteQueryAroundPos": () => [61, 62] + }); + + let dealtDamage = 0; + AddMock(61, IID_DamageReceiver, { + "TakeDamage": (hack, pierce, crush) => { + dealtDamage += hack + pierce + crush; + hitEnts.add(61); + return { "killed": false, "change": -(hack + pierce + crush) }; + } + }); + + AddMock(62, IID_Position, { + "GetPosition": () => new Vector3D(8, 10, 0), + "GetPreviousPosition": () => new Vector3D(8, 10, 0), + "GetPosition2D": () => new Vector2D(8, 0), + "IsInWorld": () => true, + }); + + AddMock(62, IID_Health, {}); + + AddMock(62, IID_DamageReceiver, { + "TakeDamage": (hack, pierce, crush) => { + TS_ASSERT_EQUALS(hack + pierce + crush, 200 * 0.75); + hitEnts.add(62); + return { "killed": false, "change": -(hack + pierce + crush) }; + } + }); + + AddMock(62, IID_Footprint, { + "GetShape": () => ({ "type": "circle", "radius": 20 }), + }); + + cmpDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(61)); + TS_ASSERT_EQUALS(dealtDamage, 100 + 200); + dealtDamage = 0; + TS_ASSERT(hitEnts.has(62)); + hitEnts.clear(); + + // Add some hard counters bonus. + + Engine.DestroyEntity(62); + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "ExecuteQueryAroundPos": () => [61] + }); + + let bonus= { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } }; + let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 10000 } }; + + AddMock(61, IID_Identity, { + "HasClass": cl => cl == "Cavalry" + }); + + data.bonus = bonus; + cmpDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(61)); + TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 200); + dealtDamage = 0; + hitEnts.clear(); + + data.splashBonus = splashBonus; + cmpDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(61)); + TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 10000 * 200); + dealtDamage = 0; + hitEnts.clear(); + + data.bonus = undefined; + cmpDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(61)); + TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); + dealtDamage = 0; + hitEnts.clear(); + + data.bonus = null; + cmpDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(61)); + TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); + dealtDamage = 0; + hitEnts.clear(); + + data.bonus = {}; + cmpDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(61)); + TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); + dealtDamage = 0; + hitEnts.clear(); +} + +Test_MissileHit(); diff --git a/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js b/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js index 70efc27fa1..a0edfeff15 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js +++ b/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js @@ -1,3 +1,4 @@ +Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Damage.js"); @@ -41,6 +42,7 @@ let result = { "radius": template.Range, "shape": template.Shape, "strengths": modifiedDamage, + "splashBonus": null, "playersToDamage": playersToDamage, "type": "Death", "attackerOwner": player @@ -62,3 +64,9 @@ AddMock(deadEnt, IID_Ownership, { TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage); cmpDeathDamage.CauseDeathDamage(); + +let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } }; +cmpDeathDamage.template.Bonuses = splashBonus; +result.splashBonus = splashBonus; +TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage); +cmpDeathDamage.CauseDeathDamage(); diff --git a/binaries/data/mods/public/simulation/helpers/DamageBonus.js b/binaries/data/mods/public/simulation/helpers/DamageBonus.js new file mode 100644 index 0000000000..c6bb702772 --- /dev/null +++ b/binaries/data/mods/public/simulation/helpers/DamageBonus.js @@ -0,0 +1,26 @@ +/** + * Calculate the attack damage multiplier against a target. + */ +function GetDamageBonus(target, template) +{ + let attackBonus = 1; + + let cmpIdentity = Engine.QueryInterface(target, IID_Identity); + if (!cmpIdentity) + return 1; + + // Multiply the bonuses for all matching classes + for (let key in template) + { + let bonus = template[key]; + if (bonus.Civ && bonus.Civ !== cmpIdentity.GetCiv()) + continue; + if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !cmpIdentity.HasClass(cls))) + continue; + attackBonus *= bonus.Multiplier; + } + + return attackBonus; +} + +Engine.RegisterGlobal("GetDamageBonus", GetDamageBonus);