Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js @@ -164,7 +164,6 @@ return capture > antiCapture + sumCapturePoints/80; }; -/** copy of GetAttackBonus from Attack.js */ m.getAttackBonus = function(ent, target, type) { let attackBonus = 1; Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js @@ -432,33 +432,14 @@ 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 @@ "position": realTargetPosition, "direction": missileDirection, "projectileId": id, - "multiplier": this.GetAttackBonus(type, target), + "bonus": this.GetBonusTemplate(type), "isSplash": false, "attackerOwner": attackerOwner }; @@ -538,7 +519,8 @@ 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 @@ 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 @@ "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 @@ return c / (Math.sqrt(disc) - b); return false; -} +}; Attack.prototype.OnValueModification = function(msg) { Index: ps/trunk/binaries/data/mods/public/simulation/components/Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Damage.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Damage.js @@ -97,11 +97,14 @@ * @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 @@ "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 @@ 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 @@ { 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 @@ * @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 @@ { 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, Index: ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js +++ ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js @@ -55,6 +55,13 @@ }; }; +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 @@ "radius": radius, "shape": this.template.Shape, "strengths": this.GetDeathDamageStrengths("Death"), + "splashBonus": this.GetBonusTemplate(), "playersToDamage": playersToDamage, "type": "Death", "attackerOwner": owner Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js @@ -4684,14 +4684,6 @@ 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. Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ ps/trunk/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 @@ "Bonuses": { "BonusCav": { "Classes": "Cavalry", - "Multiplier": 2 + "Multiplier": 3 } } } @@ -163,11 +164,21 @@ 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 Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ ps/trunk/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 @@ 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 @@ }); 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ }); 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 @@ 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 @@ "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(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js +++ ps/trunk/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 @@ "radius": template.Range, "shape": template.Shape, "strengths": modifiedDamage, + "splashBonus": null, "playersToDamage": playersToDamage, "type": "Death", "attackerOwner": player @@ -62,3 +64,9 @@ 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(); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js +++ ps/trunk/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);