Index: binaries/data/mods/public/globalscripts/DamageTypes.js =================================================================== --- binaries/data/mods/public/globalscripts/DamageTypes.js +++ binaries/data/mods/public/globalscripts/DamageTypes.js @@ -54,4 +54,19 @@ { return this.damageTypeData[code] ? this.damageTypeData[code].name : code; } + + /** + * @returns whether to show the @param code damage type in the tooltip. + * The possible choices are "always", "nonzero", "never". + */ + showInTooltip(code, value) + { + if (!this.damageTypeData[code]) + return true; + if (this.damageTypeData[code] === "always") + return true; + if (this.damageTypeData[code] === "never") + return false; + return value !== 0; + } } Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -169,7 +169,7 @@ if (template.Resistance.Entity.Damage) { ret.resistance.Damage = {}; - for (let damageType in template.Resistance.Entity.Damage) + for (const damageType in template.Resistance.Entity.Damage) ret.resistance.Damage[damageType] = getEntityValue("Resistance/Entity/Damage/" + damageType); } if (template.Resistance.Entity.Capture) @@ -177,10 +177,10 @@ if (template.Resistance.Entity.ApplyStatus) { ret.resistance.ApplyStatus = {}; - for (let statusEffect in template.Resistance.Entity.ApplyStatus) + for (const statusEffect in template.Resistance.Entity.ApplyStatus) ret.resistance.ApplyStatus[statusEffect] = { - "blockChance": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/BlockChance"), - "duration": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/Duration") + "blockChance": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/BlockChance") || 0, + "duration": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/Duration") || 1 }; } } 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 @@ -205,7 +205,11 @@ details.push(getCaptureResistanceTooltip(template.resistance.Capture)); if (template.resistance.ApplyStatus) - details.push(getStatusEffectsResistanceTooltip(template.resistance.ApplyStatus)); + { + const tp = getStatusEffectsResistanceTooltip(template.resistance.ApplyStatus); + if (tp) + details.push(tp); + } return details.length ? sprintf(translate("%(label)s\n%(details)s"), { "label": headerFont(translate("Resistance:")), @@ -217,11 +221,12 @@ { if (!resistanceTypeTemplate) return ""; - + const keys = Object.keys(resistanceTypeTemplate) + .filter(dmgType => g_DamageTypesMetadata.showInTooltip(dmgType, resistanceTypeTemplate[dmgType])); return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Damage:")), "details": - g_DamageTypesMetadata.sort(Object.keys(resistanceTypeTemplate)).map( + g_DamageTypesMetadata.sort(keys).map( dmgType => sprintf(translate("%(damage)s %(damageType)s %(resistancePercentage)s"), { "damage": resistanceTypeTemplate[dmgType].toFixed(1), "damageType": unitFont(translateWithContext("damage type", g_DamageTypesMetadata.getName(dmgType))), @@ -258,44 +263,79 @@ { if (!resistanceTypeTemplate) return ""; + const keys = Object.keys(resistanceTypeTemplate) + .filter(statusEffect => resistanceTypeTemplate[statusEffect].blockChance > 0 || + resistanceTypeTemplate[statusEffect].duration !== 1); + if (!keys.length) + 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 - }) - }); + "details": keys.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("Blocks: %(blockPercentage)s%%, Duration reduction: %(durationReduction)s%%"), { - "blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100, + "details": sprintf(translate("Duration reduction: %(durationReduction)s%%"), { "durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100) }) }); - } - ).join(commaFont(translate(", "))) + + 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 getVulnerabilityTooltip(template) +{ + if (!template.resistance) + return ""; + + const details = []; + for (const type in template.resistance) + { + if (type === "Capture") + details.push(headerFont(translate("Capture"))); + else + { + const context = type === "Damage" ? "damage type" : "status effect"; + const metadata = type === "Damage" ? g_DamageTypesMetadata : g_StatusEffectsMetadata; + const typeName = type === "Damage" ? translate("Damage:") : translate("Status Effects:"); + const subTypes = Object.keys(template.resistance[type]) + .map(subtype => unitFont(translateWithContext(context, metadata.getName(subtype)))); + details.push(sprintf(translate("%(type)s %(subtypes)s"), + { + "type": headerFont(typeName), + "subtypes": subTypes.join(commaFont(translate(", "))) + }) + ); + } + } + return details.length ? sprintf(translate("%(label)s\n%(details)s"), { + "label": headerFont(translate("Vulnerability:")), + "details": g_Indent + details.join(translate(", ")) + }) : ""; +} + function attackRateDetails(interval, projectiles) { if (!interval) Index: binaries/data/mods/public/gui/reference/common/ReferencePage.js =================================================================== --- binaries/data/mods/public/gui/reference/common/ReferencePage.js +++ binaries/data/mods/public/gui/reference/common/ReferencePage.js @@ -56,6 +56,7 @@ getAttackTooltip, getHealerTooltip, getResistanceTooltip, + getVulnerabilityTooltip, getGarrisonTooltip, getTurretsTooltip, getProjectilesTooltip, Index: binaries/data/mods/public/gui/reference/viewer/viewer.xml =================================================================== --- binaries/data/mods/public/gui/reference/viewer/viewer.xml +++ binaries/data/mods/public/gui/reference/viewer/viewer.xml @@ -11,7 +11,7 @@ - + Information Index: binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- binaries/data/mods/public/simulation/ai/common-api/entity.js +++ binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -780,26 +780,49 @@ }, /** - * Derived from Attack.js' similary named function. + * See Attack.js + */ + "isVulnerableTo": function(resistance, attackData) + { + for (const type in resistance) + { + if (attackData[type] === undefined) + continue; + if (type === "Capture") + return true; + // Check subtypes, but don't check for particular values. + for (const subType in resistance[type]) + if (attackData[type][subType] !== undefined) + return true; + } + return false; + }, + + /** + * Derived from Attack.js' similarly named function. * @return {boolean} - Whether an entity can attack a given target. */ "canAttackTarget": function(target, allowCapture) { - let attackTypes = this.get("Attack"); + const attackTypes = this.get("Attack"); if (!attackTypes) return false; - let canCapture = allowCapture && this.canCapture(target); - let health = target.get("Health"); + const canCapture = allowCapture && this.canCapture(target); + const health = target.get("Health"); if (!health) return canCapture; - for (let type in attackTypes) + const targetResistance = target.resistanceStrengths(); + for (const type in attackTypes) { - if (type == "Capture" ? !canCapture : target.isInvulnerable()) + if (type === "Capture" ? !canCapture : target.isInvulnerable()) continue; - let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); + if (!this.isVulnerableTo(targetResistance, attackTypes[type])) + continue; + + const restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses)) return true; } Index: binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/Attack.js +++ binaries/data/mods/public/simulation/components/Attack.js @@ -231,8 +231,8 @@ if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) return false; - const cmpResistance = QueryMiragedInterface(target, IID_Resistance); - if (!cmpResistance) + const cmpTargetResistance = QueryMiragedInterface(target, IID_Resistance); + if (!cmpTargetResistance) return false; const cmpIdentity = QueryMiragedInterface(target, IID_Identity); @@ -265,6 +265,9 @@ if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints())) continue; + if (!cmpTargetResistance.IsVulnerableTo(this.template[type])) + continue; + if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) continue; 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 @@ -117,9 +117,9 @@ */ Resistance.prototype.GetEffectiveResistanceAgainst = function(effectType) { - let ret = {}; + const ret = {}; - let template = this.GetResistanceOfForm(Engine.QueryInterface(this.entity, IID_Foundation) ? "Foundation" : "Entity"); + const template = this.GetResistanceOfForm(Engine.QueryInterface(this.entity, IID_Foundation) ? "Foundation" : "Entity"); if (template[effectType]) ret[effectType] = template[effectType]; @@ -146,15 +146,15 @@ */ Resistance.prototype.GetResistanceOfForm = function(entityForm) { - let ret = {}; - let template = this.template && this.template[entityForm]; + const ret = {}; + const template = this.template && this.template[entityForm]; if (!template) return ret; if (template.Damage) { ret.Damage = {}; - for (let damageType in template.Damage) + for (const damageType in template.Damage) ret.Damage[damageType] = ApplyValueModificationsToEntity("Resistance/" + entityForm + "/Damage/" + damageType, +this.template[entityForm].Damage[damageType], this.entity); } @@ -164,16 +164,47 @@ if (template.ApplyStatus) { ret.ApplyStatus = {}; - for (let effect in template.ApplyStatus) + for (const 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) + "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; }; +/** + * This is a fast resistance-check, validating that the attackData + * may potentially damage us, but not actually computing any numbers. + * (private function to avoid duplication in the mirage) + * @param resistance - the resistance form data + * @param AttackData - an attack schema object. + */ +var isVulnerableTo = function(resistance, attackData) +{ + for (const type in resistance) + { + if (attackData[type] === undefined) + continue; + if (type === "Capture") + return true; + // Check subtypes, but don't check for particular values. + for (const subType in resistance[type]) + if (attackData[type][subType] !== undefined) + return true; + } + return false; +}; + +Resistance.prototype.IsVulnerableTo = function(attackData) +{ + return isVulnerableTo( + this.template[Engine.QueryInterface(this.entity, IID_Foundation) ? "Foundation" : "Entity"], + attackData + ); +}; + Resistance.prototype.OnOwnershipChanged = function(msg) { if (msg.to === INVALID_PLAYER) @@ -209,6 +240,14 @@ return this.resistanceOfForm; }; +ResistanceMirage.prototype.IsVulnerableTo = function(attackData) +{ + return isVulnerableTo( + this.resistanceOfForm[this.isFoundation ? "Foundation" : "Entity"], + attackData + ); +}; + Engine.RegisterGlobal("ResistanceMirage", ResistanceMirage); Resistance.prototype.Mirage = function() Index: binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -175,6 +175,8 @@ }); AddMock(defender, IID_Resistance, { + "GetEffectiveResistanceAgainst": (x) => ({ [x]: 0 }), + "IsVulnerableTo": () => true }); test_function(attacker, cmpAttack, defender); Index: binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -106,6 +106,11 @@ }, }); + AddMock(target, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Crush": 0 } }), + "IsInvulnerable": () => false, + }); + AddMock(SYSTEM_ENTITY, IID_DelayedDamage, { "Hit": () => { damageTaken = true; @@ -232,6 +237,21 @@ "GetPosition2D": () => new Vector2D(5, 2), }); + AddMock(60, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Hack": 0 } }), + "IsInvulnerable": () => false, + }); + + AddMock(61, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Hack": 0 } }), + "IsInvulnerable": () => false, + }); + + AddMock(62, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Hack": 0 } }), + "IsInvulnerable": () => false, + }); + AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(60); @@ -345,6 +365,13 @@ "GetPosition2D": () => new Vector2D(23, 4), }); + for (let i = 60; i <= 65; ++i) + AddMock(i, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Hack": 0 } }), + "IsInvulnerable": () => false, + }); + + AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 100 * fallOff(0)); @@ -464,6 +491,11 @@ "GetShape": () => ({ "type": "circle", "radius": 20 }), }); + AddMock(60, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0 } }), + "IsInvulnerable": () => false, + }); + AddMock(70, IID_Ownership, { "GetOwner": () => 1, }); @@ -510,6 +542,11 @@ "GetShape": () => ({ "type": "circle", "radius": 20 }) }); + AddMock(61, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0 } }), + "IsInvulnerable": () => false, + }); + cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); hitEnts.clear(); @@ -570,6 +607,11 @@ } }); + AddMock(61, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0, "Crush": 0 } }), + "IsInvulnerable": () => false, + }); + AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), @@ -589,6 +631,11 @@ "GetShape": () => ({ "type": "circle", "radius": 20 }), }); + AddMock(62, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0, "Crush": 0 } }), + "IsInvulnerable": () => false, + }); + cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); @@ -677,6 +724,11 @@ } }); + AddMock(61, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0, "Crush": 0 } }), + "IsInvulnerable": () => false, + }); + AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), @@ -696,6 +748,11 @@ "GetShape": () => ({ "type": "circle", "radius": 20 }), }); + AddMock(62, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ "Damage": { "Pierce": 0, "Crush": 0 } }), + "IsInvulnerable": () => false, + }); + cmpDelayedDamage.Hit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); 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 @@ -58,22 +58,22 @@ TestInvulnerability() { - this.Reset(); + this.Reset({ "Entity": { "Damage": { "Name": 0 } } }); - let damage = 5; - let attackData = { "Damage": { "Name": damage } }; - let attackType = "Test"; + const damage = 5; + const attackData = { "Damage": { "Name": damage } }; + const attackType = "Test"; TS_ASSERT(!this.cmpResistance.IsInvulnerable()); - let cmpHealth = AddMock(this.EntityID, IID_Health, { + const cmpHealth = AddMock(this.EntityID, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage); return { "healthChange": -amount }; } }); - let spy = new Spy(cmpHealth, "TakeDamage"); - let data = { + const spy = new Spy(cmpHealth, "TakeDamage"); + const data = { "type": attackType, "attackData": attackData, "attacker": this.AttackerID, @@ -90,14 +90,78 @@ TS_ASSERT_EQUALS(spy._called, 1); } + TestIsVulnerableTo() + { + const attackData = { + "Damage": { + "A": 1, + "B": 0, + }, + "Capture": 2, + "ApplyStatus": { + "A": {}, + "B": {} + } + }; + // Nothing -> not vulnerable + this.Reset({ "Entity": {} }); + TS_ASSERT_EQUALS(!this.cmpResistance.IsVulnerableTo(attackData)); + // Taking damage to A -> vulnerable + this.Reset({ "Entity": { "Damage": { "A": 0 } } }); + TS_ASSERT_EQUALS(this.cmpResistance.IsVulnerableTo(attackData)); + // Taking damage to B -> vulnerable (no value check) + this.Reset({ "Entity": { "Damage": { "B": 100 } } }); + TS_ASSERT_EQUALS(this.cmpResistance.IsVulnerableTo(attackData)); + // Taking damage to C -> not vulnerable + this.Reset({ "Entity": { "Damage": { "C": 100 } } }); + TS_ASSERT_EQUALS(!this.cmpResistance.IsVulnerableTo(attackData)); + // Taking damage to capure -> vulnerable + this.Reset({ "Entity": { "Capture": 3 } }); + TS_ASSERT_EQUALS(this.cmpResistance.IsVulnerableTo(attackData)); + // Status check - again, no value check + this.Reset({ "Entity": { "ApplyStatus": { "A": {} } } }); + TS_ASSERT_EQUALS(this.cmpResistance.IsVulnerableTo(attackData)); + // Status check - again, no value check + this.Reset({ "Entity": { "ApplyStatus": { "C": {} } } }); + TS_ASSERT_EQUALS(!this.cmpResistance.IsVulnerableTo(attackData)); + } + + TestInvulnerabilityThroughUnspecifiedResistance() + { + this.Reset({ "Entity": { "Damage": {} } }); + + const damage = 5; + const attackData = { "Damage": { "Name": damage } }; + const attackType = "Test"; + + TS_ASSERT(!this.cmpResistance.IsInvulnerable()); + + const cmpHealth = AddMock(this.EntityID, IID_Health, { + "TakeDamage": (amount, __, ___) => { + TS_ASSERT_EQUALS(amount, 0); + return { "healthChange": -amount }; + } + }); + const spy = new Spy(cmpHealth, "TakeDamage"); + const data = { + "type": attackType, + "attackData": attackData, + "attacker": this.AttackerID, + "attackerOwner": this.EnemyID + }; + + AttackHelper.HandleAttackEffects(this.EntityID, data); + TS_ASSERT_EQUALS(spy._called, 1); + } + TestBonus() { - this.Reset(); + this.Reset({ "Entity": { "Damage": { "Name": 0 } } }); - let damage = 5; - let bonus = 2; - let classes = "Entity"; - let attackData = { + const damage = 5; + const bonus = 2; + const classes = "Entity"; + const attackData = { "Damage": { "Name": damage }, "Bonuses": { "bonus": { @@ -112,13 +176,13 @@ "GetCiv": () => "civ" }); - let cmpHealth = AddMock(this.EntityID, IID_Health, { + const cmpHealth = AddMock(this.EntityID, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * bonus); return { "healthChange": -amount }; } }); - let spy = new Spy(cmpHealth, "TakeDamage"); + const spy = new Spy(cmpHealth, "TakeDamage"); AttackHelper.HandleAttackEffects(this.EntityID, { "type": "Test", @@ -131,8 +195,8 @@ TestDamageResistanceApplies() { - let resistanceValue = 2; - let damageType = "Name"; + const resistanceValue = 2; + const damageType = "Name"; this.Reset({ "Entity": { "Damage": { @@ -141,18 +205,18 @@ } }); - let damage = 5; - let attackData = { + const damage = 5; + const attackData = { "Damage": { "Name": damage } }; - let cmpHealth = AddMock(this.EntityID, IID_Health, { + const cmpHealth = AddMock(this.EntityID, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * Math.pow(0.9, resistanceValue)); return { "healthChange": -amount }; } }); - let spy = new Spy(cmpHealth, "TakeDamage"); + const spy = new Spy(cmpHealth, "TakeDamage"); AttackHelper.HandleAttackEffects(this.EntityID, { "type": "Test", @@ -165,25 +229,25 @@ TestCaptureResistanceApplies() { - let resistanceValue = 2; + const resistanceValue = 2; this.Reset({ "Entity": { "Capture": resistanceValue } }); - let damage = 5; - let attackData = { + const damage = 5; + const attackData = { "Capture": damage }; - let cmpCapturable = AddMock(this.EntityID, IID_Capturable, { + const cmpCapturable = AddMock(this.EntityID, IID_Capturable, { "Capture": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * Math.pow(0.9, resistanceValue)); return { "captureChange": amount }; } }); - let spy = new Spy(cmpCapturable, "Capture"); + const spy = new Spy(cmpCapturable, "Capture"); AttackHelper.HandleAttackEffects(this.EntityID, { "type": "Test", @@ -197,8 +261,8 @@ TestStatusEffectsResistancesApplies() { // Test duration reduction. - let durationFactor = 0.5; - let statusName = "statusName"; + const durationFactor = 0.5; + const statusName = "statusName"; this.Reset({ "Entity": { "ApplyStatus": { @@ -209,7 +273,7 @@ } }); - let duration = 10; + const duration = 10; let attackData = { "ApplyStatus": { [statusName]: { @@ -262,8 +326,8 @@ TS_ASSERT_EQUALS(spy._called, 1); // Test multiple resistances. - let reducedStatusName = "reducedStatus"; - let blockedStatusName = "blockedStatus"; + const reducedStatusName = "reducedStatus"; + const blockedStatusName = "blockedStatus"; this.Reset({ "Entity": { "ApplyStatus": { @@ -308,8 +372,8 @@ TestResistanceAndBonus() { - let resistanceValue = 2; - let damageType = "Name"; + const resistanceValue = 2; + const damageType = "Name"; this.Reset({ "Entity": { "Damage": { @@ -318,10 +382,10 @@ } }); - let damage = 5; - let bonus = 2; - let classes = "Entity"; - let attackData = { + const damage = 5; + const bonus = 2; + const classes = "Entity"; + const attackData = { "Damage": { "Name": damage }, "Bonuses": { "bonus": { @@ -336,13 +400,13 @@ "GetCiv": () => "civ" }); - let cmpHealth = AddMock(this.EntityID, IID_Health, { + const cmpHealth = AddMock(this.EntityID, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * bonus * Math.pow(0.9, resistanceValue)); return { "healthChange": -amount }; } }); - let spy = new Spy(cmpHealth, "TakeDamage"); + const spy = new Spy(cmpHealth, "TakeDamage"); AttackHelper.HandleAttackEffects(this.EntityID, { "type": "Test", @@ -355,17 +419,18 @@ TestMultipleEffects() { - let captureResistanceValue = 2; + const captureResistanceValue = 2; this.Reset({ "Entity": { + "Damage": { "Name": 0 }, "Capture": captureResistanceValue } }); - let damage = 5; - let bonus = 2; - let classes = "Entity"; - let attackData = { + const damage = 5; + const bonus = 2; + const classes = "Entity"; + const attackData = { "Damage": { "Name": damage }, "Capture": damage, "Bonuses": { @@ -381,13 +446,13 @@ "GetCiv": () => "civ" }); - let cmpCapturable = AddMock(this.EntityID, IID_Capturable, { + const cmpCapturable = AddMock(this.EntityID, IID_Capturable, { "Capture": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * bonus * Math.pow(0.9, captureResistanceValue)); return { "captureChange": amount }; } }); - let cmpHealth = AddMock(this.EntityID, IID_Health, { + const cmpHealth = AddMock(this.EntityID, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, damage * bonus); return { "healthChange": -amount }; @@ -395,8 +460,8 @@ "GetHitpoints": () => 1, "GetMaxHitpoints": () => 1 }); - let healthSpy = new Spy(cmpHealth, "TakeDamage"); - let captureSpy = new Spy(cmpCapturable, "Capture"); + const healthSpy = new Spy(cmpHealth, "TakeDamage"); + const captureSpy = new Spy(cmpCapturable, "Capture"); AttackHelper.HandleAttackEffects(this.EntityID, { "type": "Test", @@ -409,7 +474,7 @@ } } -let cmp = new testResistance(); +const cmp = new testResistance(); cmp.TestInvulnerability(); cmp.TestBonus(); cmp.TestDamageResistanceApplies(); Index: binaries/data/mods/public/simulation/data/damage_types/crush.json =================================================================== --- binaries/data/mods/public/simulation/data/damage_types/crush.json +++ binaries/data/mods/public/simulation/data/damage_types/crush.json @@ -2,5 +2,6 @@ "code": "Crush", "name": "Crush", "description": "Damage caused by sheer force, like with a club or by trampling.", + "showInTooltip": "always", "order": 3 } Index: binaries/data/mods/public/simulation/data/damage_types/fire.json =================================================================== --- binaries/data/mods/public/simulation/data/damage_types/fire.json +++ binaries/data/mods/public/simulation/data/damage_types/fire.json @@ -2,5 +2,6 @@ "code": "Fire", "name": "Fire", "description": "Damage caused by burning objects, like a torch or a flaming arrow.", + "showInTooltip": "nonzero", "order": 4 } Index: binaries/data/mods/public/simulation/data/damage_types/hack.json =================================================================== --- binaries/data/mods/public/simulation/data/damage_types/hack.json +++ binaries/data/mods/public/simulation/data/damage_types/hack.json @@ -2,5 +2,6 @@ "code": "Hack", "name": "Hack", "description": "Damage caused by sharp objects cutting or chopping, like with a sword or an axe.", + "showInTooltip": "always", "order": 1 } Index: binaries/data/mods/public/simulation/data/damage_types/pierce.json =================================================================== --- binaries/data/mods/public/simulation/data/damage_types/pierce.json +++ binaries/data/mods/public/simulation/data/damage_types/pierce.json @@ -2,5 +2,6 @@ "code": "Pierce", "name": "Pierce", "description": "Damage caused by sharp pointy objects, like arrows or spears.", + "showInTooltip": "always", "order": 2 } Index: binaries/data/mods/public/simulation/data/damage_types/poison.json =================================================================== --- binaries/data/mods/public/simulation/data/damage_types/poison.json +++ binaries/data/mods/public/simulation/data/damage_types/poison.json @@ -2,5 +2,6 @@ "code": "Poison", "name": "Poison", "description": "Damage caused by venomous or poisoned objects, like a snake or a poisoned weapon.", + "showInTooltip": "nonzero", "order": 5 } Index: binaries/data/mods/public/simulation/helpers/Attack.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Attack.js +++ binaries/data/mods/public/simulation/helpers/Attack.js @@ -167,34 +167,37 @@ if (!cmpResistance) cmpResistance = Engine.QueryInterface(target, IID_Resistance); - let resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {}; + const resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {}; - if (effectType == "Damage") - for (let type in effectData.Damage) - total += effectData.Damage[type] * Math.pow(0.9, resistanceStrengths.Damage ? resistanceStrengths.Damage[type] || 0 : 0); - else if (effectType == "Capture") + // Having no resistance to a given effectType means invulnerability. + if (resistanceStrengths[effectType] === undefined || effectData[effectType] === undefined) + return 0; + + if (effectType === "Damage") + for (const type in effectData.Damage) + { + if (resistanceStrengths.Damage[type] === undefined) + continue; + total += effectData.Damage[type] * Math.pow(0.9, resistanceStrengths.Damage[type]); + } + else if (effectType === "Capture") { - total = effectData.Capture * Math.pow(0.9, resistanceStrengths.Capture || 0); + total = effectData.Capture * Math.pow(0.9, resistanceStrengths.Capture); // If Health is lower we are more susceptible to capture attacks. - let cmpHealth = Engine.QueryInterface(target, IID_Health); + const cmpHealth = Engine.QueryInterface(target, IID_Health); if (cmpHealth) total /= 0.1 + 0.9 * cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints(); } - if (effectType != "ApplyStatus") + + if (effectType !== "ApplyStatus") return total * bonusMultiplier; - if (!resistanceStrengths.ApplyStatus) - return effectData[effectType]; - - let result = {}; - for (let statusEffect in effectData[effectType]) + const result = {}; + for (const statusEffect in effectData[effectType]) { - if (!resistanceStrengths.ApplyStatus[statusEffect]) - { - result[statusEffect] = effectData[effectType][statusEffect]; + if (resistanceStrengths.ApplyStatus[statusEffect] === undefined) continue; - } if (randBool(resistanceStrengths.ApplyStatus[statusEffect].blockChance)) continue; @@ -302,19 +305,19 @@ */ AttackHelper.prototype.HandleAttackEffects = function(target, data, bonusMultiplier = 1) { - let cmpResistance = Engine.QueryInterface(target, IID_Resistance); + const cmpResistance = Engine.QueryInterface(target, IID_Resistance); if (cmpResistance && cmpResistance.IsInvulnerable()) return false; bonusMultiplier *= !data.attackData.Bonuses ? 1 : this.GetAttackBonus(data.attacker, target, data.type, data.attackData.Bonuses); - let targetState = {}; - for (let receiver of g_AttackEffects.Receivers()) + const targetState = {}; + for (const receiver of g_AttackEffects.Receivers()) { if (!data.attackData[receiver.type]) continue; - let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]); + const cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]); if (!cmpReceiver) continue; Index: binaries/data/mods/public/simulation/helpers/tests/test_Attack.js =================================================================== --- binaries/data/mods/public/simulation/helpers/tests/test_Attack.js +++ binaries/data/mods/public/simulation/helpers/tests/test_Attack.js @@ -36,7 +36,7 @@ this.TESTED_ENTITY_ID = 5; this.attackData = { - "Damage": "1", + "Damage": { "Name": "1" }, "Capture": "2", "ApplyStatus": { "statusName": {} @@ -44,20 +44,45 @@ }; } + setupTarget(id, hasHealth, hasCapture) + { + DeleteMock(id, IID_Health); + DeleteMock(id, IID_Capturable); + + // Resistance is always needed. + AddMock(id, IID_Resistance, { + "GetEffectiveResistanceAgainst": (effectType) => ({ + "Damage": { + "Name": 0, + }, + "Capture": 0, + "ApplyStatus": { + "statusName": { + "blockChance": 0, + } + } + }), + "IsInvulnerable": () => false, + }); + + if (hasHealth) + AddMock(id, IID_Health, { + "TakeDamage": x => { this.resultString += x; return { "Damage": x }; }, + "GetHitpoints": () => 1, + "GetMaxHitpoints": () => 1, + }); + + if (hasCapture) + AddMock(id, IID_Capturable, { + "Capture": x => { this.resultString += x; return { "Capture": x }; }, + }); + } + /** * This tests that we inflict multiple effect types. */ testMultipleEffects() { - AddMock(this.TESTED_ENTITY_ID, IID_Health, { - "TakeDamage": x => { this.resultString += x; }, - "GetHitpoints": () => 1, - "GetMaxHitpoints": () => 1, - }); - - AddMock(this.TESTED_ENTITY_ID, IID_Capturable, { - "Capture": x => { this.resultString += x; }, - }); - + this.setupTarget(this.TESTED_ENTITY_ID, true, true); AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { "type": "Test", "attackData": this.attackData, @@ -65,7 +90,7 @@ "attackerOwner": INVALID_PLAYER }); - TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) !== -1); + TS_ASSERT(this.resultString.indexOf(this.attackData.Damage.Name) !== -1); TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) !== -1); } @@ -73,9 +98,7 @@ * This tests that we correctly handle effect types if one is not received. */ testSkippedEffect() { - AddMock(this.TESTED_ENTITY_ID, IID_Capturable, { - "Capture": x => { this.resultString += x; }, - }); + this.setupTarget(this.TESTED_ENTITY_ID, false, true); AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { "type": "Test", @@ -83,16 +106,11 @@ "attacker": INVALID_ENTITY, "attackerOwner": INVALID_PLAYER }); - TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) === -1); + TS_ASSERT(this.resultString.indexOf(this.attackData.Damage.Name) === -1); TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) !== -1); this.resultString = ""; - DeleteMock(this.TESTED_ENTITY_ID, IID_Capturable); - AddMock(this.TESTED_ENTITY_ID, IID_Health, { - "TakeDamage": x => { this.resultString += x; }, - "GetHitpoints": () => 1, - "GetMaxHitpoints": () => 1, - }); + this.setupTarget(this.TESTED_ENTITY_ID, true, false); AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { "type": "Test", @@ -100,7 +118,7 @@ "attacker": INVALID_ENTITY, "attackerOwner": INVALID_PLAYER }); - TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) !== -1); + TS_ASSERT(this.resultString.indexOf(this.attackData.Damage.Name) !== -1); TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) === -1); } @@ -109,6 +127,9 @@ */ testAttackedMessage() { Engine.PostMessage = () => TS_ASSERT(false); + + this.setupTarget(this.TESTED_ENTITY_ID, false, false); + AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { "type": "Test", "attackData": this.attackData, @@ -116,9 +137,8 @@ "attackerOwner": INVALID_PLAYER }); - AddMock(this.TESTED_ENTITY_ID, IID_Capturable, { - "Capture": () => ({ "captureChange": 0 }), - }); + this.setupTarget(this.TESTED_ENTITY_ID, false, true); + let count = 0; Engine.PostMessage = () => count++; AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { @@ -129,11 +149,8 @@ }); TS_ASSERT_EQUALS(count, 1); - AddMock(this.TESTED_ENTITY_ID, IID_Health, { - "TakeDamage": () => ({ "healthChange": 0 }), - "GetHitpoints": () => 1, - "GetMaxHitpoints": () => 1, - }); + this.setupTarget(this.TESTED_ENTITY_ID, true, true); + count = 0; Engine.PostMessage = () => count++; AttackHelper.HandleAttackEffects(this.TESTED_ENTITY_ID, { @@ -169,9 +186,10 @@ * Regression test that bonus multiplier is handled correctly. */ testBonusMultiplier() { + this.setupTarget(this.TESTED_ENTITY_ID, false, false); AddMock(this.TESTED_ENTITY_ID, IID_Health, { "TakeDamage": (amount, __, ___) => { - TS_ASSERT_EQUALS(amount, this.attackData.Damage * 2); + TS_ASSERT_EQUALS(amount, this.attackData.Damage.Name * 2); }, "GetHitpoints": () => 1, "GetMaxHitpoints": () => 1, 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 @@ -105,30 +105,28 @@ - - - 0.0 - 1 - - 1 10 1 + 0 + 0 + + + - - - 0.0 - 1 - - 1 10 1 + 0 + 0 + + + Index: binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml +++ binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml @@ -19,12 +19,16 @@ + + + 4 25 2 + Index: binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml +++ binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml @@ -23,6 +23,14 @@ 4.5 + + + + + + + + Index: binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml +++ binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml @@ -35,12 +35,16 @@ + + + 15 40 5 + Index: binaries/data/mods/public/simulation/templates/template_unit.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit.xml +++ binaries/data/mods/public/simulation/templates/template_unit.xml @@ -82,7 +82,13 @@ 1 1 1 + 0 + 0 + + + +