Splash damage on death. When an entity dies, it can do a splash damage. Fire ship and fire raiser templates provided as example. Fix #1910.

Patch by Mate-86.
Advices from leper.
Reviewed by fatherbushido.
Differential Revision: https://code.wildfiregames.com/D451
This was SVN commit r19950.
This commit is contained in:
fatherbushido
2017-08-07 12:38:57 +00:00
parent 6e513069ad
commit 523ae47ee2
11 changed files with 192 additions and 27 deletions
@@ -178,6 +178,16 @@ function GetTemplateDataHelper(template, player, auraTemplates, resources, modif
}
}
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 = {};
@@ -64,6 +64,27 @@ Damage.prototype.TestCollision = function(ent, point, lateness)
return false;
};
/**
* 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.
@@ -90,16 +111,6 @@ Damage.prototype.MissileHit = function(data, lateness)
// 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 @@ Damage.prototype.MissileHit = function(data, lateness)
"shape": data.shape,
"strengths": data.splashStrengths,
"direction": data.direction,
"playersToDamage": playersToDamage,
"playersToDamage": this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire),
"type": data.type,
"attackerOwner": data.attackerOwner
});
@@ -0,0 +1,87 @@
function DeathDamage() {}
DeathDamage.prototype.bonusesSchema =
"<optional>" +
"<element name='Bonuses'>" +
"<zeroOrMore>" +
"<element>" +
"<anyName/>" +
"<interleave>" +
"<optional>" +
"<element name='Civ' a:help='If an entity has this civ then the bonus is applied'><text/></element>" +
"</optional>" +
"<element name='Classes' a:help='If an entity has all these classes then the bonus is applied'><text/></element>" +
"<element name='Multiplier' a:help='The attackers attack strength is multiplied by this'><ref name='nonNegativeDecimal'/></element>" +
"</interleave>" +
"</element>" +
"</zeroOrMore>" +
"</element>" +
"</optional>";
DeathDamage.prototype.Schema =
"<a:help>When a unit or building is destroyed, it inflicts damage to nearby units.</a:help>" +
"<a:example>" +
"<Shape>Circular</Shape>" +
"<Range>20</Range>" +
"<FriendlyFire>false</FriendlyFire>" +
"<Hack>0.0</Hack>" +
"<Pierce>10.0</Pierce>" +
"<Crush>50.0</Crush>" +
"</a:example>" +
"<element name='Shape' a:help='Shape of the splash damage, can be circular or linear'><text/></element>" +
"<element name='Range' a:help='Size of the area affected by the splash'><ref name='nonNegativeDecimal'/></element>" +
"<element name='FriendlyFire' a:help='Whether the splash damage can hurt non enemy units'><data type='boolean'/></element>" +
"<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
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);
@@ -434,6 +434,7 @@ GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
"armour": null,
"attack": null,
"buildingAI": null,
"deathDamage": null,
"heal": null,
"isBarterMarket": null,
"loot": null,
@@ -521,6 +522,14 @@ GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
"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 = {
@@ -217,33 +217,41 @@ Health.prototype.Reduce = function(amount)
// 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")
break;
case "remain":
{
Engine.DestroyEntity(this.entity);
}
else if (this.template.DeathType == "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);
}
}
@@ -0,0 +1 @@
Engine.RegisterInterface("DeathDamage");
@@ -31,7 +31,8 @@ function attackComponentTest(defenderClass, isEnemy, test_function)
AddMock(attacker, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 5
"GetHeightOffset": () => 5,
"GetPosition2D": () => new Vector2D(1, 2)
});
AddMock(attacker, IID_Ownership, {
@@ -67,11 +68,21 @@ function attackComponentTest(defenderClass, isEnemy, test_function)
"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 @@ attackComponentTest(undefined, true ,(attacker, cmpAttack, defender) => {
"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"])
@@ -48,11 +48,12 @@ let data = {
};
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 @@ TestDamage();
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]);
@@ -7,6 +7,7 @@ Engine.LoadComponentScript("interfaces/Builder.js");
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 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedEntityState(-1, 10), {
armour: null,
attack: null,
buildingAI: null,
deathDamage:null,
heal: null,
isBarterMarket: true,
loot: null,
@@ -10,6 +10,14 @@
<RepeatTime>100</RepeatTime>
</Melee>
</Attack>
<DeathDamage>
<Shape>Circular</Shape>
<Range>30</Range>
<FriendlyFire>true</FriendlyFire>
<Hack>300.0</Hack>
<Pierce>300.0</Pierce>
<Crush>300.0</Crush>
</DeathDamage>
<Cost>
<BuildTime>30</BuildTime>
<Resources>
@@ -13,6 +13,14 @@
<Spread>2.0</Spread>
</Ranged>
</Attack>
<DeathDamage>
<Shape>Circular</Shape>
<Range>20</Range>
<FriendlyFire>true</FriendlyFire>
<Hack>200.0</Hack>
<Pierce>200.0</Pierce>
<Crush>200.0</Crush>
</DeathDamage>
<Footprint replace="">
<Square width="6.0" depth="20.0"/>
<Height>4.5</Height>