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