From f504c643a01ba1926b3e6dd71eaba23aa5f0a550 Mon Sep 17 00:00:00 2001 From: wraitii Date: Sun, 26 May 2019 12:44:46 +0000 Subject: [PATCH] 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. --- .../public/simulation/components/Attack.js | 20 ++- .../public/simulation/components/Damage.js | 8 +- .../components/StatusEffectsReceiver.js | 69 ++++++++++ .../interfaces/StatusEffectsReceiver.js | 1 + .../components/tests/test_Damage.js | 1 + .../tests/test_StatusEffectsReceiver.js | 119 ++++++++++++++++++ .../templates/template_structure.xml | 1 + .../simulation/templates/template_unit.xml | 1 + 8 files changed, 218 insertions(+), 2 deletions(-) create mode 100755 binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js create mode 100755 binaries/data/mods/public/simulation/components/interfaces/StatusEffectsReceiver.js create mode 100755 binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index ee44af9f38..0deda9e2c5 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -2,6 +2,22 @@ function Attack() {} var g_AttackTypes = ["Melee", "Ranged", "Capture"]; +Attack.prototype.statusEffectsSchema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + Attack.prototype.bonusesSchema = "" + "" + @@ -187,6 +203,7 @@ Attack.prototype.Schema = "" + "" + "" + + 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) { diff --git a/binaries/data/mods/public/simulation/components/Damage.js b/binaries/data/mods/public/simulation/components/Damage.js index 9f1324d177..d6cc25fab6 100644 --- a/binaries/data/mods/public/simulation/components/Damage.js +++ b/binaries/data/mods/public/simulation/components/Damage.js @@ -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. diff --git a/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js b/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js new file mode 100755 index 0000000000..54f0a44bb4 --- /dev/null +++ b/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js @@ -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); diff --git a/binaries/data/mods/public/simulation/components/interfaces/StatusEffectsReceiver.js b/binaries/data/mods/public/simulation/components/interfaces/StatusEffectsReceiver.js new file mode 100755 index 0000000000..3aa8e460b7 --- /dev/null +++ b/binaries/data/mods/public/simulation/components/interfaces/StatusEffectsReceiver.js @@ -0,0 +1 @@ +Engine.RegisterInterface("StatusEffectsReceiver"); diff --git a/binaries/data/mods/public/simulation/components/tests/test_Damage.js b/binaries/data/mods/public/simulation/components/tests/test_Damage.js index e6edac1041..46b8083923 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -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"); diff --git a/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js b/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js new file mode 100755 index 0000000000..30bfcd7f35 --- /dev/null +++ b/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.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(); diff --git a/binaries/data/mods/public/simulation/templates/template_structure.xml b/binaries/data/mods/public/simulation/templates/template_structure.xml index eb8c9e9538..706c818ac9 100644 --- a/binaries/data/mods/public/simulation/templates/template_structure.xml +++ b/binaries/data/mods/public/simulation/templates/template_structure.xml @@ -141,6 +141,7 @@ 0.6 12.0 + 20 diff --git a/binaries/data/mods/public/simulation/templates/template_unit.xml b/binaries/data/mods/public/simulation/templates/template_unit.xml index ef64d2d90b..e78416060c 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit.xml @@ -111,6 +111,7 @@ 0.333 5.0 + aggressive 12.0