mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-06-21 14:43:52 +00:00
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:
@@ -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>
|
||||
|
||||
+8
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user