Index: binaries/data/mods/public/globalscripts/StatusEffects.js =================================================================== --- binaries/data/mods/public/globalscripts/StatusEffects.js +++ binaries/data/mods/public/globalscripts/StatusEffects.js @@ -28,17 +28,32 @@ } /** - * @returns the default data for @param code status effects, augmented with the given template data, - * or simply @param templateData if the code is not found in JSON files. + * @param {string} code - The code of the Status Effect. + * @return {Object} - The JSON data corresponding to the code. */ - augment(code, templateData) + getData(code) { - if (!templateData && this.statusEffectData[code]) - return this.statusEffectData[code]; + if (!this.statusEffectData[code]) + warn("No status effects data found for: " + code + "."); - if (this.statusEffectData[code]) - return Object.assign({}, this.statusEffectData[code], templateData); + return this.statusEffectData[code]; + } + + getName(code) + { + let data = this.getData(code); + return data ? data.statusName || code : code; + } - return templateData; + getApplierTooltip(code) + { + let data = this.getData(code); + return data ? data.applierTooltip || "" : ""; + } + + getReceiverTooltip(code) + { + let data = this.getData(code); + return data ? data.receiverTooltip || "" : ""; } } 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/globalscripts/tests/test_statusEffects.js =================================================================== --- binaries/data/mods/public/globalscripts/tests/test_statusEffects.js +++ binaries/data/mods/public/globalscripts/tests/test_statusEffects.js @@ -16,16 +16,9 @@ let sem = new StatusEffectsMetadata(); -// Template data takes precedence over generic data. -TS_ASSERT_UNEVAL_EQUALS(sem.augment("test_a"), { +TS_ASSERT_UNEVAL_EQUALS(sem.getData("test_a"), { "code": "test_a", "StatusName": "A", "StatusTooltip": "TTA" }); -TS_ASSERT_UNEVAL_EQUALS(sem.augment("test_b"), { +TS_ASSERT_UNEVAL_EQUALS(sem.getData("test_b"), { "code": "test_b", "StatusName": "B", "StatusTooltip": "TTB" }); -TS_ASSERT_UNEVAL_EQUALS(sem.augment("test_a", { "StatusName": "test" }), { - "code": "test_a", "StatusName": "test", "StatusTooltip": "TTA" -}); -TS_ASSERT_UNEVAL_EQUALS(sem.augment("test_c", { "StatusName": "test" }), { - "StatusName": "test" -}); 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(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))) + }); + + if (resistanceTypeTemplate[statusEffect].blockChance == 0) + return sprintf(translate("%(name)s %(details)s"), { + "name": unitFont(translateWithContext("status effect", 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(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))), + "details": sprintf(translate("Blocks: %(blockPercentage)s%%"), { + "blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100 + }) + }); + + return sprintf(translate("%(name)s %(details)s"), { + "name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))), + "details": sprintf(translate("Blocks: %(blockPercentage)s%%, Duration reduction: %(durationReduction)s%%"), { + "blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100, + "durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100) + }) + }); + } + ).join(commaFont(translate(", "))) + }); +} + function attackRateDetails(interval, projectiles) { if (!interval) @@ -346,10 +389,9 @@ return ""; return sprintf(translate("gives %(name)s"), { - "name": Object.keys(applyStatusTemplate).map(x => { - let template = g_StatusEffectsMetadata.augment(x, applyStatusTemplate[x]); - return unitFont(translateWithContext("status effect", template.StatusName)); - }).join(commaFont(translate(", "))), + "name": Object.keys(applyStatusTemplate).map(x => + unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(x))) + ).join(commaFont(translate(", "))), }); } @@ -395,10 +437,7 @@ let statusEffectsDetails = []; if (attackTypeTemplate.ApplyStatus) for (let status in attackTypeTemplate.ApplyStatus) - { - let status_template = g_StatusEffectsMetadata.augment(status, attackTypeTemplate.ApplyStatus[status]); - statusEffectsDetails.push("\n" + g_Indent + g_Indent + getStatusEffectsTooltip(status_template, true)); - } + statusEffectsDetails.push("\n" + g_Indent + g_Indent + getStatusEffectsTooltip(status, attackTypeTemplate.ApplyStatus[status], true)); statusEffectsDetails = statusEffectsDetails.join(""); tooltips.push(sprintf(translate("%(attackLabel)s: %(effects)s, %(range)s, %(rate)s%(statusEffects)s%(splash)s"), { @@ -420,13 +459,21 @@ /** * @param applier - if true, return the tooltip for the Applier. If false, Receiver is returned. */ -function getStatusEffectsTooltip(template, applier) +function getStatusEffectsTooltip(statusCode, template, applier) { let tooltipAttributes = []; - if (applier && template.ApplierTooltip) - tooltipAttributes.push(translate(template.ApplierTooltip)); - else if (!applier && template.ReceiverTooltip) - tooltipAttributes.push(translate(template.ReceiverTooltip)); + if (applier) + { + let applierTooltip = g_StatusEffectsMetadata.getApplierTooltip(statusCode); + if (applierTooltip) + tooltipAttributes.push(translate(applierTooltip)); + } + else + { + let receiverTooltip = g_StatusEffectsMetadata.getReceiverTooltip(statusCode); + if (receiverTooltip) + tooltipAttributes.push(translate(receiverTooltip)); + } if (template.Damage || template.Capture) tooltipAttributes.push(attackEffectsDetails(template)); @@ -439,12 +486,12 @@ if (applier) return sprintf(translate("%(statusName)s: %(statusInfo)s %(stackability)s"), { - "statusName": headerFont(translateWithContext("status effect", template.StatusName)), + "statusName": headerFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusCode))), "statusInfo": tooltipAttributes.join(commaFont(translate(", "))), "stackability": getStatusEffectStackabilityTooltip(template) }); return sprintf(translate("%(statusName)s: %(statusInfo)s"), { - "statusName": headerFont(translateWithContext("status effect", template.StatusName)), + "statusName": headerFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusCode))), "statusInfo": tooltipAttributes.join(commaFont(translate(", "))) }); } Index: binaries/data/mods/public/gui/session/selection_details.js =================================================================== --- binaries/data/mods/public/gui/session/selection_details.js +++ binaries/data/mods/public/gui/session/selection_details.js @@ -98,12 +98,12 @@ statusEffectsSection.hidden = false; let statusIcons = statusEffectsSection.children; let i = 0; - for (let effectName in entState.statusEffects) + for (let effectCode in entState.statusEffects) { - let effect = entState.statusEffects[effectName]; + let effect = entState.statusEffects[effectCode]; statusIcons[i].hidden = false; statusIcons[i].sprite = "stretched:session/icons/status_effects/" + (effect.Icon || "default") + ".png"; - statusIcons[i].tooltip = getStatusEffectsTooltip(effect, false); + statusIcons[i].tooltip = getStatusEffectsTooltip(effect.baseCode, effect, false); let size = statusIcons[i].size; size.top = i * 18; size.bottom = i * 18 + 16; Index: binaries/data/mods/public/l10n/messages.json =================================================================== --- binaries/data/mods/public/l10n/messages.json +++ binaries/data/mods/public/l10n/messages.json @@ -489,15 +489,6 @@ }, "options": { "keywords": { - "StatusName": { - "customContext": "status effect" - }, - "ApplierTooltip": { - "customContext": "status effect" - }, - "ReceiverTooltip": { - "customContext": "status effect" - }, "GenericName": {}, "SpecificName": {}, "History": {}, @@ -534,9 +525,9 @@ ], "options": { "keywords": [ - "StatusName", - "ApplierTooltip", - "ReceiverTooltip" + "statusName", + "applierTooltip", + "receiverTooltip" ], "context": "status effect" } 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/StatusEffectsReceiver.js =================================================================== --- binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js +++ binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js @@ -28,7 +28,7 @@ * @param {number} attackerOwner - The player ID of the attacker. * @param {number} bonusMultiplier - A value to multiply the damage with (not implemented yet for SE). * - * @return {Object} - The names of the status effects which were processed. + * @return {Object} - The codes of the status effects which were processed. */ StatusEffectsReceiver.prototype.ApplyStatus = function(effectData, attacker, attackerOwner) { @@ -36,7 +36,7 @@ for (let effect in effectData) this.AddStatus(effect, effectData[effect], attackerData); - // TODO: implement loot / resistance. + // TODO: implement loot? return { "inflictedStatuses": Object.keys(effectData) }; }; @@ -44,43 +44,46 @@ /** * Adds a status effect to the entity. * - * @param {string} statusName - The name of the status effect. + * @param {string} statusCode - The code 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) +StatusEffectsReceiver.prototype.AddStatus = function(baseCode, data, attackerData) { - if (this.activeStatusEffects[statusName]) + let statusCode = baseCode; + if (this.activeStatusEffects[statusCode]) { if (data.Stackability == "Ignore") return; if (data.Stackability == "Extend") { - this.activeStatusEffects[statusName].Duration += data.Duration; + this.activeStatusEffects[statusCode].Duration += data.Duration; return; } if (data.Stackability == "Replace") - this.RemoveStatus(statusName); + this.RemoveStatus(statusCode); else if (data.Stackability == "Stack") { let i = 0; let temp; do - temp = statusName + "_" + i++; + temp = statusCode + "_" + i++; while (!!this.activeStatusEffects[temp]); - statusName = temp; + statusCode = temp; } } - this.activeStatusEffects[statusName] = {}; - let status = this.activeStatusEffects[statusName]; + this.activeStatusEffects[statusCode] = { + "baseCode": baseCode + }; + let status = this.activeStatusEffects[statusCode]; Object.assign(status, data); if (status.Modifiers) { let modifications = DeriveModificationsFromXMLTemplate(status.Modifiers); let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); - cmpModifiersManager.AddModifiers(statusName, modifications, this.entity); + cmpModifiersManager.AddModifiers(statusCode, modifications, this.entity); } // With neither an interval nor a duration, there is no point in starting a timer. @@ -101,24 +104,24 @@ status.source = attackerData; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - status._timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +(status.Interval || status._interval), statusName); + status._timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +(status.Interval || status._interval), statusCode); }; /** * Removes a status effect from the entity. * - * @param {string} statusName - The status effect to be removed. + * @param {string} statusCode - The status effect to be removed. */ -StatusEffectsReceiver.prototype.RemoveStatus = function(statusName) +StatusEffectsReceiver.prototype.RemoveStatus = function(statusCode) { - let statusEffect = this.activeStatusEffects[statusName]; + let statusEffect = this.activeStatusEffects[statusCode]; if (!statusEffect) return; if (statusEffect.Modifiers) { let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); - cmpModifiersManager.RemoveAllModifiers(statusName, this.entity); + cmpModifiersManager.RemoveAllModifiers(statusCode, this.entity); } if (statusEffect._timer) @@ -126,23 +129,23 @@ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(statusEffect._timer); } - delete this.activeStatusEffects[statusName]; + delete this.activeStatusEffects[statusCode]; }; /** * Called by the timers. Executes a status effect. * - * @param {string} statusName - The name of the status effect to be executed. + * @param {string} statusCode - The status effect to be executed. * @param {number} lateness - The delay between the calling of the function and the actual execution (turn time?). */ -StatusEffectsReceiver.prototype.ExecuteEffect = function(statusName, lateness) +StatusEffectsReceiver.prototype.ExecuteEffect = function(statusCode, lateness) { - let status = this.activeStatusEffects[statusName]; + let status = this.activeStatusEffects[statusCode]; if (!status) return; if (status.Damage || status.Capture) - Attacking.HandleAttackEffects(this.entity, statusName, status, status.source.entity, status.source.owner); + Attacking.HandleAttackEffects(this.entity, statusCode, status, status.source.entity, status.source.owner); if (!status.Duration) return; @@ -156,7 +159,7 @@ status._timeElapsed += +(status.Interval || status._interval) + lateness; if (status._timeElapsed >= +status.Duration) - this.RemoveStatus(statusName); + this.RemoveStatus(statusCode); }; Engine.RegisterComponentType(IID_StatusEffectsReceiver, "StatusEffectsReceiver", StatusEffectsReceiver); 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,9 +27,10 @@ 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_Identity); + DeleteMock(this.EntityID, IID_StatusEffectsReceiver); } TestInvulnerability() @@ -148,6 +150,103 @@ 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) }; + } + }); + spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus"); + + Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID); + TS_ASSERT_EQUALS(spy._called, 1); + + // 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 +345,6 @@ cmp.TestBonus(); cmp.TestDamageResistanceApplies(); cmp.TestCaptureResistanceApplies(); +cmp.TestStatusEffectsResistancesApplies(); cmp.TestResistanceAndBonus(); cmp.TestMultipleEffects(); Index: binaries/data/mods/public/simulation/data/template_helpers/status_effects/anotherBogus.json =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/data/template_helpers/status_effects/anotherBogus.json @@ -0,0 +1,5 @@ +{ + "code": "AnotherBogus", + "statusName": "StatusEffect's name 3", + "applierTooltip": "Some bogus bonus tooltip describing the effect of the modifiers. This increases the armour permanently." +} Index: binaries/data/mods/public/simulation/data/template_helpers/status_effects/bogus.json =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/data/template_helpers/status_effects/bogus.json @@ -0,0 +1,5 @@ +{ + "code": "Bogus", + "statusName": "StatusEffect's name 1", + "applierTooltip": "Some bogus bonus tooltip describing the effect of the modifiers. This speeds up the entity but halves the health and resource gathering speed." +} Index: binaries/data/mods/public/simulation/data/template_helpers/status_effects/bogus2.json =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/data/template_helpers/status_effects/bogus2.json @@ -0,0 +1,5 @@ +{ + "code": "Bogus2", + "statusName": "StatusEffect's name 2", + "receiverTooltip": "You've been poissoned!" +} 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 @@ -23,18 +23,9 @@ "" + "" + "" + - "" + - "" + - "" + "" + "" + "" + - "" + - "" + - "" + - "" + - "" + - "" + "" + "" + "" + @@ -197,7 +188,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,46 @@ 1.5 0 + + + Extend + 15000 + + + UnitMotion/WalkSpeed + Unit + 20 + + + Health/Max ResourceGatherer/BaseSpeed + Unit Structure + 0.5 + + + + + Replace + 10000 + 1000 + + 1 + + + + Stack + 1000 + + 1 + + + + Armour/Hack Armour/Pierce Armour/Crush + Unit + 20 + + + + 10.0 0.0 750