From ea67b7966752156e7d30de2fa77fc6b89a13f528 Mon Sep 17 00:00:00 2001 From: wraitii Date: Tue, 9 Jun 2020 11:47:16 +0000 Subject: [PATCH] Handle stacking status effects, modifiers, some related fixes. Continuation of work on status effects, following 82a5ab6d19. Allow techs to modify properties. Enable different behaviour when status effects would stack, such as extending the duration or stacking the effects. Fix GUI issue with more than 5 status effects. Improve tooltips. Let status effects show a different tooltip for the attacker and the receiver. Patch by: Freagarach Reviewed By: wraitii, Angen (on an older differential). Differential Revision: https://code.wildfiregames.com/D2296 This was SVN commit r23757. --- .../data/mods/public/gui/common/tooltips.js | 84 ++-- .../public/gui/session/selection_details.js | 6 +- .../single_details_area.xml | 2 +- binaries/data/mods/public/l10n/messages.json | 18 +- .../components/StatusEffectsReceiver.js | 22 +- .../components/tests/test_Attack.js | 50 ++- .../tests/test_StatusEffectsReceiver.js | 402 +++++++++++++----- .../public/simulation/helpers/Attacking.js | 56 ++- 8 files changed, 498 insertions(+), 142 deletions(-) diff --git a/binaries/data/mods/public/gui/common/tooltips.js b/binaries/data/mods/public/gui/common/tooltips.js index 8465170fe1..7f5de823e3 100644 --- a/binaries/data/mods/public/gui/common/tooltips.js +++ b/binaries/data/mods/public/gui/common/tooltips.js @@ -333,7 +333,7 @@ function getAttackTooltip(template) for (let status in attackTypeTemplate.ApplyStatus) { let status_template = g_StatusEffectsMetadata.augment(status, attackTypeTemplate.ApplyStatus[status]); - statusEffectsDetails.push("\n " + getStatusEffectsTooltip(status_template)); + statusEffectsDetails.push("\n " + getStatusEffectsTooltip(status_template, true)); } statusEffectsDetails = statusEffectsDetails.join(""); @@ -380,48 +380,66 @@ function getSplashDamageTooltip(template) return tooltips.join("\n"); } -function getStatusEffectsTooltip(template) +/** + * @param applier - if true, return the tooltip for the Applier. If false, Receiver is returned. + */ +function getStatusEffectsTooltip(template, applier) { let tooltipAttributes = []; - let tooltipString = ""; - if (template.StatusTooltip) - { - tooltipAttributes.push("%(tooltip)s"); - tooltipString = translate(template.StatusTooltip); - } + if (applier && template.ApplierTooltip) + tooltipAttributes.push(translate(template.ApplierTooltip)); + else if (!applier && template.ReceiverTooltip) + tooltipAttributes.push(translate(template.ReceiverTooltip)); - let attackEffectsString = ""; if (template.Damage || template.Capture) - { - tooltipAttributes.push("%(effects)s"); - attackEffectsString = attackEffectsDetails(template); - } + tooltipAttributes.push(attackEffectsDetails(template)); - let intervalString = ""; if (template.Interval) - { - tooltipAttributes.push("%(rate)s"); - intervalString = sprintf(translate("%(interval)s"), { - "interval": attackRateDetails(+template.Interval) - }); - } + tooltipAttributes.push(attackRateDetails(+template.Interval)); - let durationString = ""; if (template.Duration) - { - tooltipAttributes.push("%(duration)s"); - durationString = sprintf(translate("%(durName)s: %(duration)s"), { - "durName": headerFont(translate("Duration")), - "duration": getSecondsString((template._timeElapsed ? +template.Duration - template._timeElapsed : +template.Duration) / 1000), - }); - } + tooltipAttributes.push(getStatusEffectDurationTooltip(template)); - return sprintf(translate("%(statusName)s: " + tooltipAttributes.join(translate(commaFont(", ")))), { + if (applier) + return sprintf(translate("%(statusName)s: %(statusInfo)s %(stackability)s"), { + "statusName": headerFont(translateWithContext("status effect", template.StatusName)), + "statusInfo": tooltipAttributes.join(commaFont(translate(", "))), + "stackability": getStatusEffectStackabilityTooltip(template) + }); + return sprintf(translate("%(statusName)s: %(statusInfo)s"), { "statusName": headerFont(translateWithContext("status effect", template.StatusName)), - "tooltip": tooltipString, - "effects": attackEffectsString, - "rate": intervalString, - "duration": durationString + "statusInfo": tooltipAttributes.join(commaFont(translate(", "))) + }); +} + +function getStatusEffectDurationTooltip(template) +{ + if (!template.Duration) + return ""; + + return sprintf(translate("%(durName)s: %(duration)s"), { + "durName": headerFont(translate("Duration")), + "duration": getSecondsString((template._timeElapsed ? + +template.Duration - template._timeElapsed : + +template.Duration) / 1000) + }); +} + +function getStatusEffectStackabilityTooltip(template) +{ + if (!template.Stackability || template.Stackability == "Ignore") + return ""; + + let stackabilityString = ""; + if (template.Stackability === "Extend") + stackabilityString = translateWithContext("status effect stackability", "(extends)"); + else if (template.Stackability === "Replace") + stackabilityString = translateWithContext("status effect stackability", "(replaces)"); + else if (template.Stackability === "Stack") + stackabilityString = translateWithContext("status effect stackability", "(stacks)"); + + return sprintf(translate("%(stackability)s"), { + "stackability": stackabilityString }); } diff --git a/binaries/data/mods/public/gui/session/selection_details.js b/binaries/data/mods/public/gui/session/selection_details.js index 43d77c273e..b12b3154ef 100644 --- a/binaries/data/mods/public/gui/session/selection_details.js +++ b/binaries/data/mods/public/gui/session/selection_details.js @@ -103,12 +103,14 @@ function displaySingle(entState) let effect = entState.statusEffects[effectName]; statusIcons[i].hidden = false; statusIcons[i].sprite = "stretched:session/icons/status_effects/" + (effect.Icon || "default") + ".png"; - statusIcons[i].tooltip = getStatusEffectsTooltip(effect); + statusIcons[i].tooltip = getStatusEffectsTooltip(effect, false); let size = statusIcons[i].size; size.top = i * 18; size.bottom = i * 18 + 16; statusIcons[i].size = size; - i++; + + if (++i >= statusIcons.length) + break; } for (; i < statusIcons.length; ++i) statusIcons[i].hidden = true; diff --git a/binaries/data/mods/public/gui/session/selection_panels_middle/single_details_area.xml b/binaries/data/mods/public/gui/session/selection_panels_middle/single_details_area.xml index b57cc7ad16..9d9299e100 100644 --- a/binaries/data/mods/public/gui/session/selection_panels_middle/single_details_area.xml +++ b/binaries/data/mods/public/gui/session/selection_panels_middle/single_details_area.xml @@ -83,7 +83,7 @@ Rank - + diff --git a/binaries/data/mods/public/l10n/messages.json b/binaries/data/mods/public/l10n/messages.json index 5806737916..2a51838a35 100644 --- a/binaries/data/mods/public/l10n/messages.json +++ b/binaries/data/mods/public/l10n/messages.json @@ -403,7 +403,10 @@ "StatusName": { "customContext": "status effect" }, - "StatusTooltip": { + "ApplierTooltip": { + "customContext": "status effect" + }, + "ReceiverTooltip": { "customContext": "status effect" }, "GenericName": {}, @@ -441,7 +444,10 @@ "StatusName": { "customContext": "status effect" }, - "StatusTooltip": { + "ApplierTooltip": { + "customContext": "status effect" + }, + "ReceiverTooltip": { "customContext": "status effect" }, "GenericName": {}, @@ -486,7 +492,10 @@ "StatusName": { "customContext": "status effect" }, - "StatusTooltip": { + "ApplierTooltip": { + "customContext": "status effect" + }, + "ReceiverTooltip": { "customContext": "status effect" }, "GenericName": {}, @@ -526,7 +535,8 @@ "options": { "keywords": [ "StatusName", - "StatusTooltip" + "ApplierTooltip", + "ReceiverTooltip" ], "context": "status effect" } diff --git a/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js b/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js index 0a19df0183..778a791506 100755 --- a/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js +++ b/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js @@ -46,14 +46,30 @@ StatusEffectsReceiver.prototype.ApplyStatus = function(effectData, attacker, att * * @param {string} statusName - The name of the status effect. * @param {object} data - The various effects and timings. + * @param {object} attackerData - The attacker and attackerOwner. */ StatusEffectsReceiver.prototype.AddStatus = function(statusName, data, attackerData) { if (this.activeStatusEffects[statusName]) { - // TODO: implement different behaviour when receiving the same status multiple times. - // For now, these are ignored. - return; + if (data.Stackability == "Ignore") + return; + if (data.Stackability == "Extend") + { + this.activeStatusEffects[statusName].Duration += data.Duration; + return; + } + if (data.Stackability == "Replace") + this.RemoveStatus(statusName); + else if (data.Stackability == "Stack") + { + let i = 0; + let temp; + do + temp = statusName + "_" + i++; + while (!!this.activeStatusEffects[temp]); + statusName = temp; + } } this.activeStatusEffects[statusName] = {}; diff --git a/binaries/data/mods/public/simulation/components/tests/test_Attack.js b/binaries/data/mods/public/simulation/components/tests/test_Attack.js index 07a2027be7..165d20c3dc 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -108,7 +108,31 @@ function attackComponentTest(defenderClass, isEnemy, test_function) "Capture": 8, "MaxRange": 10, }, - "Slaughter": {} + "Slaughter": {}, + "StatusEffect": { + "ApplyStatus": { + "StatusInternalName": { + "StatusName": "StatusShownName", + "ApplierTooltip": "ApplierTooltip", + "ReceiverTooltip": "ReceiverTooltip", + "Duration": 5000, + "Stackability": "Stacks", + "Modifiers": { + "SE": { + "Paths": { + "_string": "Health/Max" + }, + "Affects": { + "_string": "Unit" + }, + "Add": 10 + } + } + } + }, + "MinRange": "10", + "MaxRange": "80" + } }); let defender = ++entityID; @@ -175,6 +199,30 @@ attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => { } }); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("StatusEffect"), { + "ApplyStatus": { + "StatusInternalName": { + "StatusName": "StatusShownName", + "ApplierTooltip": "ApplierTooltip", + "ReceiverTooltip": "ReceiverTooltip", + "Duration": 5000, + "Interval": 0, + "Stackability": "Stacks", + "Modifiers": { + "SE": { + "Paths": { + "_string": "Health/Max" + }, + "Affects": { + "_string": "Unit" + }, + "Add": 10 + } + } + } + } + }); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), { "prepare": 300, "repeat": 500 diff --git a/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js b/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js index de38f6adc8..00e97ce33a 100755 --- a/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js +++ b/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js @@ -1,135 +1,345 @@ +Engine.LoadHelperScript("MultiKeyMap.js"); +Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("ValueModification.js"); +Engine.LoadComponentScript("interfaces/Health.js"); +Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/Timer.js"); +Engine.LoadComponentScript("Health.js"); +Engine.LoadComponentScript("ModifiersManager.js"); Engine.LoadComponentScript("StatusEffectsReceiver.js"); Engine.LoadComponentScript("Timer.js"); -var target = 42; -var cmpStatusReceiver; -var cmpTimer; -var dealtDamage; -var enemyEntity = 4; -var enemy = 2; +let target = 42; +let cmpStatusReceiver = ConstructComponent(target, "StatusEffectsReceiver"); +let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); +let dealtDamage = 0; +let enemyEntity = 4; +let enemy = 2; +let statusName; -function setup() +let Attacking = { + "HandleAttackEffects": (_, attackData) => { + for (let type in attackData.Damage) + dealtDamage += attackData.Damage[type]; + } +}; +Engine.RegisterGlobal("Attacking", Attacking); + +function reset() { - cmpStatusReceiver = ConstructComponent(target, "StatusEffectsReceiver"); - cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); + for (let status in cmpStatusReceiver.GetActiveStatuses()) + cmpStatusReceiver.RemoveStatus(status); dealtDamage = 0; } -function testInflictEffects() -{ - setup(); - let statusName = "Burn"; - let Attacking = { - "HandleAttackEffects": (_, attackData) => { dealtDamage += attackData.Damage[statusName]; } - }; - Engine.RegisterGlobal("Attacking", Attacking); +// Test adding a single effect. +statusName = "Burn"; - // damage scheduled: 0, 10, 20 sec - cmpStatusReceiver.AddStatus(statusName, { +// Damage scheduled: 0, 10, 20 seconds. +cmpStatusReceiver.AddStatus(statusName, { + "Duration": 20000, + "Interval": 10000, + "Damage": { + [statusName]: 1 + } +}, +{ + "entity": enemyEntity, + "owner": enemy, +}); + +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 + + +// Test adding multiple effects. +reset(); + +// Damage scheduled: 0, 1, 2, 10 seconds. +cmpStatusReceiver.ApplyStatus({ + "Burn": { "Duration": 20000, "Interval": 10000, "Damage": { - [statusName]: 1 + "Burn": 10 } }, - { - "entity": enemyEntity, - "owner": enemy, - }); + "Poison": { + "Duration": 3000, + "Interval": 1000, + "Damage": { + "Poison": 1 + } + } +}); - cmpTimer.OnUpdate({ "turnLength": 1 }); - TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 12); // 1 sec - cmpTimer.OnUpdate({ "turnLength": 8 }); - TS_ASSERT_EQUALS(dealtDamage, 1); // 9 sec +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 13); // 2 sec - cmpTimer.OnUpdate({ "turnLength": 1 }); - TS_ASSERT_EQUALS(dealtDamage, 2); // 10 sec +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 13); // 3 sec - cmpTimer.OnUpdate({ "turnLength": 10 }); - TS_ASSERT_EQUALS(dealtDamage, 3); // 20 sec +cmpTimer.OnUpdate({ "turnLength": 7 }); +TS_ASSERT_EQUALS(dealtDamage, 23); // 10 sec - cmpTimer.OnUpdate({ "turnLength": 10 }); - TS_ASSERT_EQUALS(dealtDamage, 3); // 30 sec -} -testInflictEffects(); +// Test removing a status removes effects. +reset(); +statusName = "Poison"; -function testMultipleEffects() +// Damage scheduled: 0, 10, 20 seconds. +cmpStatusReceiver.AddStatus(statusName, { + "Duration": 20000, + "Interval": 10000, + "Damage": { + [statusName]: 1 + } +}, { - setup(); - let Attacking = { - "HandleAttackEffects": (_, attackData) => { - if (attackData.Damage.Burn) dealtDamage += attackData.Damage.Burn; - if (attackData.Damage.Poison) dealtDamage += attackData.Damage.Poison; - }, - }; - Engine.RegisterGlobal("Attacking", Attacking); + "entity": enemyEntity, + "owner": enemy, +}); - // damage scheduled: 0, 1, 2, 10 sec +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec + +cmpStatusReceiver.RemoveStatus(statusName); + +cmpTimer.OnUpdate({ "turnLength": 10 }); +TS_ASSERT_EQUALS(dealtDamage, 1); // 11 sec + + +// Test that a status effect with modifications modifies. +reset(); + +AddMock(target, IID_Identity, { + "GetClassesList": () => ["AffectedClass"] +}); +let cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager"); + +let maxHealth = 100; +AddMock(target, IID_Health, { + "GetMaxHitpoints": () => ApplyValueModificationsToEntity("Health/Max", maxHealth, target) +}); + +statusName = "Haste"; +let factor = 0.5; +cmpStatusReceiver.AddStatus(statusName, { + "Duration": 5000, + "Modifiers": { + [statusName]: { + "Paths": { + "_string": "Health/Max" + }, + "Affects": { + "_string": "AffectedClass" + }, + "Multiply": factor + } + } +}, +{ + "entity": enemyEntity, + "owner": enemy, +}); + +let cmpHealth = Engine.QueryInterface(target, IID_Health); +// Test that the modification is applied. +TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth * factor); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth * factor); + +// Test that the modification is removed after the appropriate time. +cmpTimer.OnUpdate({ "turnLength": 4 }); +TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth); + + +// Test addition. +let addition = 50; +cmpStatusReceiver.AddStatus(statusName, { + "Duration": 5000, + "Modifiers": { + [statusName]: { + "Paths": { + "_string": "Health/Max" + }, + "Affects": { + "_string": "AffectedClass" + }, + "Add": addition + } + } +}, +{ + "entity": enemyEntity, + "owner": enemy, +}); + +// Test that the addition modification is applied. +TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth + addition); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth + addition); + +// Test that the modification is removed after the appropriate time. +cmpTimer.OnUpdate({ "turnLength": 4 }); +TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth); + + +// Test replacement. +let newValue = 50; +cmpStatusReceiver.AddStatus(statusName, { + "Duration": 5000, + "Modifiers": { + [statusName]: { + "Paths": { + "_string": "Health/Max" + }, + "Affects": { + "_string": "AffectedClass" + }, + "Replace": newValue + } + } +}, +{ + "entity": enemyEntity, + "owner": enemy, +}); + +// Test that the replacement modification is applied. +TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), newValue); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), newValue); + +// Test that the modification is removed after the appropriate time. +cmpTimer.OnUpdate({ "turnLength": 4 }); +TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), maxHealth); + + +function applyStatus(stackability) +{ cmpStatusReceiver.ApplyStatus({ - "Burn": { - "Duration": 20000, - "Interval": 10000, - "Damage": { - "Burn": 10 - } - }, - "Poison": { + "randomName": { "Duration": 3000, "Interval": 1000, "Damage": { - "Poison": 1 - } + "randomName": 1 + }, + "Stackability": stackability } }); - - 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 testRemoveStatus() -{ - setup(); - let statusName = "Poison"; - let Attacking = { - "HandleAttackEffects": (_, attackData) => { dealtDamage += attackData.Damage[statusName]; } - }; - Engine.RegisterGlobal("Attacking", Attacking); +// Test different stackabilities. +// First ignoring, i.e. next time the same status is added it is just ignored. +reset(); +applyStatus("Ignore"); - // damage scheduled: 0, 10, 20 sec - cmpStatusReceiver.AddStatus(statusName, { - "Duration": 20000, - "Interval": 10000, - "Damage": { - [statusName]: 1 - } - }, - { - "entity": enemyEntity, - "owner": enemy, - }); +// 1 Second: 1 update and lateness. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 2); - cmpTimer.OnUpdate({ "turnLength": 1 }); - TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec +applyStatus("Ignore"); - cmpStatusReceiver.RemoveStatus(statusName); +// 2 Seconds. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 3); - cmpTimer.OnUpdate({ "turnLength": 10 }); - TS_ASSERT_EQUALS(dealtDamage, 1); // 11 sec -} +// 3 Seconds: finished in previous turn. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 3); -testRemoveStatus(); + +// Extending, i.e. next time the same status is applied the times are added. +reset(); +applyStatus("Extend"); + +// 1 Second: 1 update and lateness. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 2); + +// Add 3 seconds. +applyStatus("Extend"); + +// 2 Seconds. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 3); + +// 3 Seconds: extended in previous turn. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 4); + +// 4 Seconds. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 5); + +// 5 Seconds. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 6); + +// 6 Seconds: finished in previous turn (3 + 3). +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 6); + + +// Replacing, i.e. the next applied status replaces the former. +reset(); +applyStatus("Replace"); + +// 1 Second: 1 update and lateness. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 2); + +applyStatus("Replace"); + +// 2 Seconds: 1 update and lateness of the new status. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 4); + +// 3 Seconds. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 5); + +// 4 Seconds: finished in previous turn. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 5); + + +// Stacking, every new status just applies besides the rest. +reset(); +applyStatus("Stack"); + +// 1 Second: 1 update and lateness. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 2); + +applyStatus("Stack"); + +// 2 Seconds: 1 damage from the previous status + 2 from the new (1 turn + lateness). +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 5); + +// 3 Seconds: first one finished in the previous turn, +1 from the new. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 6); + +// 4 Seconds: new status finished in previous turn. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(dealtDamage, 6); diff --git a/binaries/data/mods/public/simulation/helpers/Attacking.js b/binaries/data/mods/public/simulation/helpers/Attacking.js index 24a69ceed8..5bd5d01d95 100644 --- a/binaries/data/mods/public/simulation/helpers/Attacking.js +++ b/binaries/data/mods/public/simulation/helpers/Attacking.js @@ -32,7 +32,10 @@ const StatusEffectsSchema = "" + "" + "" + - "" + + "" + + "" + + "" + + "" + "" + "" + "" + @@ -50,6 +53,14 @@ const StatusEffectsSchema = "" + ModificationsSchema + "" + + "" + + "" + + "Ignore" + + "Extend" + + "Replace" + + "Stack" + + "" + + "" + "" + "" + "" + @@ -108,7 +119,7 @@ Attacking.prototype.GetAttackEffectsData = function(valueModifRoot, template, en ret.Capture = ApplyValueModificationsToEntity(valueModifRoot + "/Capture", +(template.Capture || 0), entity); if (template.ApplyStatus) - ret.ApplyStatus = template.ApplyStatus; + ret.ApplyStatus = this.GetStatusEffectsData(valueModifRoot, template.ApplyStatus, entity); if (template.Bonuses) ret.Bonuses = template.Bonuses; @@ -116,6 +127,47 @@ Attacking.prototype.GetAttackEffectsData = function(valueModifRoot, template, en return ret; }; +Attacking.prototype.GetStatusEffectsData = function(valueModifRoot, template, entity) +{ + let result = {}; + for (let effect in template) + { + let statusTemplate = template[effect]; + result[effect] = { + "StatusName": statusTemplate.StatusName, + "ApplierTooltip": statusTemplate.ApplierTooltip, + "ReceiverTooltip": statusTemplate.ReceiverTooltip, + "Duration": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Duration", +(statusTemplate.Duration || 0), entity), + "Interval": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Interval", +(statusTemplate.Interval || 0), entity), + "Stackability": statusTemplate.Stackability + }; + Object.assign(result[effect], this.GetAttackEffectsData(valueModifRoot + "/ApplyStatus" + effect, statusTemplate, entity)); + if (statusTemplate.Modifiers) + result[effect].Modifiers = this.GetStatusEffectsModifications(valueModifRoot, statusTemplate.Modifiers, entity, effect); + } + return result; +}; + +Attacking.prototype.GetStatusEffectsModifications = function(valueModifRoot, template, entity, effect) +{ + let modifiers = {}; + for (let modifier in template) + { + let modifierTemplate = template[modifier]; + modifiers[modifier] = { + "Paths": modifierTemplate.Paths, + "Affects": modifierTemplate.Affects + }; + if (modifierTemplate.Add !== undefined) + modifiers[modifier].Add = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Add", +modifierTemplate.Add, entity); + if (modifierTemplate.Multiply !== undefined) + modifiers[modifier].Multiply = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Multiply", +modifierTemplate.Multiply, entity); + if (modifierTemplate.Replace !== undefined) + modifiers[modifier].Replace = modifierTemplate.Replace; + } + return modifiers; +}; + Attacking.prototype.GetTotalAttackEffects = function(effectData, effectType, cmpResistance) { let total = 0;