Allow entities to be affected by Status Effects from ranged attacks.

This implements a status effects receiver component (in a similar
fashion to the damage receiver component). The plan is to further extend
this component notably to handle graphical indication of status effects,
and a variety of other effects.

Currently implemented: ranged attacks can inflict status effects that
reduce HP over time.
This can be resisted by armour.

No units currently utilise this in-game but with proper graphics support
that could be changed.

Patch By: Mate-86
Reviewed By: wraitii
References #1912

Differential Revision: https://code.wildfiregames.com/D1252
This was SVN commit r22304.
This commit is contained in:
wraitii
2019-05-26 12:44:46 +00:00
parent 65fcd66556
commit f504c643a0
8 changed files with 218 additions and 2 deletions
@@ -2,6 +2,22 @@ function Attack() {}
var g_AttackTypes = ["Melee", "Ranged", "Capture"];
Attack.prototype.statusEffectsSchema =
"<optional>" +
"<element name='StatusEffects' a:help='Effects like poisioning or burning a unit.'>" +
"<oneOrMore>" +
"<element>" +
"<anyName/>" +
"<interleave>" +
"<element name='Duration' a:help='The duration of the status while the effect occurs.'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Interval' a:help='Interval between the occurances of the effect.'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Damage' a:help='Damage caused by the effect.'><ref name='nonNegativeDecimal'/></element>" +
"</interleave>" +
"</element>" +
"</oneOrMore>" +
"</element>" +
"</optional>";
Attack.prototype.bonusesSchema =
"<optional>" +
"<element name='Bonuses'>" +
@@ -187,6 +203,7 @@ Attack.prototype.Schema =
"</optional>" +
"</interleave>" +
"</element>" +
Attack.prototype.statusEffectsSchema +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
@@ -577,7 +594,8 @@ Attack.prototype.PerformAttack = function(type, target)
"bonus": this.GetBonusTemplate(type),
"isSplash": false,
"attackerOwner": attackerOwner,
"attackImpactSound": attackImpactSound
"attackImpactSound": attackImpactSound,
"statusEffects": this.template[type].StatusEffects
};
if (this.template[type].Splash)
{
@@ -93,6 +93,7 @@ Damage.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
* @param {Vector3D} data.direction - the unit vector defining the direction.
* @param {Object} data.bonus - the attack bonus template from the attacker.
* @param {string} data.attackImpactSound - the name of the sound emited on impact.
* @param {Object} data.statusEffects - status effects eg. poisoning, burning etc.
* ***When splash damage***
* @param {boolean} data.friendlyFire - a flag indicating if allied entities are also damaged.
* @param {number} data.radius - the radius of the splash damage.
@@ -136,6 +137,11 @@ Damage.prototype.MissileHit = function(data, lateness)
data.multiplier = GetDamageBonus(data.target, data.bonus);
this.CauseDamage(data);
cmpProjectileManager.RemoveProjectile(data.projectileId);
let cmpStatusReceiver = Engine.QueryInterface(data.target, IID_StatusEffectsReceiver);
if (cmpStatusReceiver && data.statusEffects)
cmpStatusReceiver.InflictEffects(data.statusEffects);
return;
}
@@ -236,7 +242,7 @@ Damage.prototype.CauseSplashDamage = function(data)
* @param {Object} data - the data passed by the caller.
* @param {Object} data.strengths - data in the form of { 'hack': number, 'pierce': number, 'crush': number }.
* @param {number} data.target - the entity id of the target.
* @param {number} data.attacker - the entity id og the attacker.
* @param {number} data.attacker - the entity id of the attacker.
* @param {number} data.multiplier - the damage multiplier.
* @param {string} data.type - the type of damage.
* @param {number} data.attackerOwner - the player id of the attacker.
@@ -0,0 +1,69 @@
function StatusEffectsReceiver() {}
StatusEffectsReceiver.prototype.Init = function()
{
this.activeStatusEffects = {};
};
StatusEffectsReceiver.prototype.InflictEffects = function(statusEffects)
{
for (let effect in statusEffects)
this.InflictEffect(effect, statusEffects[effect]);
};
StatusEffectsReceiver.prototype.InflictEffect = function(statusName, data)
{
if (this.activeStatusEffects[statusName])
return;
this.activeStatusEffects[statusName] = {};
let status = this.activeStatusEffects[statusName];
status.duration = +data.Duration;
status.interval = +data.Interval;
status.damage = +data.Damage;
status.timeElapsed = 0;
status.firstTime = true;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
status.timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +status.interval, statusName);
};
StatusEffectsReceiver.prototype.RemoveEffect = function(statusName) {
if (!this.activeStatusEffects[statusName])
return;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.activeStatusEffects[statusName].timer);
this.activeStatusEffects[statusName] = undefined;
};
StatusEffectsReceiver.prototype.ExecuteEffect = function(statusName, lateness)
{
let status = this.activeStatusEffects[statusName];
if (!status)
return;
if (status.firstTime)
{
status.firstTime = false;
status.timeElapsed += lateness;
}
else
status.timeElapsed += status.interval + lateness;
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
cmpDamage.CauseDamage({
"strengths": { [statusName]: status.damage },
"target": this.entity,
"attacker": -1,
"multiplier": 1,
"type": statusName,
"attackerOwner": -1
});
if (status.timeElapsed >= status.duration)
this.RemoveEffect(statusName);
};
Engine.RegisterComponentType(IID_StatusEffectsReceiver, "StatusEffectsReceiver", StatusEffectsReceiver);
@@ -0,0 +1 @@
Engine.RegisterInterface("StatusEffectsReceiver");
@@ -12,6 +12,7 @@ Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Player.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("Attack.js");
@@ -0,0 +1,119 @@
Engine.LoadComponentScript("interfaces/Damage.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("StatusEffectsReceiver.js");
Engine.LoadComponentScript("Timer.js");
var target = 42;
var cmpStatusReceiver;
var cmpTimer;
var dealtDamage;
function setup()
{
cmpStatusReceiver = ConstructComponent(target, "StatusEffectsReceiver");
cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
dealtDamage = 0;
}
function testInflictEffects()
{
setup();
let statusName = "Burn";
AddMock(SYSTEM_ENTITY, IID_Damage, {
"CauseDamage": (data) => { dealtDamage += data.strengths[statusName]; }
});
// damage scheduled: 0, 10, 20 sec
cmpStatusReceiver.InflictEffects({
[statusName]: {
"Duration": 20000,
"Interval": 10000,
"Damage": 1
}
});
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec
cmpTimer.OnUpdate({ "turnLength": 8 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 9 sec
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 2); // 10 sec
cmpTimer.OnUpdate({ "turnLength": 10 });
TS_ASSERT_EQUALS(dealtDamage, 3); // 20 sec
cmpTimer.OnUpdate({ "turnLength": 10 });
TS_ASSERT_EQUALS(dealtDamage, 3); // 30 sec
}
testInflictEffects();
function testMultipleEffects()
{
setup();
AddMock(SYSTEM_ENTITY, IID_Damage, {
"CauseDamage": (data) => {
if (data.strengths.Burn) dealtDamage += data.strengths.Burn;
if (data.strengths.Poison) dealtDamage += data.strengths.Poison;
}
});
// damage scheduled: 0, 1, 2, 10 sec
cmpStatusReceiver.InflictEffects({
"Burn": {
"Duration": 20000,
"Interval": 10000,
"Damage": 10
},
"Poison": {
"Duration": 3000,
"Interval": 1000,
"Damage": 1
}
});
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 12); // 1 sec
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 13); // 2 sec
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 13); // 3 sec
cmpTimer.OnUpdate({ "turnLength": 7 });
TS_ASSERT_EQUALS(dealtDamage, 23); // 10 sec
}
testMultipleEffects();
function testRemoveEffect()
{
setup();
let statusName = "Poison";
AddMock(SYSTEM_ENTITY, IID_Damage, {
"CauseDamage": (data) => { dealtDamage += data.strengths[statusName]; }
});
// damage scheduled: 0, 10, 20 sec
cmpStatusReceiver.InflictEffects({
[statusName]: {
"Duration": 20000,
"Interval": 10000,
"Damage": 1
}
});
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec
cmpStatusReceiver.RemoveEffect(statusName);
cmpTimer.OnUpdate({ "turnLength": 10 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 11 sec
}
testRemoveEffect();
@@ -141,6 +141,7 @@
<BarHeight>0.6</BarHeight>
<HeightOffset>12.0</HeightOffset>
</StatusBars>
<StatusEffectsReceiver/>
<TerritoryDecay>
<DecayRate>20</DecayRate>
</TerritoryDecay>
@@ -111,6 +111,7 @@
<BarHeight>0.333</BarHeight>
<HeightOffset>5.0</HeightOffset>
</StatusBars>
<StatusEffectsReceiver/>
<UnitAI>
<DefaultStance>aggressive</DefaultStance>
<FleeDistance>12.0</FleeDistance>