Index: binaries/data/mods/public/globalscripts/StatusEffects.js =================================================================== --- binaries/data/mods/public/globalscripts/StatusEffects.js +++ binaries/data/mods/public/globalscripts/StatusEffects.js @@ -41,4 +41,10 @@ return templateData; } + + getName(code) + { + let knownData = this.augment(code); + return knownData ? knownData.name || code : code; + } } Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -174,8 +174,15 @@ } if (template.Resistance.Entity.Capture) ret.resistance.Capture = getEntityValue("Resistance/Entity/Capture"); - - // ToDo: Resistance against StatusEffects. + if (template.Resistance.Entity.ApplyStatus) + { + ret.resistance.ApplyStatus = {}; + for (let statusEffect in template.Resistance.Entity.ApplyStatus) + ret.resistance.ApplyStatus[statusEffect] = { + "blockChance": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/BlockChance"), + "duration": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/Duration") + }; + } } } Index: binaries/data/mods/public/gui/common/tooltips.js =================================================================== --- binaries/data/mods/public/gui/common/tooltips.js +++ binaries/data/mods/public/gui/common/tooltips.js @@ -181,7 +181,8 @@ if (template.resistance.Capture) details.push(getCaptureResistanceTooltip(template.resistance.Capture)); - // TODO: Status effects resistance. + if (template.resistance.ApplyStatus) + details.push(getStatusEffectsResistanceTooltip(template.resistance.ApplyStatus)); return sprintf(translate("%(label)s\n%(details)s"), { "label": headerFont(translate("Resistance:")), @@ -230,6 +231,48 @@ }); } +function getStatusEffectsResistanceTooltip(resistanceTypeTemplate) +{ + if (!resistanceTypeTemplate) + return ""; + return sprintf(translate("%(label)s %(details)s"), { + "label": headerFont(translate("Status Effects:")), + "details": + Object.keys(resistanceTypeTemplate).map( + statusEffect => { + if (resistanceTypeTemplate[statusEffect].blockChance == 1) + return sprintf(translate("Blocks %(name)s"), { + "name": unitFont(translate(g_StatusEffectsMetadata.getName(statusEffect))) + }); + + if (resistanceTypeTemplate[statusEffect].blockChance == 0) + return sprintf(translate("%(name)s %(details)s"), { + "name": unitFont(translate(g_StatusEffectsMetadata.getName(statusEffect))), + "details": sprintf(translate("Duration reduction: %(durationReduction)s%%"), { + "durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100) + }) + }); + + if (resistanceTypeTemplate[statusEffect].duration == 1) + return sprintf(translate("%(name)s %(details)s"), { + "name": unitFont(translate(g_StatusEffectsMetadata.getName(statusEffect))), + "details": sprintf(translate("Block chance: %(blockChance)s%%"), { + "blockChance": resistanceTypeTemplate[statusEffect].blockChance * 100 + }) + }); + + return sprintf(translate("%(name)s %(details)s"), { + "name": unitFont(translate(g_StatusEffectsMetadata.getName(statusEffect))), + "details": sprintf(translate("Block chance: %(blockChance)s%%, Duration reduction: %(durationReduction)s%%"), { + "blockChance": resistanceTypeTemplate[statusEffect].blockChance * 100, + "durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100) + }) + }); + } + ).join(commaFont(translate(", "))) + }); +} + function attackRateDetails(interval, projectiles) { if (!interval) Index: binaries/data/mods/public/simulation/components/Resistance.js =================================================================== --- binaries/data/mods/public/simulation/components/Resistance.js +++ binaries/data/mods/public/simulation/components/Resistance.js @@ -2,7 +2,6 @@ /** * Builds a RelaxRNG schema of possible attack effects. - * ToDo: Resistance to StatusEffects. * * @return {string} - RelaxNG schema string. */ @@ -22,6 +21,23 @@ "" + "" + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + ""; }; @@ -122,6 +138,16 @@ if (template.Capture) ret.Capture = ApplyValueModificationsToEntity("Resistance/" + entityForm + "/Capture", +this.template[entityForm].Capture, this.entity); + if (template.ApplyStatus) + { + ret.ApplyStatus = {}; + for (let effect in template.ApplyStatus) + ret.ApplyStatus[effect] = { + "duration": ApplyValueModificationsToEntity("Resistance/" + entityForm + "/ApplyStatus/" + effect + "/Duration", +(template.ApplyStatus[effect].Duration || 1), this.entity), + "blockChance": ApplyValueModificationsToEntity("Resistance/" + entityForm + "/ApplyStatus/" + effect + "/BlockChance", +(template.ApplyStatus[effect].BlockChance || 0), this.entity) + }; + } + return ret; }; Index: binaries/data/mods/public/simulation/components/tests/test_Resistance.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Resistance.js +++ binaries/data/mods/public/simulation/components/tests/test_Resistance.js @@ -10,6 +10,7 @@ Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); +Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("Resistance.js"); class testResistance @@ -26,8 +27,9 @@ Reset(schema = {}) { this.cmpResistance = ConstructComponent(this.EntityID, "Resistance", schema); - DeleteMock(this.EntityID, IID_Health); DeleteMock(this.EntityID, IID_Capturable); + DeleteMock(this.EntityID, IID_Health); + DeleteMock(this.EntityID, IID_StatusEffectsReceiver); DeleteMock(this.EntityID, IID_Identity); } @@ -148,6 +150,101 @@ TS_ASSERT_EQUALS(spy._called, 1); } + TestStatusEffectsResistancesApplies() + { + // Test duration reduction. + let durationFactor = 0.5; + let statusName = "statusName"; + this.Reset({ + "Entity": { + "ApplyStatus": { + [statusName]: { + "Duration": durationFactor + } + } + } + }); + + let duration = 10; + let attackData = { + "ApplyStatus": { + [statusName]: { + "Duration": duration + } + } + }; + + let cmpStatusEffectsReceiver = AddMock(this.EntityID, IID_StatusEffectsReceiver, { + "ApplyStatus": (effectData, __, ___) => { + TS_ASSERT_EQUALS(effectData[statusName].Duration, duration * durationFactor); + return { "inflictedStatuses": Object.keys(effectData) }; + } + }); + let spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus"); + + Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID); + TS_ASSERT_EQUALS(spy._called, 1); + + // Test blocking. + this.Reset({ + "Entity": { + "ApplyStatus": { + [statusName]: { + "BlockChance": "1" + } + } + } + }); + + cmpStatusEffectsReceiver = AddMock(this.EntityID, IID_StatusEffectsReceiver, { + "ApplyStatus": (effectData, __, ___) => { + TS_ASSERT_UNEVAL_EQUALS(effectData, {}); + return { "inflictedStatuses": Object.keys(effectData) }; + } + }); + + Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID); + + // Test multiple resistances. + let reducedStatusName = "reducedStatus"; + let blockedStatusName = "blockedStatus"; + this.Reset({ + "Entity": { + "ApplyStatus": { + [reducedStatusName]: { + "Duration": durationFactor + }, + [blockedStatusName]: { + "BlockChance": "1" + } + } + } + }); + + attackData = { + "ApplyStatus": { + [reducedStatusName]: { + "Duration": duration + }, + [blockedStatusName]: { + "Duration": duration + } + } + }; + + cmpStatusEffectsReceiver = AddMock(this.EntityID, IID_StatusEffectsReceiver, { + "ApplyStatus": (effectData, __, ___) => { + TS_ASSERT_EQUALS(effectData[reducedStatusName].Duration, duration * durationFactor); + TS_ASSERT_UNEVAL_EQUALS(Object.keys(effectData), [reducedStatusName]); + return { "inflictedStatuses": Object.keys(effectData) }; + } + }); + spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus"); + + Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID); + TS_ASSERT_EQUALS(spy._called, 1); + } + TestResistanceAndBonus() { let resistanceValue = 2; @@ -246,5 +343,6 @@ cmp.TestBonus(); cmp.TestDamageResistanceApplies(); cmp.TestCaptureResistanceApplies(); +cmp.TestStatusEffectsResistancesApplies(); cmp.TestResistanceAndBonus(); cmp.TestMultipleEffects(); Index: binaries/data/mods/public/simulation/helpers/Attacking.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Attacking.js +++ binaries/data/mods/public/simulation/helpers/Attacking.js @@ -197,7 +197,29 @@ total /= 0.1 + 0.9 * cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints(); } else if (effectType == "ApplyStatus") - return effectData[effectType]; + { + if (!resistanceStrengths.ApplyStatus) + return effectData[effectType]; + + let result = {}; + for (let statusEffect in effectData[effectType]) + { + if (!resistanceStrengths.ApplyStatus[statusEffect]) + { + result[statusEffect] = effectData[effectType][statusEffect]; + continue; + } + + if (randBool(resistanceStrengths.ApplyStatus[statusEffect].blockChance)) + continue; + + result[statusEffect] = effectData[effectType][statusEffect]; + + if (effectData[effectType][statusEffect].Duration) + result[statusEffect].Duration = effectData[effectType][statusEffect].Duration * resistanceStrengths.ApplyStatus[statusEffect].duration; + } + return result; + } return total * bonusMultiplier; }; Index: binaries/data/mods/public/simulation/templates/template_structure.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure.xml +++ binaries/data/mods/public/simulation/templates/template_structure.xml @@ -115,6 +115,12 @@ 1 1 + + + 0.5 + 1 + + Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml @@ -7,6 +7,52 @@ 1.5 0 + + + StatusEffect's name 1 + Extend + Some bogus bonus tooltip describing the effect of the modifiers. This speeds up the entity but halves the health and resource gathering speed. + 15000 + + + UnitMotion/WalkSpeed + Unit + 20 + + + Health/Max ResourceGatherer/BaseSpeed + Unit Structure + 0.5 + + + + + StatusEffect's name 2 + You've been poissoned! + Replace + 10000 + 1000 + + 1 + + + + StatusEffect's name 3 + Stack + Some bogus bonus tooltip describing the effect of the modifiers. This increases the armour permanently. + 1000 + + 1 + + + + Armour/Hack Armour/Pierce Armour/Crush + Unit + 20 + + + + 10.0 0.0 750