From 16b452cf91774389c1bb283d78ff08568d083766 Mon Sep 17 00:00:00 2001 From: wraitii Date: Thu, 22 Aug 2019 18:00:33 +0000 Subject: [PATCH] Generalise Attack effects. All attacks, including death damage and splash, can deal any number of attack effects (damaging, capture, giving status effects.) This moves most of what was in the Damage system component to a helper, and renames that component DelayedDamage. It also introduces a new global script with all possible attack effects. Comments Taken From: Freagarach, Stan, bb Differential Revision: https://code.wildfiregames.com/D2092 This was SVN commit r22754. --- .../public/globalscripts/AttackEffects.js | 16 + .../mods/public/globalscripts/Templates.js | 52 +-- .../data/mods/public/gui/common/tooltips.js | 8 +- .../public/maps/random/polar_sea_triggers.js | 3 +- .../random/survivalofthefittest_triggers.js | 2 +- .../public/maps/scripts/CaptureTheRelic.js | 4 +- .../public/simulation/ai/common-api/entity.js | 2 +- .../public/simulation/components/AIProxy.js | 6 +- .../public/simulation/components/Armour.js | 51 ++- .../public/simulation/components/Attack.js | 178 ++-------- .../simulation/components/BuildingAI.js | 2 +- .../simulation/components/Capturable.js | 18 + .../public/simulation/components/Damage.js | 312 ------------------ .../simulation/components/DeathDamage.js | 34 +- .../simulation/components/DelayedDamage.js | 85 +++++ .../simulation/components/GuiInterface.js | 12 +- .../public/simulation/components/Health.js | 33 +- .../components/StatusEffectsReceiver.js | 28 +- .../public/simulation/components/UnitAI.js | 10 +- .../components/interfaces/Damage.js | 1 - .../components/interfaces/DelayedDamage.js | 1 + .../{DamageReceiver.js => Resistance.js} | 2 +- .../components/tests/test_Attack.js | 56 ++-- .../components/tests/test_Damage.js | 239 +++++++------- .../components/tests/test_DeathDamage.js | 38 +-- .../components/tests/test_GuiInterface.js | 2 +- .../components/tests/test_Health.js | 8 +- .../tests/test_StatusEffectsReceiver.js | 56 ++-- .../components/tests/test_UnitAI.js | 2 +- .../public/simulation/helpers/Attacking.js | 308 +++++++++++++++++ .../public/simulation/helpers/DamageBonus.js | 4 +- .../public/simulation/helpers/DamageTypes.js | 22 -- .../templates/template_unit_cavalry.xml | 2 +- .../templates/template_unit_champion.xml | 2 +- .../templates/template_unit_hero.xml | 2 +- .../templates/template_unit_infantry.xml | 2 +- 36 files changed, 794 insertions(+), 809 deletions(-) create mode 100755 binaries/data/mods/public/globalscripts/AttackEffects.js delete mode 100644 binaries/data/mods/public/simulation/components/Damage.js create mode 100644 binaries/data/mods/public/simulation/components/DelayedDamage.js delete mode 100644 binaries/data/mods/public/simulation/components/interfaces/Damage.js create mode 100644 binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js rename binaries/data/mods/public/simulation/components/interfaces/{DamageReceiver.js => Resistance.js} (75%) create mode 100644 binaries/data/mods/public/simulation/helpers/Attacking.js delete mode 100644 binaries/data/mods/public/simulation/helpers/DamageTypes.js diff --git a/binaries/data/mods/public/globalscripts/AttackEffects.js b/binaries/data/mods/public/globalscripts/AttackEffects.js new file mode 100755 index 0000000000..48d42eb590 --- /dev/null +++ b/binaries/data/mods/public/globalscripts/AttackEffects.js @@ -0,0 +1,16 @@ +// TODO: could be worth putting this in json files someday +const g_EffectTypes = ["Damage", "Capture", "GiveStatus"]; +const g_EffectReceiver = { + "Damage": { + "IID": "IID_Health", + "method": "TakeDamage" + }, + "Capture": { + "IID": "IID_Capturable", + "method": "Capture" + }, + "GiveStatus": { + "IID": "IID_StatusEffectsReceiver", + "method": "GiveStatus" + } +}; diff --git a/binaries/data/mods/public/globalscripts/Templates.js b/binaries/data/mods/public/globalscripts/Templates.js index ea1dc89246..8a66fcc3da 100644 --- a/binaries/data/mods/public/globalscripts/Templates.js +++ b/binaries/data/mods/public/globalscripts/Templates.js @@ -170,6 +170,22 @@ function GetTemplateDataHelper(template, player, auraTemplates, resources, damag ret.armour[damageType] = getEntityValue("Armour/" + damageType); } + let getAttackEffects = (temp, path) => { + let effects = {}; + if (temp.Capture) + effects.Capture = getEntityValue(path + "/Capture"); + + if (temp.Damage) + { + effects.Damage = {}; + for (let damageType in temp.Damage) + effects.Damage[damageType] = getEntityValue(path + "/Damage/" + damageType); + } + + // TODO: status effects + return effects; + }; + if (template.Attack) { ret.attack = {}; @@ -179,36 +195,27 @@ function GetTemplateDataHelper(template, player, auraTemplates, resources, damag return getEntityValue("Attack/" + type + "/" + stat); }; - if (type == "Capture") - ret.attack.Capture = { - "value": getAttackStat("Value") - }; - else - { - ret.attack[type] = { - "minRange": getAttackStat("MinRange"), - "maxRange": getAttackStat("MaxRange"), - "elevationBonus": getAttackStat("ElevationBonus"), - "damage": {} - }; - for (let damageType in template.Attack[type].Damage) - ret.attack[type].damage[damageType] = getAttackStat("Damage/" + damageType); + ret.attack[type] = { + "minRange": getAttackStat("MinRange"), + "maxRange": getAttackStat("MaxRange"), + "elevationBonus": getAttackStat("ElevationBonus"), + }; + + ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * + (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange)); - ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * - (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange)); - } ret.attack[type].repeatTime = getAttackStat("RepeatTime"); + Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type)); + if (template.Attack[type].Splash) { ret.attack[type].splash = { // true if undefined "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape, - "damage": {} }; - for (let damageType in template.Attack[type].Splash.Damage) - ret.attack[type].splash.damage[damageType] = getAttackStat("Splash/Damage/" + damageType); + Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash")); } } } @@ -217,10 +224,9 @@ function GetTemplateDataHelper(template, player, auraTemplates, resources, damag { ret.deathDamage = { "friendlyFire": template.DeathDamage.FriendlyFire != "false", - "damage": {} }; - for (let damageType in template.DeathDamage.Damage) - ret.deathDamage.damage[damageType] = getEntityValue("DeathDamage/Damage/" + damageType); + + Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage")); } if (template.Auras && auraTemplates) diff --git a/binaries/data/mods/public/gui/common/tooltips.js b/binaries/data/mods/public/gui/common/tooltips.js index 51b6a22e61..87a196e3ac 100644 --- a/binaries/data/mods/public/gui/common/tooltips.js +++ b/binaries/data/mods/public/gui/common/tooltips.js @@ -270,8 +270,8 @@ function getAttackTooltip(template) "attackLabel": attackLabel, "details": type == "Capture" ? - template.attack.Capture.value : - damageTypesToText(template.attack[type].damage), + template.attack.Capture.Capture : + damageTypesToText(template.attack[type].Damage), "rate": rate })); continue; @@ -283,7 +283,7 @@ function getAttackTooltip(template) let relativeRange = realRange ? Math.round(realRange - maxRange) : 0; tooltips.push(sprintf(g_RangeTooltipString[relativeRange ? "relative" : "non-relative"][minRange ? "minRange" : "no-minRange"], { "attackLabel": attackLabel, - "damageTypes": damageTypesToText(template.attack[type].damage), + "damageTypes": damageTypesToText(template.attack[type].Damage), "rangeLabel": headerFont(translate("Range:")), "minRange": minRange, "maxRange": maxRange, @@ -313,7 +313,7 @@ function getSplashDamageTooltip(template) let splashDamageTooltip = sprintf(translate("%(label)s: %(value)s"), { "label": headerFont(g_SplashDamageTypes[splash.shape]), - "value": damageTypesToText(splash.damage) + "value": damageTypesToText(splash.Damage) }); if (g_AlwaysDisplayFriendlyFire || splash.friendlyFire) diff --git a/binaries/data/mods/public/maps/random/polar_sea_triggers.js b/binaries/data/mods/public/maps/random/polar_sea_triggers.js index 40d72f9959..9e478356da 100644 --- a/binaries/data/mods/public/maps/random/polar_sea_triggers.js +++ b/binaries/data/mods/public/maps/random/polar_sea_triggers.js @@ -37,7 +37,6 @@ Trigger.prototype.SpawnWolvesAndAttack = function() let allTargets; - let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); let players = new Array(TriggerHelper.GetNumberOfPlayers()).fill(0).map((v, i) => i + 1); for (let spawnPoint in attackers) @@ -52,7 +51,7 @@ Trigger.prototype.SpawnWolvesAndAttack = function() continue; // The returned entities are sorted by RangeManager already - let targets = cmpDamage.EntitiesNearPoint(attackerPos, 200, players).filter(ent => { + let targets = Attack.EntitiesNearPoint(attackerPos, 200, players).filter(ent => { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses); }); diff --git a/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js b/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js index b585d0098c..d58cd2bfa9 100644 --- a/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js +++ b/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js @@ -152,7 +152,7 @@ Trigger.prototype.InitStartingUnits = function() { this.playerCivicCenter[playerID] = TriggerHelper.GetPlayerEntitiesByClass(playerID, "CivilCentre")[0]; this.treasureFemale[playerID] = TriggerHelper.GetPlayerEntitiesByClass(playerID, "FemaleCitizen")[0]; - Engine.QueryInterface(this.treasureFemale[playerID], IID_DamageReceiver).SetInvulnerability(true); + Engine.QueryInterface(this.treasureFemale[playerID], IID_Resistance).SetInvulnerability(true); } }; diff --git a/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js b/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js index 2c378ebc6f..c8b3ce0251 100644 --- a/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js +++ b/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js @@ -20,8 +20,8 @@ Trigger.prototype.InitCaptureTheRelic = function() { this.relics[i] = TriggerHelper.SpawnUnits(pickRandom(potentialSpawnPoints), catafalqueTemplates[i], 1, 0)[0]; - let cmpDamageReceiver = Engine.QueryInterface(this.relics[i], IID_DamageReceiver); - cmpDamageReceiver.SetInvulnerability(true); + let cmpResistance = Engine.QueryInterface(this.relics[i], IID_Resistance); + cmpResistance.SetInvulnerability(true); let cmpPositionRelic = Engine.QueryInterface(this.relics[i], IID_Position); cmpPositionRelic.SetYRotation(randomAngle()); diff --git a/binaries/data/mods/public/simulation/ai/common-api/entity.js b/binaries/data/mods/public/simulation/ai/common-api/entity.js index a5e0f0bd3f..38c84b2c1d 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/entity.js +++ b/binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -246,7 +246,7 @@ m.Template = m.Class({ if (!this.get("Attack/Capture")) return undefined; - return +this.get("Attack/Capture/Value") || 0; + return +this.get("Attack/Capture/Capture") || 0; }, "attackTimes": function(type) { diff --git a/binaries/data/mods/public/simulation/components/AIProxy.js b/binaries/data/mods/public/simulation/components/AIProxy.js index e5b80d7e69..d643f46554 100644 --- a/binaries/data/mods/public/simulation/components/AIProxy.js +++ b/binaries/data/mods/public/simulation/components/AIProxy.js @@ -260,9 +260,9 @@ AIProxy.prototype.GetFullRepresentation = function() ret.hitpoints = cmpHealth.GetHitpoints(); } - let cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); - if (cmpDamageReceiver) - ret.invulnerability = cmpDamageReceiver.IsInvulnerable(); + let cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance); + if (cmpResistance) + ret.invulnerability = cmpResistance.IsInvulnerable(); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) diff --git a/binaries/data/mods/public/simulation/components/Armour.js b/binaries/data/mods/public/simulation/components/Armour.js index 42fdc4b682..9633fde307 100644 --- a/binaries/data/mods/public/simulation/components/Armour.js +++ b/binaries/data/mods/public/simulation/components/Armour.js @@ -1,5 +1,15 @@ function Armour() {} +Armour.prototype.DamageResistanceSchema = "" + + "" + + "" + + "" + + "Foundation" + + "" + + "" + + "" + + ""; + Armour.prototype.Schema = "Controls the damage resistance of the unit." + "" + @@ -7,12 +17,10 @@ Armour.prototype.Schema = "0.0" + "5.0" + "" + - BuildDamageTypesSchema("damage protection") + + Armour.prototype.DamageResistanceSchema + "" + "" + - "" + - BuildDamageTypesSchema("damage protection") + - "" + + Armour.prototype.DamageResistanceSchema + "" + ""; @@ -32,32 +40,7 @@ Armour.prototype.SetInvulnerability = function(invulnerability) Engine.PostMessage(this.entity, MT_InvulnerabilityChanged, { "entity": this.entity, "invulnerability": invulnerability }); }; -/** - * Take damage according to the entity's armor. - * @param {Object} strengths - { "hack": number, "pierce": number, "crush": number } or something like that. - * @param {number} multiplier - the damage multiplier. - * Returns object of the form { "killed": false, "change": -12 }. - */ -Armour.prototype.TakeDamage = function(strengths, multiplier = 1) -{ - if (this.invulnerable) - return { "killed": false, "change": 0 }; - - // Adjust damage values based on armour; exponential armour: damage = attack * 0.9^armour - var armourStrengths = this.GetArmourStrengths(); - - // Total is sum of individual damages - // Don't bother rounding, since HP is no longer integral. - var total = 0; - for (let type in strengths) - total += strengths[type] * multiplier * Math.pow(0.9, armourStrengths[type] || 0); - - // Reduce health - var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); - return cmpHealth.Reduce(total); -}; - -Armour.prototype.GetArmourStrengths = function() +Armour.prototype.GetArmourStrengths = function(effectType) { // Work out the armour values with technology effects. let applyMods = (type, foundation) => { @@ -70,12 +53,16 @@ Armour.prototype.GetArmourStrengths = function() else strength = +this.template[type]; - return ApplyValueModificationsToEntity("Armour/" + type, strength, this.entity); + return ApplyValueModificationsToEntity("Armour/" + effectType + "/" + type, strength, this.entity); }; let foundation = Engine.QueryInterface(this.entity, IID_Foundation) && this.template.Foundation; let ret = {}; + + if (effectType != "Damage") + return ret; + for (let damageType in this.template) if (damageType != "Foundation") ret[damageType] = applyMods(damageType, foundation); @@ -83,4 +70,4 @@ Armour.prototype.GetArmourStrengths = function() return ret; }; -Engine.RegisterComponentType(IID_DamageReceiver, "Armour", Armour); +Engine.RegisterComponentType(IID_Resistance, "Armour", Armour); diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index 32720a1ec4..cd6a8c0984 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -2,40 +2,6 @@ function Attack() {} var g_AttackTypes = ["Melee", "Ranged", "Capture"]; -Attack.prototype.statusEffectsSchema = - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - ""; - -Attack.prototype.bonusesSchema = - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - ""; - Attack.prototype.preferredClassesSchema = "" + "" + @@ -130,9 +96,7 @@ Attack.prototype.Schema = "" + "" + "" + - "" + - BuildDamageTypesSchema("damage strength") + - "" + + Attacking.BuildAttackEffectsSchema() + "" + "" + "" + @@ -140,7 +104,6 @@ Attack.prototype.Schema = "" + // TODO: it shouldn't be stretched "" + "" + - Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + @@ -149,9 +112,7 @@ Attack.prototype.Schema = "" + "" + "" + - "" + - BuildDamageTypesSchema("damage strength") + - "" + + Attacking.BuildAttackEffectsSchema() + "" + "" + ""+ @@ -179,10 +140,7 @@ Attack.prototype.Schema = "" + "" + "" + - "" + - BuildDamageTypesSchema("damage strength") + - "" + - Attack.prototype.bonusesSchema + + Attacking.BuildAttackEffectsSchema() + "" + "" + "" + @@ -217,8 +175,6 @@ Attack.prototype.Schema = "" + "" + "" + - Attack.prototype.statusEffectsSchema + - Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + @@ -227,12 +183,11 @@ Attack.prototype.Schema = "" + "" + "" + - "" + + Attacking.BuildAttackEffectsSchema() + "" + "" + // TODO: it shouldn't be stretched "" + "" + - Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + @@ -241,11 +196,8 @@ Attack.prototype.Schema = "" + "" + "" + - "" + - BuildDamageTypesSchema("damage strength") + - "" + + Attacking.BuildAttackEffectsSchema() + "" + // TODO: how do these work? - Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + @@ -387,6 +339,14 @@ Attack.prototype.GetFullAttackRange = function() return ret; }; +Attack.prototype.GetAttackEffectsData = function(type, splash) +{ + let tp = this.template[type]; + if (splash) + tp = tp.Splash; + return Attacking.GetAttackEffectsData("Attack/" + type + splash ? "/Splash" : "", tp, this.entity); +}; + Attack.prototype.GetBestAttackAgainst = function(target, allowCapture) { let cmpFormation = Engine.QueryInterface(target, IID_Formation); @@ -448,40 +408,16 @@ Attack.prototype.GetTimers = function(type) return { "prepare": prepare, "repeat": repeat }; }; -Attack.prototype.GetAttackStrengths = function(type) -{ - // Work out the attack values with technology effects - let template = this.template[type]; - let splash = ""; - if (!template) - { - template = this.template[type.split(".")[0]].Splash; - splash = "/Splash"; - } - - let applyMods = damageType => - ApplyValueModificationsToEntity("Attack/" + type + splash + "/Damage/" + damageType, +(template.Damage[damageType] || 0), this.entity); - - if (type == "Capture") - return { "value": ApplyValueModificationsToEntity("Attack/Capture/Value", +(template.Value || 0), this.entity) }; - - let ret = {}; - for (let damageType in template.Damage) - ret[damageType] = applyMods(damageType); - - return ret; -}; - Attack.prototype.GetSplashDamage = function(type) { if (!this.template[type].Splash) return false; - let splash = {}; - splash.damage = this.GetAttackStrengths(type + ".Splash"); - splash.friendlyFire = this.template[type].Splash.FriendlyFire != "false"; - splash.shape = this.template[type].Splash.Shape; - return splash; + return { + "attackData": this.GetAttackEffectsData(type, true), + "friendlyFire": this.template[type].Splash.FriendlyFire != "false", + "shape": this.template[type].Splash.Shape, + }; }; Attack.prototype.GetRange = function(type) @@ -498,15 +434,6 @@ Attack.prototype.GetRange = function(type) return { "max": max, "min": min, "elevationBonus": elevationBonus }; }; -Attack.prototype.GetBonusTemplate = function(type) -{ - let template = this.template[type]; - if (!template) - template = this.template[type.split(".")[0]].Splash; - - return template.Bonuses || null; -}; - /** * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last @@ -515,7 +442,6 @@ Attack.prototype.GetBonusTemplate = function(type) Attack.prototype.PerformAttack = function(type, target) { let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); - let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); // If this is a ranged attack, then launch a projectile if (type == "Ranged") @@ -528,7 +454,7 @@ Attack.prototype.PerformAttack = function(type, target) let horizSpeed = +this.template[type].Projectile.Speed; let gravity = +this.template[type].Projectile.Gravity; - //horizSpeed /= 2; gravity /= 2; // slow it down for testing + // horizSpeed /= 2; gravity /= 2; // slow it down for testing let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) @@ -575,7 +501,7 @@ Attack.prototype.PerformAttack = function(type, target) // TODO: Use unit rotation to implement x/z offsets. let deltaLaunchPoint = new Vector3D(0, this.template[type].Projectile.LaunchPoint["@y"], 0.0); let launchPoint = Vector3D.add(selfPosition, deltaLaunchPoint); - + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { @@ -598,66 +524,26 @@ Attack.prototype.PerformAttack = function(type, target) let data = { "type": type, - "attacker": this.entity, + "attackData": this.GetAttackEffectsData(type), "target": target, - "strengths": this.GetAttackStrengths(type), + "attacker": this.entity, + "attackerOwner": attackerOwner, "position": realTargetPosition, "direction": missileDirection, "projectileId": id, - "bonus": this.GetBonusTemplate(type), - "isSplash": false, - "attackerOwner": attackerOwner, - "attackImpactSound": attackImpactSound, - "statusEffects": this.template[type].StatusEffects + "attackImpactSound": attackImpactSound }; if (this.template[type].Splash) - { - data.friendlyFire = this.template[type].Splash.FriendlyFire != "false"; - data.radius = +this.template[type].Splash.Range; - data.shape = this.template[type].Splash.Shape; - data.isSplash = true; - data.splashStrengths = this.GetAttackStrengths(type + ".Splash"); - data.splashBonus = this.GetBonusTemplate(type + ".Splash"); - } - cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000 + +this.template[type].Delay, data); - } - else if (type == "Capture") - { - if (attackerOwner == INVALID_PLAYER) - return; - - let multiplier = GetDamageBonus(this.entity, target, type, this.GetBonusTemplate(type)); - let cmpHealth = Engine.QueryInterface(target, IID_Health); - if (!cmpHealth || cmpHealth.GetHitpoints() == 0) - return; - multiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints()); - - let cmpCapturable = Engine.QueryInterface(target, IID_Capturable); - if (!cmpCapturable || !cmpCapturable.CanCapture(attackerOwner)) - return; - - let strength = this.GetAttackStrengths("Capture").value * multiplier; - if (cmpCapturable.Reduce(strength, attackerOwner) && IsOwnedByEnemyOfPlayer(attackerOwner, target)) - Engine.PostMessage(target, MT_Attacked, { - "attacker": this.entity, - "target": target, - "type": type, - "damage": strength, - "attackerOwner": attackerOwner - }); + data.splash = { + "friendlyFire": this.template[type].Splash.FriendlyFire != "false", + "radius": +this.template[type].Splash.Range, + "shape": this.template[type].Splash.Shape, + "attackData": this.GetAttackEffectsData(type, true), + }; + cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", +this.template[type].Delay + timeToTarget * 1000, data); } else - { - // Melee attack - hurt the target immediately - cmpDamage.CauseDamage({ - "strengths": this.GetAttackStrengths(type), - "target": target, - "attacker": this.entity, - "multiplier": GetDamageBonus(this.entity, target, type, this.GetBonusTemplate(type)), - "type": type, - "attackerOwner": attackerOwner - }); - } + Attacking.HandleAttackEffects(type, this.GetAttackEffectsData(type), target, this.entity, attackerOwner); }; /** diff --git a/binaries/data/mods/public/simulation/components/BuildingAI.js b/binaries/data/mods/public/simulation/components/BuildingAI.js index 87969bfa30..e99f3b17f2 100644 --- a/binaries/data/mods/public/simulation/components/BuildingAI.js +++ b/binaries/data/mods/public/simulation/components/BuildingAI.js @@ -131,7 +131,7 @@ BuildingAI.prototype.SetupRangeQuery = function() var range = cmpAttack.GetRange(attackType); this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery( this.entity, range.min, range.max, range.elevationBonus, - enemies, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal")); + enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal")); cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery); }; diff --git a/binaries/data/mods/public/simulation/components/Capturable.js b/binaries/data/mods/public/simulation/components/Capturable.js index e136c199a7..381318bfc3 100644 --- a/binaries/data/mods/public/simulation/components/Capturable.js +++ b/binaries/data/mods/public/simulation/components/Capturable.js @@ -48,6 +48,24 @@ Capturable.prototype.SetCapturePoints = function(capturePointsArray) this.cp = capturePointsArray; }; +Capturable.prototype.Capture = function(effectData, attacker, attackerOwner, bonusMultiplier) +{ + let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); + + if (attackerOwner == INVALID_PLAYER || !this.CanCapture(attackerOwner) || + !cmpHealth || cmpHealth.GetHitpoints() == 0) + return {}; + + bonusMultiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints()); + + let total = Attacking.GetTotalAttackEffects({ "Capture": effectData }, "Capture") * bonusMultiplier; + + let change = this.Reduce(total, attackerOwner); + // TODO: implement loot + + return { "captureChange": change }; +}; + /** * Reduces the amount of capture points of an entity, * in favour of the player of the source diff --git a/binaries/data/mods/public/simulation/components/Damage.js b/binaries/data/mods/public/simulation/components/Damage.js deleted file mode 100644 index 2b82e715c3..0000000000 --- a/binaries/data/mods/public/simulation/components/Damage.js +++ /dev/null @@ -1,312 +0,0 @@ -function Damage() {} - -Damage.prototype.Schema = - ""; - -Damage.prototype.Init = function() -{ -}; - -/** - * Gives the position of the given entity, taking the lateness into account. - * @param {number} ent - Entity id of the entity we are finding the location for. - * @param {number} lateness - The time passed since the expected time to fire the function. - * @return {Vector3D} The location of the entity. - */ -Damage.prototype.InterpolatedLocation = function(ent, lateness) -{ - let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); - if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly - return undefined; - let curPos = cmpTargetPosition.GetPosition(); - let prevPos = cmpTargetPosition.GetPreviousPosition(); - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - let turnLength = cmpTimer.GetLatestTurnLength(); - return new Vector3D( - (curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength, - 0, - (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength); -}; - -/** - * Test if a point is inside of an entity's footprint. - * @param {number} ent - Id of the entity we are checking with. - * @param {Vector3D} point - The point we are checking with. - * @param {number} lateness - The time passed since the expected time to fire the function. - * @return {boolean} True if the point is inside of the entity's footprint. - */ -Damage.prototype.TestCollision = function(ent, point, lateness) -{ - let targetPosition = this.InterpolatedLocation(ent, lateness); - if (!targetPosition) - return false; - - let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint); - if (!cmpFootprint) - return false; - - let targetShape = cmpFootprint.GetShape(); - - if (!targetShape) - return false; - - if (targetShape.type == "circle") - return targetPosition.horizDistanceToSquared(point) < targetShape.radius * targetShape.radius; - - if (targetShape.type == "square") - { - let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y; - let distance = Vector2D.from3D(Vector3D.sub(point, targetPosition)).rotate(-angle); - return Math.abs(distance.x) < targetShape.width / 2 && Math.abs(distance.y) < targetShape.depth / 2; - } - - warn("TestCollision called with an invalid footprint shape"); - return false; -}; - -/** - * Get the list of players affected by the damage. - * @param {number} attackerOwner - The player id of the attacker. - * @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged. - * @return {number[]} The ids of players need to be damaged. - */ -Damage.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire) -{ - if (!friendlyFire) - return QueryPlayerIDInterface(attackerOwner).GetEnemies(); - - return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); -}; - -/** - * Handles hit logic after the projectile travel time has passed. - * @param {Object} data - The data sent by the caller. - * @param {number} data.attacker - The entity id of the attacker. - * @param {number} data.target - The entity id of the target. - * @param {Vector2D} data.origin - The origin of the projectile hit. - * @param {Object} data.strengths - Data of the form { 'hack': number, 'pierce': number, 'crush': number }. - * @param {string} data.type - The type of damage. - * @param {number} data.attackerOwner - The player id of the owner of the attacker. - * @param {boolean} data.isSplash - A flag indicating if it's splash damage. - * @param {Vector3D} data.position - The expected position of the target. - * @param {number} data.projectileId - The id of the projectile. - * @param {Vector3D} data.direction - The unit vector defining the direction. - * @param {Object} data.bonus - The attack bonus template from the attacker. - * @param {string} data.attackImpactSound - The name of the sound emited on impact. - * @param {Object} data.statusEffects - Status effects eg. poisoning, burning etc. - * ***When splash damage*** - * @param {boolean} data.friendlyFire - A flag indicating if allied entities are also damaged. - * @param {number} data.radius - The radius of the splash damage. - * @param {string} data.shape - The shape of the splash range. - * @param {Object} data.splashBonus - The attack bonus template from the attacker. - * @param {Object} data.splashStrengths - Data of the form { 'hack': number, 'pierce': number, 'crush': number }. - */ -Damage.prototype.MissileHit = function(data, lateness) -{ - if (!data.position) - return; - - let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager); - if (cmpSoundManager && data.attackImpactSound) - cmpSoundManager.PlaySoundGroupAtPosition(data.attackImpactSound, data.position); - - // Do this first in case the direct hit kills the target - if (data.isSplash) - { - this.CauseDamageOverArea({ - "attacker": data.attacker, - "origin": Vector2D.from3D(data.position), - "radius": data.radius, - "shape": data.shape, - "strengths": data.splashStrengths, - "splashBonus": data.splashBonus, - "direction": data.direction, - "playersToDamage": this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire), - "type": data.type, - "attackerOwner": data.attackerOwner - }); - } - - let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); - - // Deal direct damage if we hit the main target - // and if the target has DamageReceiver (not the case for a mirage for example) - let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); - if (cmpDamageReceiver && this.TestCollision(data.target, data.position, lateness)) - { - data.multiplier = GetDamageBonus(data.attacker, data.target, data.type, data.bonus); - this.CauseDamage(data); - cmpProjectileManager.RemoveProjectile(data.projectileId); - - let cmpStatusReceiver = Engine.QueryInterface(data.target, IID_StatusEffectsReceiver); - if (cmpStatusReceiver && data.statusEffects) - cmpStatusReceiver.InflictEffects(data.statusEffects); - - return; - } - - let targetPosition = this.InterpolatedLocation(data.target, lateness); - if (!targetPosition) - return; - - // If we didn't hit the main target look for nearby units - let cmpPlayer = QueryPlayerIDInterface(data.attackerOwner); - let ents = this.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies()); - for (let ent of ents) - { - if (!this.TestCollision(ent, data.position, lateness)) - continue; - - this.CauseDamage({ - "strengths": data.strengths, - "target": ent, - "attacker": data.attacker, - "multiplier": GetDamageBonus(data.attacker, ent, data.type, data.bonus), - "type": data.type, - "attackerOwner": data.attackerOwner - }); - cmpProjectileManager.RemoveProjectile(data.projectileId); - break; - } -}; - -/** - * Damages units around a given origin. - * @param {Object} data - The data sent by the caller. - * @param {number} data.attacker - The entity id of the attacker. - * @param {Vector2D} data.origin - The origin of the projectile hit. - * @param {number} data.radius - The radius of the splash damage. - * @param {string} data.shape - The shape of the radius. - * @param {Object} data.strengths - Data of the form { 'hack': number, 'pierce': number, 'crush': number }. - * @param {string} data.type - The type of damage. - * @param {number} data.attackerOwner - The player id of the attacker. - * @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage. - * @param {Object} data.splashBonus - The attack bonus template from the attacker. - * @param {number[]} data.playersToDamage - The array of player id's to damage. - */ -Damage.prototype.CauseDamageOverArea = function(data) -{ - // Get nearby entities and define variables - let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage); - let damageMultiplier = 1; - - // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin. - for (let ent of nearEnts) - { - let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D(); - if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction - damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / (data.radius * data.radius); - else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles) - { - // Get position of entity relative to splash origin. - let relativePos = entityPosition.sub(data.origin); - - // Get the position relative to the missile direction. - let direction = Vector2D.from3D(data.direction); - let parallelPos = relativePos.dot(direction); - let perpPos = relativePos.cross(direction); - - // The width of linear splash is one fifth of the normal splash radius. - let width = data.radius / 5; - - // Check that the unit is within the distance splash width of the line starting at the missile's - // landing point which extends in the direction of the missile for length splash radius. - if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions - damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) * - (1 - perpPos * perpPos / (width * width)); - else - damageMultiplier = 0; - } - else // In case someone calls this function with an invalid shape. - { - warn("The " + data.shape + " splash damage shape is not implemented!"); - } - - if (data.splashBonus) - damageMultiplier *= GetDamageBonus(data.attacker, ent, data.type, data.splashBonus); - - // Call CauseDamage which reduces the hitpoints, posts network command, plays sounds.... - this.CauseDamage({ - "strengths": data.strengths, - "target": ent, - "attacker": data.attacker, - "multiplier": damageMultiplier, - "type": data.type + ".Splash", - "attackerOwner": data.attackerOwner - }); - } -}; - -/** - * Causes damage on a given unit. - * @param {Object} data - The data passed by the caller. - * @param {Object} data.strengths - Data in the form of { 'hack': number, 'pierce': number, 'crush': number }. - * @param {number} data.target - The entity id of the target. - * @param {number} data.attacker - The entity id of the attacker. - * @param {number} data.multiplier - The damage multiplier. - * @param {string} data.type - The type of damage. - * @param {number} data.attackerOwner - The player id of the attacker. - */ -Damage.prototype.CauseDamage = function(data) -{ - let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); - if (!cmpDamageReceiver) - return; - - let targetState = cmpDamageReceiver.TakeDamage(data.strengths, data.multiplier); - - let cmpPromotion = Engine.QueryInterface(data.attacker, IID_Promotion); - let cmpLoot = Engine.QueryInterface(data.target, IID_Loot); - let cmpHealth = Engine.QueryInterface(data.target, IID_Health); - if (cmpPromotion && cmpLoot && cmpLoot.GetXp() > 0) - cmpPromotion.IncreaseXp(cmpLoot.GetXp() * -targetState.change / cmpHealth.GetMaxHitpoints()); - - if (targetState.killed) - this.TargetKilled(data.attacker, data.target, data.attackerOwner); - - Engine.PostMessage(data.target, MT_Attacked, { "attacker": data.attacker, "target": data.target, "type": data.type, "damage": -targetState.change, "attackerOwner": data.attackerOwner }); -}; - -/** - * Gets entities near a give point for given players. - * @param {Vector2D} origin - The point to check around. - * @param {number} radius - The radius around the point to check. - * @param {number[]} players - The players of which we need to check entities. - * @return {number[]} The id's of the entities in range of the given point. - */ -Damage.prototype.EntitiesNearPoint = function(origin, radius, players) -{ - // If there is insufficient data return an empty array. - if (!origin || !radius || !players) - return []; - - return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, 0, radius, players, IID_DamageReceiver); -}; - -/** - * Called when a unit kills something (another unit, building, animal etc). - * @param {number} attacker - The entity id of the killer. - * @param {number} target - The entity id of the target. - * @param {number} attackerOwner - The player id of the attacker. - */ -Damage.prototype.TargetKilled = function(attacker, target, attackerOwner) -{ - let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership); - let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner; - - // Add to killer statistics. - let cmpKillerPlayerStatisticsTracker = QueryPlayerIDInterface(atkOwner, IID_StatisticsTracker); - if (cmpKillerPlayerStatisticsTracker) - cmpKillerPlayerStatisticsTracker.KilledEntity(target); - // Add to loser statistics. - let cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(target, IID_StatisticsTracker); - if (cmpTargetPlayerStatisticsTracker) - cmpTargetPlayerStatisticsTracker.LostEntity(target); - - // If killer can collect loot, let's try to collect it. - let cmpLooter = Engine.QueryInterface(attacker, IID_Looter); - if (cmpLooter) - cmpLooter.Collect(target); -}; - -Engine.RegisterSystemComponentType(IID_Damage, "Damage", Damage); diff --git a/binaries/data/mods/public/simulation/components/DeathDamage.js b/binaries/data/mods/public/simulation/components/DeathDamage.js index 1248679655..16db65c843 100644 --- a/binaries/data/mods/public/simulation/components/DeathDamage.js +++ b/binaries/data/mods/public/simulation/components/DeathDamage.js @@ -33,10 +33,7 @@ DeathDamage.prototype.Schema = "" + "" + "" + - "" + - BuildDamageTypesSchema("damage strength") + - "" + - DeathDamage.prototype.bonusesSchema; + Attacking.BuildAttackEffectsSchema(); DeathDamage.prototype.Init = function() { @@ -44,22 +41,9 @@ DeathDamage.prototype.Init = function() DeathDamage.prototype.Serialize = null; // we have no dynamic state to save -DeathDamage.prototype.GetDeathDamageStrengths = function() +DeathDamage.prototype.GetDeathDamageEffects = function() { - // Work out the damage values with technology effects - let applyMods = damageType => - ApplyValueModificationsToEntity("DeathDamage/Damage/" + damageType, +(this.template.Damage[damageType] || 0), this.entity); - - let ret = {}; - for (let damageType in this.template.Damage) - ret[damageType] = applyMods(damageType); - - return ret; -}; - -DeathDamage.prototype.GetBonusTemplate = function() -{ - return this.template.Bonuses || null; + return Attacking.GetAttackEffectsData("DeathDamage", this.template, this.entity); }; DeathDamage.prototype.CauseDeathDamage = function() @@ -74,21 +58,19 @@ DeathDamage.prototype.CauseDeathDamage = function() if (owner == INVALID_PLAYER) warn("Unit causing death damage does not have any owner."); - let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); - let playersToDamage = cmpDamage.GetPlayersToDamage(owner, this.template.FriendlyFire); + let playersToDamage = Attacking.GetPlayersToDamage(owner, this.template.FriendlyFire); let radius = ApplyValueModificationsToEntity("DeathDamage/Range", +this.template.Range, this.entity); - cmpDamage.CauseDamageOverArea({ + Attacking.CauseDamageOverArea({ + "type": "Death", + "attackData": this.GetDeathDamageEffects(), "attacker": this.entity, + "attackerOwner": owner, "origin": pos, "radius": radius, "shape": this.template.Shape, - "strengths": this.GetDeathDamageStrengths(), - "splashBonus": this.GetBonusTemplate(), "playersToDamage": playersToDamage, - "type": "Death", - "attackerOwner": owner }); }; diff --git a/binaries/data/mods/public/simulation/components/DelayedDamage.js b/binaries/data/mods/public/simulation/components/DelayedDamage.js new file mode 100644 index 0000000000..abe0031b7c --- /dev/null +++ b/binaries/data/mods/public/simulation/components/DelayedDamage.js @@ -0,0 +1,85 @@ +function DelayedDamage() {} + +DelayedDamage.prototype.Schema = + ""; + +DelayedDamage.prototype.Init = function() +{ +}; + +/** + * Handles hit logic after the projectile travel time has passed. + * @param {Object} data - The data sent by the caller. + * @param {string} data.type - The type of damage. + * @param {Object} data.attackData - Data of the form { 'effectType': { ...opaque effect data... }, 'Bonuses': {...} }. + * @param {number} data.target - The entity id of the target. + * @param {number} data.attacker - The entity id of the attacker. + * @param {number} data.attackerOwner - The player id of the owner of the attacker. + * @param {Vector2D} data.origin - The origin of the projectile hit. + * @param {Vector3D} data.position - The expected position of the target. + * @param {number} data.projectileId - The id of the projectile. + * @param {Vector3D} data.direction - The unit vector defining the direction. + * @param {string} data.attackImpactSound - The name of the sound emited on impact. + * ***When splash damage*** + * @param {boolean} data.splash.friendlyFire - A flag indicating if allied entities are also damaged. + * @param {number} data.splash.radius - The radius of the splash damage. + * @param {string} data.splash.shape - The shape of the splash range. + * @param {Object} data.splash.attackData - same as attackData, for splash. + */ +DelayedDamage.prototype.MissileHit = function(data, lateness) +{ + if (!data.position) + return; + + let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager); + if (cmpSoundManager && data.attackImpactSound) + cmpSoundManager.PlaySoundGroupAtPosition(data.attackImpactSound, data.position); + + // Do this first in case the direct hit kills the target + if (data.splash) + { + Attacking.CauseDamageOverArea({ + "type": data.type, + "attackData": data.splash.attackData, + "attacker": data.attacker, + "attackerOwner": data.attackerOwner, + "origin": Vector2D.from3D(data.position), + "radius": data.splash.radius, + "shape": data.splash.shape, + "direction": data.direction, + "playersToDamage": Attacking.GetPlayersToDamage(data.attackerOwner, data.splash.friendlyFire) + }); + } + + let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); + + // Deal direct damage if we hit the main target + // and if the target has Resistance (not the case for a mirage for example) + if (Attacking.TestCollision(data.target, data.position, lateness)) + { + cmpProjectileManager.RemoveProjectile(data.projectileId); + + Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); + return; + } + + let targetPosition = Attacking.InterpolatedLocation(data.target, lateness); + if (!targetPosition) + return; + + // If we didn't hit the main target look for nearby units + let cmpPlayer = QueryPlayerIDInterface(data.attackerOwner); + let ents = Attacking.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies()); + for (let ent of ents) + { + if (!Attacking.TestCollision(ent, data.position, lateness)) + continue; + + Attacking.HandleAttackEffects(data.type, data.attackData, ent, data.attacker, data.attackerOwner); + + cmpProjectileManager.RemoveProjectile(data.projectileId); + break; + } +}; + +Engine.RegisterSystemComponentType(IID_DelayedDamage, "DelayedDamage", DelayedDamage); diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js index fd3421fedb..286e8bd800 100644 --- a/binaries/data/mods/public/simulation/components/GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -389,12 +389,12 @@ GuiInterface.prototype.GetEntityState = function(player, ent) for (let type of types) { ret.attack[type] = {}; - if (type == "Capture") - ret.attack[type] = cmpAttack.GetAttackStrengths(type); - else - ret.attack[type].damage = cmpAttack.GetAttackStrengths(type); + + Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type)); ret.attack[type].splash = cmpAttack.GetSplashDamage(type); + if (ret.attack[type].splash) + Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true)); let range = cmpAttack.GetRange(type); ret.attack[type].minRange = range.min; @@ -432,9 +432,9 @@ GuiInterface.prototype.GetEntityState = function(player, ent) } } - let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); + let cmpArmour = Engine.QueryInterface(ent, IID_Resistance); if (cmpArmour) - ret.armour = cmpArmour.GetArmourStrengths(); + ret.armour = cmpArmour.GetArmourStrengths("Damage"); let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) diff --git a/binaries/data/mods/public/simulation/components/Health.js b/binaries/data/mods/public/simulation/components/Health.js index 4f903775cf..8129c2a7f4 100644 --- a/binaries/data/mods/public/simulation/components/Health.js +++ b/binaries/data/mods/public/simulation/components/Health.js @@ -177,9 +177,34 @@ Health.prototype.Kill = function() this.Reduce(this.hitpoints); }; +/** + * Take damage according to the entity's resistance. + * @param {Object} strengths - { "hack": number, "pierce": number, "crush": number } or something like that. + * @param {number} bonusMultiplier - the damage multiplier. + * Returns object of the form { "killed": false, "change": -12 }. + */ +Health.prototype.TakeDamage = function(effectData, attacker, attackerOwner, bonusMultiplier) +{ + let cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance); + + if (cmpResistance && cmpResistance.IsInvulnerable()) + return { "killed": false }; + + let total = Attacking.GetTotalAttackEffects(effectData, "Damage", cmpResistance) * bonusMultiplier; + + // Reduce health + let change = this.Reduce(total); + + let cmpLoot = Engine.QueryInterface(this.entity, IID_Loot); + if (cmpLoot && cmpLoot.GetXp() > 0 && change.HPchange < 0) + change.xp = cmpLoot.GetXp() * -change.HPchange / this.GetMaxHitpoints(); + + return change; +}; + /** * @param {number} amount - The amount of hitpoints to substract. Kills the entity if required. - * @return {{killed:boolean, change:number}} - Number of health points lost and whether the entity was killed. + * @return {{killed:boolean, HPchange:number}} - Number of health points lost and whether the entity was killed. */ Health.prototype.Reduce = function(amount) { @@ -188,7 +213,7 @@ Health.prototype.Reduce = function(amount) // might get called multiple times) // Likewise if the amount is 0. if (!amount || !this.hitpoints) - return { "killed": false, "change": 0 }; + return { "killed": false, "HPchange": 0 }; // Before changing the value, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); @@ -202,7 +227,7 @@ Health.prototype.Reduce = function(amount) this.hitpoints = 0; this.RegisterHealthChanged(oldHitpoints); this.HandleDeath(); - return { "killed": true, "change": -oldHitpoints }; + return { "killed": true, "HPchange": -oldHitpoints }; } // If we are not marked as injured, do it now @@ -215,7 +240,7 @@ Health.prototype.Reduce = function(amount) this.hitpoints -= amount; this.RegisterHealthChanged(oldHitpoints); - return { "killed": false, "change": this.hitpoints - oldHitpoints }; + return { "killed": false, "HPchange": this.hitpoints - oldHitpoints }; }; /** diff --git a/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js b/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js index 54f0a44bb4..565bf3d8f2 100755 --- a/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js +++ b/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js @@ -5,13 +5,18 @@ StatusEffectsReceiver.prototype.Init = function() this.activeStatusEffects = {}; }; -StatusEffectsReceiver.prototype.InflictEffects = function(statusEffects) +// Called by attacking effects. +StatusEffectsReceiver.prototype.GiveStatus = function(effectData, attacker, attackerOwner, bonusMultiplier) { - for (let effect in statusEffects) - this.InflictEffect(effect, statusEffects[effect]); + for (let effect in effectData) + this.AddStatus(effect, effectData[effect]); + + // TODO: implement loot / resistance. + + return { "inflictedStatuses": Object.keys(effectData) }; }; -StatusEffectsReceiver.prototype.InflictEffect = function(statusName, data) +StatusEffectsReceiver.prototype.AddStatus = function(statusName, data) { if (this.activeStatusEffects[statusName]) return; @@ -28,7 +33,7 @@ StatusEffectsReceiver.prototype.InflictEffect = function(statusName, data) status.timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +status.interval, statusName); }; -StatusEffectsReceiver.prototype.RemoveEffect = function(statusName) { +StatusEffectsReceiver.prototype.RemoveStatus = function(statusName) { if (!this.activeStatusEffects[statusName]) return; @@ -51,19 +56,10 @@ StatusEffectsReceiver.prototype.ExecuteEffect = function(statusName, lateness) else status.timeElapsed += status.interval + lateness; - let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); - - cmpDamage.CauseDamage({ - "strengths": { [statusName]: status.damage }, - "target": this.entity, - "attacker": -1, - "multiplier": 1, - "type": statusName, - "attackerOwner": -1 - }); + Attacking.HandleAttackEffects(statusName, { "Damage": { [statusName]: status.damage } }, this.entity, -1, -1); if (status.timeElapsed >= status.duration) - this.RemoveEffect(statusName); + this.RemoveStatus(statusName); }; Engine.RegisterComponentType(IID_StatusEffectsReceiver, "StatusEffectsReceiver", StatusEffectsReceiver); diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 21f3b1721a..646eb3d3ad 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -2958,8 +2958,8 @@ UnitAI.prototype.UnitFsmSpec = { "CHEERING": { "enter": function() { // Unit is invulnerable while cheering - var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); - cmpDamageReceiver.SetInvulnerability(true); + var cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance); + cmpResistance.SetInvulnerability(true); this.SelectAnimation("promotion"); this.StartTimer(2800, 2800); return false; @@ -2968,8 +2968,8 @@ UnitAI.prototype.UnitFsmSpec = { "leave": function() { this.StopTimer(); this.ResetAnimation(); - var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); - cmpDamageReceiver.SetInvulnerability(false); + var cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance); + cmpResistance.SetInvulnerability(false); }, "Timer": function(msg) { @@ -3466,7 +3466,7 @@ UnitAI.prototype.SetupRangeQuery = function(enable = true) var players = cmpPlayer.GetEnemies(); var range = this.GetQueryRange(IID_Attack); - this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal")); + this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal")); if (enable) cmpRangeManager.EnableActiveQuery(this.losRangeQuery); diff --git a/binaries/data/mods/public/simulation/components/interfaces/Damage.js b/binaries/data/mods/public/simulation/components/interfaces/Damage.js deleted file mode 100644 index aed935a34c..0000000000 --- a/binaries/data/mods/public/simulation/components/interfaces/Damage.js +++ /dev/null @@ -1 +0,0 @@ -Engine.RegisterInterface("Damage"); diff --git a/binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js b/binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js new file mode 100644 index 0000000000..94edfbf3c1 --- /dev/null +++ b/binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js @@ -0,0 +1 @@ +Engine.RegisterInterface("DelayedDamage"); diff --git a/binaries/data/mods/public/simulation/components/interfaces/DamageReceiver.js b/binaries/data/mods/public/simulation/components/interfaces/Resistance.js similarity index 75% rename from binaries/data/mods/public/simulation/components/interfaces/DamageReceiver.js rename to binaries/data/mods/public/simulation/components/interfaces/Resistance.js index 111c319973..0226ee06f5 100644 --- a/binaries/data/mods/public/simulation/components/interfaces/DamageReceiver.js +++ b/binaries/data/mods/public/simulation/components/interfaces/Resistance.js @@ -1,4 +1,4 @@ -Engine.RegisterInterface("DamageReceiver"); +Engine.RegisterInterface("Resistance"); /** * Message of the form { "entity": entity, "invulnerability": true/false } 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 a6678fe779..4223f17055 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -1,5 +1,5 @@ Engine.LoadHelperScript("DamageBonus.js"); -Engine.LoadHelperScript("DamageTypes.js"); +Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Attack.js"); @@ -104,7 +104,7 @@ function attackComponentTest(defenderClass, isEnemy, test_function) } }, "Capture": { - "Value": 8, + "Capture": 8, "MaxRange": 10, }, "Slaughter": {} @@ -150,18 +150,28 @@ attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => { TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 }); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Capture"), { "value": 8 }); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 }); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Ranged"), { - "Hack": 0, - "Pierce": 10, - "Crush": 0 + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), { + "Damage": { + "Hack": 0, + "Pierce": 10, + "Crush": 0 + } }); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Ranged.Splash"), { - "Hack": 0.0, - "Pierce": 15.0, - "Crush": 35.0 + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), { + "Damage": { + "Hack": 0.0, + "Pierce": 15.0, + "Crush": 35.0 + }, + "Bonuses": { + "BonusCav": { + "Classes": "Cavalry", + "Multiplier": 3 + } + } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), { @@ -175,10 +185,18 @@ attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => { }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashDamage("Ranged"), { - "damage": { - "Hack": 0, - "Pierce": 15, - "Crush": 35, + "attackData": { + "Damage": { + "Hack": 0, + "Pierce": 15, + "Crush": 35, + }, + "Bonuses": { + "BonusCav": { + "Classes": "Cavalry", + "Multiplier": 3 + } + } }, "friendlyFire": false, "shape": "Circular" @@ -188,14 +206,14 @@ attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => { for (let className of ["Infantry", "Cavalry"]) attackComponentTest(className, true, (attacker, cmpAttack, defender) => { - TS_ASSERT_EQUALS(cmpAttack.GetBonusTemplate("Melee").BonusCav.Multiplier, 2); + TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Melee").Bonuses.BonusCav.Multiplier, 2); - TS_ASSERT(cmpAttack.GetBonusTemplate("Capture") === null); + TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Capture").Bonuses || null, null); - let getAttackBonus = (s, t, e) => GetDamageBonus(s, e, t, cmpAttack.GetBonusTemplate(t)); + let getAttackBonus = (s, t, e, splash) => GetAttackBonus(s, e, t, cmpAttack.GetAttackEffectsData(t, splash).Bonuses || null); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Melee", defender), className == "Cavalry" ? 2 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender), 1); - TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged.Splash", defender), className == "Cavalry" ? 3 : 1); + TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender, true), className == "Cavalry" ? 3 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Capture", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Slaughter", defender), 1); }); diff --git a/binaries/data/mods/public/simulation/components/tests/test_Damage.js b/binaries/data/mods/public/simulation/components/tests/test_Damage.js index c309787102..1998f0efdc 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -1,13 +1,13 @@ Engine.LoadHelperScript("DamageBonus.js"); -Engine.LoadHelperScript("DamageTypes.js"); +Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/AttackDetection.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); -Engine.LoadComponentScript("interfaces/Damage.js"); -Engine.LoadComponentScript("interfaces/DamageReceiver.js"); +Engine.LoadComponentScript("interfaces/DelayedDamage.js"); +Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Player.js"); @@ -16,22 +16,24 @@ Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Attack.js"); -Engine.LoadComponentScript("Damage.js"); +Engine.LoadComponentScript("DelayedDamage.js"); Engine.LoadComponentScript("Timer.js"); function Test_Generic() { ResetState(); - let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); - cmpTimer.OnUpdate({ turnLength: 1 }); + cmpTimer.OnUpdate({ "turnLength": 1 }); let attacker = 11; let atkPlayerEntity = 1; let attackerOwner = 6; let cmpAttack = ConstructComponent(attacker, "Attack", { "Ranged": { + "Damage": { + "Crush": 5, + }, "MaxRange": 50, "MinRange": 0, "Delay": 0, @@ -51,19 +53,19 @@ function Test_Generic() let type = "Melee"; let damageTaken = false; - cmpAttack.GetAttackStrengths = attackType => ({ "hack": 0, "pierce": 0, "crush": damage }); + cmpAttack.GetAttackStrengths = attackType => ({ "Hack": 0, "Pierce": 0, "Crush": damage }); let data = { - "attacker": attacker, - "target": target, "type": "Melee", - "strengths": { "hack": 0, "pierce": 0, "crush": damage }, - "multiplier": 1.0, + "attackData": { + "Damage": { "Hack": 0, "Pierce": 0, "Crush": damage }, + }, + "target": target, + "attacker": attacker, "attackerOwner": attackerOwner, "position": targetPos, - "isSplash": false, "projectileId": 9, - "direction": new Vector3D(1,0,0) + "direction": new Vector3D(1, 0, 0) }; AddMock(atkPlayerEntity, IID_Player, { @@ -87,15 +89,22 @@ function Test_Generic() "IsInWorld": () => true, }); - AddMock(target, IID_Health, {}); + AddMock(target, IID_Health, { + "TakeDamage": (effectData, __, ___, bonusMultiplier) => { + damageTaken = true; + return { "killed": false, "HPchange": -bonusMultiplier * effectData.Crush }; + }, + }); - AddMock(target, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { damageTaken = true; return { "killed": false, "change": -multiplier * strengths.crush }; }, + AddMock(SYSTEM_ENTITY, IID_DelayedDamage, { + "MissileHit": () => { + damageTaken = true; + }, }); Engine.PostMessage = function(ent, iid, message) { - TS_ASSERT_UNEVAL_EQUALS({ "attacker": attacker, "target": target, "type": type, "damage": damage, "attackerOwner": attackerOwner }, message); + TS_ASSERT_UNEVAL_EQUALS({ "type": type, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "damage": damage, "capture": 0, "statusEffects": [] }, message); }; AddMock(target, IID_Footprint, { @@ -114,16 +123,17 @@ function Test_Generic() function TestDamage() { - cmpTimer.OnUpdate({ turnLength: 1 }); + cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT(damageTaken); damageTaken = false; } - cmpDamage.CauseDamage(data); + Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); TestDamage(); - type = data.type = "Ranged"; - cmpDamage.CauseDamage(data); + data.type = "Ranged"; + type = data.type; + Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); TestDamage(); // Check for damage still being dealt if the attacker dies @@ -135,8 +145,8 @@ function Test_Generic() AddMock(atkPlayerEntity, IID_Player, { "GetEnemies": () => [2, 3] }); - TS_ASSERT_UNEVAL_EQUALS(cmpDamage.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]); - TS_ASSERT_UNEVAL_EQUALS(cmpDamage.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]); + TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]); + TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]); } Test_Generic(); @@ -152,26 +162,24 @@ function TestLinearSplashDamage() const origin = new Vector2D(0, 0); let data = { + "type": "Ranged", + "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": attacker, + "attackerOwner": attackerOwner, "origin": origin, "radius": 10, "shape": "Linear", - "strengths": { "hack" : 100, "pierce" : 0, "crush": 0 }, "direction": new Vector3D(1, 747, 0), "playersToDamage": [2], - "type": "Ranged", - "attackerOwner": attackerOwner }; - let fallOff = function(x,y) + let fallOff = function(x, y) { return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius)); }; let hitEnts = new Set(); - let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); - AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62], }); @@ -188,31 +196,31 @@ function TestLinearSplashDamage() "GetPosition2D": () => new Vector2D(5, 2), }); - AddMock(60, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { + AddMock(60, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(2.2, -0.4)); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(2.2, -0.4)); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); - AddMock(61, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { + AddMock(61, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(0, 0)); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0, 0)); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); - AddMock(62, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { + AddMock(62, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(62); - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 0); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); - cmpDamage.CauseDamageOverArea(data); + Attacking.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); @@ -220,15 +228,15 @@ function TestLinearSplashDamage() data.direction = new Vector3D(0.6, 747, 0.8); - AddMock(60, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { + AddMock(60, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(1, 2)); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1, 2)); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); - cmpDamage.CauseDamageOverArea(data); + Attacking.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); @@ -249,8 +257,6 @@ function TestCircularSplashDamage() return 1 - r * r / (radius * radius); }; - let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); - AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62, 64], }); @@ -276,49 +282,49 @@ function TestCircularSplashDamage() "GetPosition2D": () => new Vector2D(9, -4), }); - AddMock(60, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(0)); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + AddMock(60, IID_Resistance, { + "TakeDamage": (effectData, __, ___, mult) => { + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0)); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); - AddMock(61, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(5)); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + AddMock(61, IID_Resistance, { + "TakeDamage": (effectData, __, ___, mult) => { + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(5)); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); - AddMock(62, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(1)); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + AddMock(62, IID_Resistance, { + "TakeDamage": (effectData, __, ___, mult) => { + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1)); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); - AddMock(63, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { + AddMock(63, IID_Resistance, { + "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT(false); } }); - AddMock(64, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 0); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + AddMock(64, IID_Resistance, { + "TakeDamage": (effectData, __, ___, mult) => { + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); - cmpDamage.CauseDamageOverArea({ + Attacking.CauseDamageOverArea({ + "type": "Ranged", + "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": 50, + "attackerOwner": 1, "origin": new Vector2D(3, 4), "radius": radius, "shape": "Circular", - "strengths": { "hack" : 100, "pierce" : 0, "crush": 0 }, "playersToDamage": [2], - "type": "Ranged", - "attackerOwner": 1 }); } @@ -329,7 +335,7 @@ function Test_MissileHit() ResetState(); Engine.PostMessage = (ent, iid, message) => {}; - let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); + let cmpDelayedDamage = ConstructComponent(SYSTEM_ENTITY, "DelayedDamage"); let target = 60; let targetOwner = 1; @@ -344,15 +350,13 @@ function Test_MissileHit() let data = { "type": "Ranged", - "attacker": 70, + "attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } }, "target": 60, - "strengths": { "hack": 0, "pierce": 100, "crush": 0 }, + "attacker": 70, + "attackerOwner": 1, "position": targetPos, "direction": new Vector3D(1, 0, 0), "projectileId": 9, - "bonus": undefined, - "isSplash": false, - "attackerOwner": 1 }; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { @@ -372,13 +376,11 @@ function Test_MissileHit() "IsInWorld": () => true, }); - AddMock(60, IID_Health, {}); - - AddMock(60, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { + AddMock(60, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); @@ -400,7 +402,7 @@ function Test_MissileHit() "GetEnemies": () => [2] }); - cmpDamage.MissileHit(data, 0); + cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(60)); hitEnts.clear(); @@ -413,10 +415,10 @@ function Test_MissileHit() "IsInWorld": () => true, }); - AddMock(60, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { + AddMock(60, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(false); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); @@ -431,13 +433,11 @@ function Test_MissileHit() "IsInWorld": () => true, }); - AddMock(61, IID_Health, {}); - - AddMock(61, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100); + AddMock(61, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); @@ -445,28 +445,27 @@ function Test_MissileHit() "GetShape": () => ({ "type": "circle", "radius": 20 }), }); - cmpDamage.MissileHit(data, 0); + cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); hitEnts.clear(); // Add a splash damage. - - data.friendlyFire = false; - data.radius = 10; - data.shape = "Circular"; - data.isSplash = true; - data.splashStrengths = { "hack": 0, "pierce": 0, "crush": 200 }; + data.splash = {}; + data.splash.friendlyFire = false; + data.splash.radius = 10; + data.splash.shape = "Circular"; + data.splash.attackData = { "Damage": { "Hack": 0, "Pierce": 0, "Crush": 200 } }; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61, 62] }); let dealtDamage = 0; - AddMock(61, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { - dealtDamage += multiplier * (strengths.hack + strengths.pierce + strengths.crush); + AddMock(61, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + dealtDamage += mult * (effectData.Hack + effectData.Pierce + effectData.Crush); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); @@ -477,13 +476,11 @@ function Test_MissileHit() "IsInWorld": () => true, }); - AddMock(62, IID_Health, {}); - - AddMock(62, IID_DamageReceiver, { - "TakeDamage": (strengths, multiplier) => { - TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 200 * 0.75); + AddMock(62, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(62); - return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) }; + TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 200 * 0.75); + return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); @@ -491,7 +488,7 @@ function Test_MissileHit() "GetShape": () => ({ "type": "circle", "radius": 20 }), }); - cmpDamage.MissileHit(data, 0); + cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); dealtDamage = 0; @@ -512,36 +509,36 @@ function Test_MissileHit() "HasClass": cl => cl == "Cavalry" }); - data.bonus = bonus; - cmpDamage.MissileHit(data, 0); + data.attackData.Bonuses = bonus; + cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 200); dealtDamage = 0; hitEnts.clear(); - data.splashBonus = splashBonus; - cmpDamage.MissileHit(data, 0); + data.splash.attackData.Bonuses = splashBonus; + cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); - data.bonus = undefined; - cmpDamage.MissileHit(data, 0); + data.attackData.Bonuses = undefined; + cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); - data.bonus = null; - cmpDamage.MissileHit(data, 0); + data.attackData.Bonuses = null; + cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); - data.bonus = {}; - cmpDamage.MissileHit(data, 0); + data.attackData.Bonuses = {}; + cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; diff --git a/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js b/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js index 3f48723261..9947791a83 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js +++ b/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js @@ -1,8 +1,7 @@ Engine.LoadHelperScript("DamageBonus.js"); -Engine.LoadHelperScript("DamageTypes.js"); +Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); -Engine.LoadComponentScript("interfaces/Damage.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("DeathDamage.js"); @@ -28,10 +27,12 @@ let template = { } }; -let modifiedDamage = { - "Hack": 0.0, - "Pierce": 215.0, - "Crush": 35.0 +let effects = { + "Damage": { + "Hack": 0.0, + "Pierce": 215.0, + "Crush": 35.0 + } }; let cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template); @@ -40,21 +41,18 @@ let playersToDamage = [2, 3, 7]; let pos = new Vector2D(3, 4.2); let result = { + "type": "Death", + "attackData": effects, "attacker": deadEnt, + "attackerOwner": player, "origin": pos, "radius": template.Range, "shape": template.Shape, - "strengths": modifiedDamage, - "splashBonus": null, - "playersToDamage": playersToDamage, - "type": "Death", - "attackerOwner": player + "playersToDamage": playersToDamage }; -AddMock(SYSTEM_ENTITY, IID_Damage, { - "CauseDamageOverArea": data => TS_ASSERT_UNEVAL_EQUALS(data, result), - "GetPlayersToDamage": (owner, friendlyFire) => playersToDamage -}); +Attacking.CauseDamageOverArea = data => TS_ASSERT_UNEVAL_EQUALS(data, result); +Attacking.GetPlayersToDamage = () => playersToDamage; AddMock(deadEnt, IID_Position, { "GetPosition2D": () => pos, @@ -65,13 +63,13 @@ AddMock(deadEnt, IID_Ownership, { "GetOwner": () => player }); -TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage); +TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects); cmpDeathDamage.CauseDeathDamage(); // Test splash damage bonus -let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } }; -template.Bonuses = splashBonus; +effects.Bonuses = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } }; +template.Bonuses = effects.Bonuses; cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template); -result.splashBonus = splashBonus; -TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage); +result.attackData.Bonuses = effects.Bonuses; +TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects); cmpDeathDamage.CauseDeathDamage(); diff --git a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js index ef662c0f1d..190a16da9b 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -6,7 +6,7 @@ Engine.LoadComponentScript("interfaces/Barter.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/CeasefireManager.js"); -Engine.LoadComponentScript("interfaces/DamageReceiver.js"); +Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/EndGameManager.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); diff --git a/binaries/data/mods/public/simulation/components/tests/test_Health.js b/binaries/data/mods/public/simulation/components/tests/test_Health.js index aa2a1bcc50..161b23a214 100755 --- a/binaries/data/mods/public/simulation/components/tests/test_Health.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Health.js @@ -64,7 +64,7 @@ var change = cmpHealth.Reduce(25); TS_ASSERT_EQUALS(injured_flag, true); TS_ASSERT_EQUALS(change.killed, false); -TS_ASSERT_EQUALS(change.change, -25); +TS_ASSERT_EQUALS(change.HPchange, -25); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 25); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), true); @@ -107,7 +107,7 @@ TS_ASSERT_EQUALS(corpse_entity, "corpse|test"); TS_ASSERT_EQUALS(injured_flag, false); TS_ASSERT_EQUALS(change.killed, true); -TS_ASSERT_EQUALS(change.change, -50); +TS_ASSERT_EQUALS(change.HPchange, -50); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); @@ -122,7 +122,7 @@ TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); // Check that we can't die twice. change = cmpHealth.Reduce(50); TS_ASSERT_EQUALS(change.killed, false); -TS_ASSERT_EQUALS(change.change, 0); +TS_ASSERT_EQUALS(change.HPchange, 0); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); @@ -132,7 +132,7 @@ cmpHealth = setEntityUp(); // Check that we still die with > Max HP of damage. change = cmpHealth.Reduce(60); TS_ASSERT_EQUALS(change.killed, true); -TS_ASSERT_EQUALS(change.change, -50); +TS_ASSERT_EQUALS(change.HPchange, -50); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); 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 30bfcd7f35..0e4703f7d1 100755 --- a/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js +++ b/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js @@ -1,4 +1,3 @@ -Engine.LoadComponentScript("interfaces/Damage.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("StatusEffectsReceiver.js"); @@ -20,17 +19,16 @@ function testInflictEffects() { setup(); let statusName = "Burn"; - AddMock(SYSTEM_ENTITY, IID_Damage, { - "CauseDamage": (data) => { dealtDamage += data.strengths[statusName]; } - }); + let Attacking = { + "HandleAttackEffects": (_, attackData) => { dealtDamage += attackData.Damage[statusName]; } + }; + Engine.RegisterGlobal("Attacking", Attacking); // damage scheduled: 0, 10, 20 sec - cmpStatusReceiver.InflictEffects({ - [statusName]: { - "Duration": 20000, - "Interval": 10000, - "Damage": 1 - } + cmpStatusReceiver.AddStatus(statusName, { + "Duration": 20000, + "Interval": 10000, + "Damage": 1 }); cmpTimer.OnUpdate({ "turnLength": 1 }); @@ -54,15 +52,16 @@ testInflictEffects(); function testMultipleEffects() { setup(); - AddMock(SYSTEM_ENTITY, IID_Damage, { - "CauseDamage": (data) => { - if (data.strengths.Burn) dealtDamage += data.strengths.Burn; - if (data.strengths.Poison) dealtDamage += data.strengths.Poison; - } - }); + let Attacking = { + "HandleAttackEffects": (_, attackData) => { + if (attackData.Damage.Burn) dealtDamage += attackData.Damage.Burn; + if (attackData.Damage.Poison) dealtDamage += attackData.Damage.Poison; + }, + }; + Engine.RegisterGlobal("Attacking", Attacking); // damage scheduled: 0, 1, 2, 10 sec - cmpStatusReceiver.InflictEffects({ + cmpStatusReceiver.GiveStatus({ "Burn": { "Duration": 20000, "Interval": 10000, @@ -90,30 +89,29 @@ function testMultipleEffects() testMultipleEffects(); -function testRemoveEffect() +function testRemoveStatus() { setup(); let statusName = "Poison"; - AddMock(SYSTEM_ENTITY, IID_Damage, { - "CauseDamage": (data) => { dealtDamage += data.strengths[statusName]; } - }); + let Attacking = { + "HandleAttackEffects": (_, attackData) => { dealtDamage += attackData.Damage[statusName]; } + }; + Engine.RegisterGlobal("Attacking", Attacking); // damage scheduled: 0, 10, 20 sec - cmpStatusReceiver.InflictEffects({ - [statusName]: { - "Duration": 20000, - "Interval": 10000, - "Damage": 1 - } + cmpStatusReceiver.AddStatus(statusName, { + "Duration": 20000, + "Interval": 10000, + "Damage": 1 }); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec - cmpStatusReceiver.RemoveEffect(statusName); + cmpStatusReceiver.RemoveStatus(statusName); cmpTimer.OnUpdate({ "turnLength": 10 }); TS_ASSERT_EQUALS(dealtDamage, 1); // 11 sec } -testRemoveEffect(); +testRemoveStatus(); diff --git a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js index 474648616b..75a63928a6 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -5,7 +5,7 @@ Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/BuildingAI.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); -Engine.LoadComponentScript("interfaces/DamageReceiver.js"); +Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); diff --git a/binaries/data/mods/public/simulation/helpers/Attacking.js b/binaries/data/mods/public/simulation/helpers/Attacking.js new file mode 100644 index 0000000000..0fc99b68bd --- /dev/null +++ b/binaries/data/mods/public/simulation/helpers/Attacking.js @@ -0,0 +1,308 @@ +/** + * Provides attack and damage-related helpers under the Attacking umbrella (to avoid name ambiguity with the component). + */ +function Attacking() {} + +/** + * Builds a RelaxRNG schema of possible attack effects. + * Currently harcoded to "Damage", "Capture" and "StatusEffects". + * Attacks may also have a "Bonuses" element. + * + * @return {string} - RelaxNG schema string + */ +Attacking.prototype.BuildAttackEffectsSchema = function() +{ + return "" + + "" + + "" + + "" + + "" + + "" + + "" + + // Armour requires Foundation to not be a damage type. + "Foundation" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; +}; + +/** + * Returns a template-like object of attack effects. + */ +Attacking.prototype.GetAttackEffectsData = function(valueModifRoot, template, entity) +{ + let ret = {}; + + if (template.Damage) + { + ret.Damage = {}; + let applyMods = damageType => + ApplyValueModificationsToEntity(valueModifRoot + "/Damage/" + damageType, +(template.Damage[damageType] || 0), entity); + for (let damageType in template.Damage) + ret.Damage[damageType] = applyMods(damageType); + } + if (template.Capture) + ret.Capture = ApplyValueModificationsToEntity(valueModifRoot + "/Capture", +(template.Capture || 0), entity); + + if (template.StatusEffects) + ret.StatusEffects = template.StatusEffects; + + if (template.Bonuses) + ret.Bonuses = template.Bonuses; + + return ret; +}; + +Attacking.prototype.GetTotalAttackEffects = function(effectData, effectType, cmpResistance) +{ + let total = 0; + let armourStrengths = cmpResistance ? cmpResistance.GetArmourStrengths(effectType) : {}; + + for (let type in effectData) + total += effectData[type] * Math.pow(0.9, armourStrengths[type] || 0); + + return total; +}; + +/** + * Gives the position of the given entity, taking the lateness into account. + * @param {number} ent - Entity id of the entity we are finding the location for. + * @param {number} lateness - The time passed since the expected time to fire the function. + * @return {Vector3D} The location of the entity. + */ +Attacking.prototype.InterpolatedLocation = function(ent, lateness) +{ + let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); + if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly + return undefined; + let curPos = cmpTargetPosition.GetPosition(); + let prevPos = cmpTargetPosition.GetPreviousPosition(); + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + let turnLength = cmpTimer.GetLatestTurnLength(); + return new Vector3D( + (curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength, + 0, + (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength + ); +}; + +/** + * Test if a point is inside of an entity's footprint. + * @param {number} ent - Id of the entity we are checking with. + * @param {Vector3D} point - The point we are checking with. + * @param {number} lateness - The time passed since the expected time to fire the function. + * @return {boolean} True if the point is inside of the entity's footprint. + */ +Attacking.prototype.TestCollision = function(ent, point, lateness) +{ + let targetPosition = this.InterpolatedLocation(ent, lateness); + if (!targetPosition) + return false; + + let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint); + if (!cmpFootprint) + return false; + + let targetShape = cmpFootprint.GetShape(); + + if (!targetShape) + return false; + + if (targetShape.type == "circle") + return targetPosition.horizDistanceToSquared(point) < targetShape.radius * targetShape.radius; + + if (targetShape.type == "square") + { + let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y; + let distance = Vector2D.from3D(Vector3D.sub(point, targetPosition)).rotate(-angle); + return Math.abs(distance.x) < targetShape.width / 2 && Math.abs(distance.y) < targetShape.depth / 2; + } + + warn("TestCollision called with an invalid footprint shape"); + return false; +}; + +/** + * Get the list of players affected by the damage. + * @param {number} attackerOwner - The player id of the attacker. + * @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged. + * @return {number[]} The ids of players need to be damaged. + */ +Attacking.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire) +{ + if (!friendlyFire) + return QueryPlayerIDInterface(attackerOwner).GetEnemies(); + + return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); +}; + +/** + * Damages units around a given origin. + * @param {Object} data - The data sent by the caller. + * @param {string} data.type - The type of damage. + * @param {Object} data.attackData - The attack data. + * @param {number} data.attacker - The entity id of the attacker. + * @param {number} data.attackerOwner - The player id of the attacker. + * @param {Vector2D} data.origin - The origin of the projectile hit. + * @param {number} data.radius - The radius of the splash damage. + * @param {string} data.shape - The shape of the radius. + * @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage. + * @param {number[]} data.playersToDamage - The array of player id's to damage. + */ +Attacking.prototype.CauseDamageOverArea = function(data) +{ + // Get nearby entities and define variables + let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage); + let damageMultiplier = 1; + + // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin. + for (let ent of nearEnts) + { + let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D(); + if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction + damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / (data.radius * data.radius); + else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles) + { + // Get position of entity relative to splash origin. + let relativePos = entityPosition.sub(data.origin); + + // Get the position relative to the missile direction. + let direction = Vector2D.from3D(data.direction); + let parallelPos = relativePos.dot(direction); + let perpPos = relativePos.cross(direction); + + // The width of linear splash is one fifth of the normal splash radius. + let width = data.radius / 5; + + // Check that the unit is within the distance splash width of the line starting at the missile's + // landing point which extends in the direction of the missile for length splash radius. + if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions + damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) * + (1 - perpPos * perpPos / (width * width)); + else + damageMultiplier = 0; + } + else // In case someone calls this function with an invalid shape. + { + warn("The " + data.shape + " splash damage shape is not implemented!"); + } + + this.HandleAttackEffects(data.type + ".Splash", data.attackData, ent, data.attacker, data.attackerOwner, damageMultiplier); + } +}; + +Attacking.prototype.HandleAttackEffects = function(attackType, attackData, target, attacker, attackerOwner, bonusMultiplier = 1) +{ + let targetState = {}; + for (let effectType of g_EffectTypes) + { + if (!attackData[effectType]) + continue; + + bonusMultiplier *= !attackData.Bonuses ? 1 : GetAttackBonus(attacker, target, attackType, attackData.Bonuses); + + let receiver = g_EffectReceiver[effectType]; + let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]); + if (!cmpReceiver) + return; + + Object.assign(targetState, cmpReceiver[receiver.method](attackData[effectType], attacker, attackerOwner, bonusMultiplier)); + } + + let cmpPromotion = Engine.QueryInterface(attacker, IID_Promotion); + if (cmpPromotion && targetState.xp) + cmpPromotion.IncreaseXp(targetState.xp); + + if (targetState.killed) + this.TargetKilled(attacker, target, attackerOwner); + + Engine.PostMessage(target, MT_Attacked, { + "type": attackType, + "target": target, + "attacker": attacker, + "attackerOwner": attackerOwner, + "damage": -(targetState.HPchange || 0), + "capture": targetState.captureChange || 0, + "statusEffects": targetState.inflictedStatuses || [], + }); +}; + +/** + * Gets entities near a give point for given players. + * @param {Vector2D} origin - The point to check around. + * @param {number} radius - The radius around the point to check. + * @param {number[]} players - The players of which we need to check entities. + * @return {number[]} The id's of the entities in range of the given point. + */ +Attacking.prototype.EntitiesNearPoint = function(origin, radius, players) +{ + // If there is insufficient data return an empty array. + if (!origin || !radius || !players) + return []; + + return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, 0, radius, players, IID_Resistance); +}; + +/** + * Called when a unit kills something (another unit, building, animal etc). + * @param {number} attacker - The entity id of the killer. + * @param {number} target - The entity id of the target. + * @param {number} attackerOwner - The player id of the attacker. + */ +Attacking.prototype.TargetKilled = function(attacker, target, attackerOwner) +{ + let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership); + let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner; + + // Add to killer statistics. + let cmpKillerPlayerStatisticsTracker = QueryPlayerIDInterface(atkOwner, IID_StatisticsTracker); + if (cmpKillerPlayerStatisticsTracker) + cmpKillerPlayerStatisticsTracker.KilledEntity(target); + // Add to loser statistics. + let cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(target, IID_StatisticsTracker); + if (cmpTargetPlayerStatisticsTracker) + cmpTargetPlayerStatisticsTracker.LostEntity(target); + + // If killer can collect loot, let's try to collect it. + let cmpLooter = Engine.QueryInterface(attacker, IID_Looter); + if (cmpLooter) + cmpLooter.Collect(target); +}; + +var AttackingInstance = new Attacking(); +Engine.RegisterGlobal("Attacking", AttackingInstance); diff --git a/binaries/data/mods/public/simulation/helpers/DamageBonus.js b/binaries/data/mods/public/simulation/helpers/DamageBonus.js index e3c88bc0ae..b44c14d44a 100644 --- a/binaries/data/mods/public/simulation/helpers/DamageBonus.js +++ b/binaries/data/mods/public/simulation/helpers/DamageBonus.js @@ -6,7 +6,7 @@ * @param {Object} template - The bonus' template. * @return {number} - The source entity's attack bonus against the specified target. */ -function GetDamageBonus(source, target, type, template) +function GetAttackBonus(source, target, type, template) { let attackBonus = 1; @@ -28,4 +28,4 @@ function GetDamageBonus(source, target, type, template) return attackBonus; } -Engine.RegisterGlobal("GetDamageBonus", GetDamageBonus); +Engine.RegisterGlobal("GetAttackBonus", GetAttackBonus); diff --git a/binaries/data/mods/public/simulation/helpers/DamageTypes.js b/binaries/data/mods/public/simulation/helpers/DamageTypes.js deleted file mode 100644 index 891c94efaf..0000000000 --- a/binaries/data/mods/public/simulation/helpers/DamageTypes.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Builds a RelaxRNG schema based on currently valid elements. - * - * To prevent validation errors, disabled damage types are included in the schema. - * - * @param {string} helptext - Text displayed as help - * @return {string} - RelaxNG schema string - */ -function BuildDamageTypesSchema(helptext = "") -{ - return "" + - "" + - "" + - // Armour requires Foundation to not be a damage type. - "Foundation" + - "" + - "" + - "" + - ""; -} - -Engine.RegisterGlobal("BuildDamageTypesSchema", BuildDamageTypesSchema); diff --git a/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml b/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml index 4d32a8c6e4..4e3bf67f46 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml @@ -7,7 +7,7 @@ - 2 + 2 4 1000 Field Palisade SiegeWall StoneWall diff --git a/binaries/data/mods/public/simulation/templates/template_unit_champion.xml b/binaries/data/mods/public/simulation/templates/template_unit_champion.xml index 382df43995..3e29e7c8a5 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit_champion.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit_champion.xml @@ -2,7 +2,7 @@ - 5 + 5 4 1000 Field Palisade SiegeWall StoneWall diff --git a/binaries/data/mods/public/simulation/templates/template_unit_hero.xml b/binaries/data/mods/public/simulation/templates/template_unit_hero.xml index fe070b4d5b..0fc4a77d93 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit_hero.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit_hero.xml @@ -7,7 +7,7 @@ - 15 + 15 4 1000 Field Palisade SiegeWall StoneWall diff --git a/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml b/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml index 20ff74dcd0..a8aebbb6d9 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml @@ -7,7 +7,7 @@ - 2 + 2 4 1000 Field Palisade SiegeWall StoneWall