Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -175,6 +175,15 @@ "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape }; + + if (type == "Death") + ret.attack.Death = { + "hack": getAttackStat("Hack"), + "pierce": getAttackStat("Pierce"), + "crush": getAttackStat("Crush"), + // true if undefined + "friendlyFire": template.Attack.Death.FriendlyFire != "false" + }; } } 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 @@ -8,7 +8,8 @@ var g_AttackTypes = { "Melee": translate("Melee Attack:"), "Ranged": translate("Ranged Attack:"), - "Capture": translate("Capture Attack:") + "Capture": translate("Capture Attack:"), + "Death": translate("Damage on Destroy:") }; var g_DamageTypes = { @@ -176,6 +177,16 @@ let tooltips = []; for (let type in template.attack) { + if (type == "Death") + { + attacks.push(sprintf(translate("%(label)s %(details)s Friendly Fire: %(enabled)s"), { + "label": headerFont(g_AttackTypes[type]), + "details": damageTypesToText(template.attack.Death), + "enabled": template.attack.Death.friendlyFire ? translate("Yes") : translate("No") + })); + continue; + } + if (type == "Slaughter") continue; // Slaughter is used to kill animals, so do not show it. 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 @@ -39,6 +39,17 @@ "" + "" + ""; + +Attack.prototype.splashSchema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + Attack.prototype.bonusesSchema + + ""; Attack.prototype.Schema = "Controls the attack abilities and strengths of the unit." + @@ -96,6 +107,14 @@ "0.0" + "4.0" + "" + + "" + + "Circular" + + "20" + + "false" + + "0.0" + + "10.0" + + "50.0" + + "" + "" + "" + "" + @@ -143,13 +162,7 @@ "" + "" + "" + - "" + - "" + - "" + - "" + - "" + - "" + - Attack.prototype.bonusesSchema + + Attack.prototype.splashSchema + "" + "" + "" + @@ -182,6 +195,11 @@ Attack.prototype.restrictedClassesSchema + "" + "" + + "" + + "" + + "" + + Attack.prototype.splashSchema + + "" + ""; Attack.prototype.Init = function() @@ -625,4 +643,36 @@ cmpUnitAI.UpdateRangeQueries(); }; +Attack.prototype.CauseDeathDamage = function() +{ + if (!this.template.Death) + return; + + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return; + let pos = cmpPosition.GetPosition2D(); + + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership) + warn("Unit causing death damage does not have any owner.") + let owner = cmpOwnership.GetOwner(); + + let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); + let playersToDamage = cmpDamage.GetPlayersToDamage(owner, this.template.Death.FriendlyFire); + + let radius = ApplyValueModificationsToEntity("Attack/Death/Range", +this.template.Death.Range, this.entity); + + cmpDamage.CauseSplashDamage({ + "attacker": this.entity, + "origin": pos, + "radius": radius, + "shape": this.template.Death.Shape, + "strengths": this.GetAttackStrengths("Death"), + "playersToDamage": playersToDamage, + "type": "Death", + "attackerOwner": owner + }); +}; + Engine.RegisterComponentType(IID_Attack, "Attack", Attack); Index: binaries/data/mods/public/simulation/components/Damage.js =================================================================== --- binaries/data/mods/public/simulation/components/Damage.js +++ binaries/data/mods/public/simulation/components/Damage.js @@ -65,6 +65,22 @@ }; /** + * Get the list of players affected by the damage. + * @param {number} attackerOwner - the player id of the attacker. + * @param {boolean} friendlyFire - a flag indicating if allied entities are also damaged. + * @return {array.number} - the ids of players need to be damaged + */ +Damage.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire) +{ + let player = QueryPlayerIDInterface(attackerOwner); + + if (!friendlyFire) + return player.GetEnemies(); + + return player.GetAllPlayers(); +} + +/** * Handles hit logic after the projectile travel time has passed. * @param {Object} data - the data sent by the caller. * @param {number} data.attacker - the entity id of the attacker. @@ -90,16 +106,6 @@ // Do this first in case the direct hit kills the target if (data.isSplash) { - let playersToDamage = []; - if (!data.friendlyFire) - playersToDamage = QueryPlayerIDInterface(data.attackerOwner).GetEnemies(); - else - { - let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); - for (let i = 0; i < numPlayers; ++i) - playersToDamage.push(i); - } - this.CauseSplashDamage({ "attacker": data.attacker, "origin": Vector2D.from3D(data.position), @@ -107,7 +113,7 @@ "shape": data.shape, "strengths": data.splashStrengths, "direction": data.direction, - "playersToDamage": playersToDamage, + "playersToDamage": this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire), "type": data.type, "attackerOwner": data.attackerOwner }); Index: binaries/data/mods/public/simulation/components/Health.js =================================================================== --- binaries/data/mods/public/simulation/components/Health.js +++ binaries/data/mods/public/simulation/components/Health.js @@ -217,33 +217,31 @@ // might get called multiple times) if (this.hitpoints) { + this.hitpoints = 0; + this.RegisterHealthChanged(oldHitpoints); state.killed = true; + let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + if (cmpAttack) + cmpAttack.CauseDeathDamage(); + PlaySound("death", this.entity); - // If SpawnEntityOnDeath is set, spawn the entity - if(this.template.SpawnEntityOnDeath) + if (this.template.SpawnEntityOnDeath) this.CreateDeathSpawnedEntity(); - if (this.template.DeathType == "corpse") - { - this.CreateCorpse(); - Engine.DestroyEntity(this.entity); - } - else if (this.template.DeathType == "vanish") + switch (this.template.DeathType) { - Engine.DestroyEntity(this.entity); - } - else if (this.template.DeathType == "remain") - { - var resource = this.CreateCorpse(true); - if (resource != INVALID_ENTITY) - Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: resource }); - Engine.DestroyEntity(this.entity); + case "corpse": + this.CreateCorpse(); + break; + case "remain": + let resource = this.CreateCorpse(true); + if (resource != INVALID_ENTITY) + Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: resource }); } - this.hitpoints = 0; - this.RegisterHealthChanged(oldHitpoints); + Engine.DestroyEntity(this.entity); } } Index: binaries/data/mods/public/simulation/components/Player.js =================================================================== --- binaries/data/mods/public/simulation/components/Player.js +++ binaries/data/mods/public/simulation/components/Player.js @@ -676,6 +676,11 @@ return this.GetPlayersByDiplomacy("IsEnemy"); }; +Player.prototype.GetNeutrals = function() +{ + return this.GetPlayersByDiplomacy("IsNeutral"); +}; + Player.prototype.SetNeutral = function(id) { this.SetDiplomacyIndex(id, 0); @@ -689,6 +694,16 @@ return this.diplomacy[id] == 0; }; +Player.prototype.GetAllPlayers = function() +{ + return this.GetPlayersByDiplomacy("IsAny"); +}; + +Player.prototype.IsAny = function(id) +{ + return true; +} + /** * Do some map dependant initializations */ 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 @@ -6,7 +6,10 @@ Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Attack.js"); +Engine.LoadComponentScript("interfaces/Damage.js"); +Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Attack.js"); +Engine.LoadComponentScript("Timer.js"); let entityID = 903; @@ -31,7 +34,8 @@ AddMock(attacker, IID_Position, { "IsInWorld": () => true, - "GetHeightOffset": () => 5 + "GetHeightOffset": () => 5, + "GetPosition2D": () => new Vector2D(1, 2) }); AddMock(attacker, IID_Ownership, { @@ -67,18 +71,36 @@ "MaxRange": 80, "PrepareTime": 300, "RepeatTime": 500, + "ProjectileSpeed": 50, + "Spread": 2.5, "PreferredClasses": { "_string": "Archer" }, "RestrictedClasses": { "_string": "Elephant" + }, + "Splash" : { + "Shape": "Circular", + "Range": 10, + "FriendlyFire": "false", + "Hack": 0.0, + "Pierce": 15.0, + "Crush": 35.0 } }, "Capture" : { "Value": 8, "MaxRange": 10, }, - "Slaughter": {} + "Slaughter": {}, + "Death": { + "Shape": "Circular", + "Range": 20, + "FriendlyFire": "false", + "Hack": 1000.0, + "Pierce": 1000.0, + "Crush": 500.0 + } }); let defender = ++entityID; @@ -134,6 +156,8 @@ "prepare": 0, "repeat": 1000 }); + + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashDamage("Ranged"), { "hack": 0, "pierce": 15, "crush": 35, "friendlyFire": false, "shape": "Circular" }); }); for (let className of ["Infantry", "Cavalry"]) @@ -219,6 +243,31 @@ testGetBestAttackAgainst("Domestic", "Slaughter"); testGetBestAttackAgainst("Structure", "Capture", true); +attackComponentTest(undefined, false, (attacker, cmpAttack, defender) => { + + let causeSplashDamageArg = ""; + + AddMock(SYSTEM_ENTITY, IID_Damage, { + "CauseDamage": () => false, + "SetTimeout": () => false, + "GetPlayersToDamage": () => [1, 2], + "CauseSplashDamage": (arg) => { causeSplashDamageArg = arg; } + }); + + cmpAttack.CauseDeathDamage(); + + TS_ASSERT_UNEVAL_EQUALS(causeSplashDamageArg, { + attacker: attacker, + origin: { "x": 1, "y": 2 }, + radius: 20, + shape: "Circular", + strengths: { "hack": 1000, "pierce": 1000, "crush": 500 }, + playersToDamage: [1, 2], + type: "Death", + attackerOwner: 1 + }); +}); + function testPredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity) { let cmpAttack = ConstructComponent(1, "Attack", {}); 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 @@ -49,10 +49,12 @@ AddMock(atkPlayerEntity, IID_Player, { GetEnemies: () => [targetOwner], + GetNeutrals: () => [] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { GetPlayerByID: (id) => atkPlayerEntity, + GetNumPlayers: () => 5 }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { @@ -122,3 +124,11 @@ cmpAttack.PerformAttack("Ranged", target); Engine.DestroyEntity(attacker); TestDamage(); + +atkPlayerEntity = 1; +AddMock(atkPlayerEntity, IID_Player, { + GetEnemies: () => [2, 3], + GetAllPlayers: () => [0, 1, 2, 3, 4] +}); +TS_ASSERT_UNEVAL_EQUALS(cmpDamage.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]); +TS_ASSERT_UNEVAL_EQUALS(cmpDamage.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]); Index: binaries/data/mods/public/simulation/components/tests/test_Player.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Player.js +++ binaries/data/mods/public/simulation/components/tests/test_Player.js @@ -52,6 +52,8 @@ cmpPlayer.SetDiplomacy([-1, 1, 0, 1, -1]); TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetAllies(), [1, 3]); TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetEnemies(), [0, 4]); +TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetNeutrals(), [2]); +TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetAllPlayers(), [0, 1, 2, 3, 4]); var diplo = cmpPlayer.GetDiplomacy(); diplo[0] = 1; Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml @@ -9,6 +9,14 @@ 50 100 + + Circular + 30 + true + 300.0 + 300.0 + 300.0 + 30