Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js @@ -178,6 +178,16 @@ } } + if (template.DeathDamage) + { + ret.deathDamage = { + "hack": getEntityValue("DeathDamage/Hack"), + "pierce": getEntityValue("DeathDamage/Pierce"), + "crush": getEntityValue("DeathDamage/Crush"), + "friendlyFire": template.DeathDamage.FriendlyFire != "false" + }; + } + if (template.Auras) { ret.auras = {}; 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 @@ -65,6 +65,27 @@ }; /** + * 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 {number[]} - the ids of players need to be damaged + */ +Damage.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire) +{ + let cmpPlayer = QueryPlayerIDInterface(attackerOwner); + + if (!friendlyFire) + return cmpPlayer.GetEnemies(); + + let playersToDamage = []; + let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + for (let i = 0; i < numPlayers; ++i) + playersToDamage.push(i) + + return playersToDamage; +} + +/** * 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 +111,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 +118,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: 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 @@ -0,0 +1,87 @@ +function DeathDamage() {} + +DeathDamage.prototype.bonusesSchema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + +DeathDamage.prototype.Schema = + "When a unit or building is destroyed, it inflicts damage to nearby units." + + "" + + "Circular" + + "20" + + "false" + + "0.0" + + "10.0" + + "50.0" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + DeathDamage.prototype.bonusesSchema; + +DeathDamage.prototype.Init = function() +{ +}; + +DeathDamage.prototype.Serialize = null; // we have no dynamic state to save + +DeathDamage.prototype.GetDeathDamageStrengths = function(type) +{ + // Work out the damage values with technology effects + let applyMods = damageType => + ApplyValueModificationsToEntity("DeathDamage/" + damageType, +(this.template[damageType] || 0), this.entity); + + return { + "hack": applyMods("Hack"), + "pierce": applyMods("Pierce"), + "crush": applyMods("Crush") + }; +}; + +DeathDamage.prototype.CauseDeathDamage = function() +{ + 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); + let owner = cmpOwnership.GetOwner(); + if (owner == -1) + warn("Unit causing death damage does not have any owner."); + + let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); + let playersToDamage = cmpDamage.GetPlayersToDamage(owner, this.template.FriendlyFire); + + let radius = ApplyValueModificationsToEntity("DeathDamage/Range", +this.template.Range, this.entity); + + cmpDamage.CauseSplashDamage({ + "attacker": this.entity, + "origin": pos, + "radius": radius, + "shape": this.template.Shape, + "strengths": this.GetDeathDamageStrengths("Death"), + "playersToDamage": playersToDamage, + "type": "Death", + "attackerOwner": owner + }); +}; + +Engine.RegisterComponentType(IID_DeathDamage, "DeathDamage", DeathDamage); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -434,6 +434,7 @@ "armour": null, "attack": null, "buildingAI": null, + "deathDamage": null, "heal": null, "isBarterMarket": null, "loot": null, @@ -521,6 +522,14 @@ "arrowCount": cmpBuildingAI.GetArrowCount() }; + let cmpDeathDamage = Engine.QueryInterface(ent, IID_DeathDamage); + if (cmpDeathDamage) + ret.deathDeath = { + "hack": cmpDeathDamage.GetDeathDamageStrengths("hack"), + "pierce": cmpDeathDamage.GetDeathDamageStrengths("pierce"), + "crush": cmpDeathDamage.GetDeathDamageStrengths("crush") + }; + let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (cmpObstruction) ret.obstruction = { Index: ps/trunk/binaries/data/mods/public/simulation/components/Health.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Health.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Health.js @@ -217,33 +217,41 @@ // might get called multiple times) if (this.hitpoints) { + this.hitpoints = 0; + this.RegisterHealthChanged(oldHitpoints); state.killed = true; + let cmpDeathDamage = Engine.QueryInterface(this.entity, IID_DeathDamage); + if (cmpDeathDamage) + cmpDeathDamage.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") + switch (this.template.DeathType) { + case "corpse": this.CreateCorpse(); - Engine.DestroyEntity(this.entity); - } - else if (this.template.DeathType == "vanish") - { - Engine.DestroyEntity(this.entity); - } - else if (this.template.DeathType == "remain") + break; + + case "remain": { - var resource = this.CreateCorpse(true); + let resource = this.CreateCorpse(true); if (resource != INVALID_ENTITY) Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: resource }); - Engine.DestroyEntity(this.entity); } - this.hitpoints = 0; - this.RegisterHealthChanged(oldHitpoints); + case "vanish": + break; + + default: + error("Invalid template.DeathType: " + this.template.DeathType); + break; + } + + Engine.DestroyEntity(this.entity); } } Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DeathDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DeathDamage.js +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DeathDamage.js @@ -0,0 +1 @@ +Engine.RegisterInterface("DeathDamage"); 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 @@ -31,7 +31,8 @@ AddMock(attacker, IID_Position, { "IsInWorld": () => true, - "GetHeightOffset": () => 5 + "GetHeightOffset": () => 5, + "GetPosition2D": () => new Vector2D(1, 2) }); AddMock(attacker, IID_Ownership, { @@ -67,11 +68,21 @@ "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" : { @@ -134,6 +145,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"]) 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 @@ -48,11 +48,12 @@ }; AddMock(atkPlayerEntity, IID_Player, { - GetEnemies: () => [targetOwner], + GetEnemies: () => [targetOwner] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { GetPlayerByID: (id) => atkPlayerEntity, + GetNumPlayers: () => 5 }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { @@ -122,3 +123,10 @@ cmpAttack.PerformAttack("Ranged", target); Engine.DestroyEntity(attacker); TestDamage(); + +atkPlayerEntity = 1; +AddMock(atkPlayerEntity, IID_Player, { + GetEnemies: () => [2, 3] +}); +TS_ASSERT_UNEVAL_EQUALS(cmpDamage.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]); +TS_ASSERT_UNEVAL_EQUALS(cmpDamage.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -7,6 +7,7 @@ Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/CeasefireManager.js"); Engine.LoadComponentScript("interfaces/DamageReceiver.js"); +Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/EndGameManager.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); @@ -644,6 +645,7 @@ armour: null, attack: null, buildingAI: null, + deathDamage:null, heal: null, isBarterMarket: true, loot: null, Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml @@ -10,6 +10,14 @@ 100 + + Circular + 30 + true + 300.0 + 300.0 + 300.0 + 30 Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/theb_mechanical_siege_fireraiser.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/theb_mechanical_siege_fireraiser.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/theb_mechanical_siege_fireraiser.xml @@ -13,6 +13,14 @@ 2.0 + + Circular + 20 + true + 200.0 + 200.0 + 200.0 + 4.5