Index: ps/trunk/binaries/data/mods/public/globalscripts/AttackEffects.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/AttackEffects.js +++ ps/trunk/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" + } +}; Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js @@ -170,6 +170,22 @@ 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 @@ 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 @@ { 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) Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js +++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js @@ -270,8 +270,8 @@ "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 @@ 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 @@ 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) Index: ps/trunk/binaries/data/mods/public/maps/random/polar_sea_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/polar_sea_triggers.js +++ ps/trunk/binaries/data/mods/public/maps/random/polar_sea_triggers.js @@ -37,7 +37,6 @@ 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 @@ 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); }); Index: ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js +++ ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js @@ -152,7 +152,7 @@ { 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); } }; Index: ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js +++ ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js @@ -20,8 +20,8 @@ { 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()); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -246,7 +246,7 @@ if (!this.get("Attack/Capture")) return undefined; - return +this.get("Attack/Capture/Value") || 0; + return +this.get("Attack/Capture/Capture") || 0; }, "attackTimes": function(type) { Index: ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js +++ ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js @@ -260,9 +260,9 @@ 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) Index: ps/trunk/binaries/data/mods/public/simulation/components/Armour.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Armour.js +++ ps/trunk/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 @@ "0.0" + "5.0" + "" + - BuildDamageTypesSchema("damage protection") + + Armour.prototype.DamageResistanceSchema + "" + "" + - "" + - BuildDamageTypesSchema("damage protection") + - "" + + Armour.prototype.DamageResistanceSchema + "" + ""; @@ -32,32 +40,7 @@ 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 @@ 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 @@ return ret; }; -Engine.RegisterComponentType(IID_DamageReceiver, "Armour", Armour); +Engine.RegisterComponentType(IID_Resistance, "Armour", Armour); Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js @@ -2,40 +2,6 @@ var g_AttackTypes = ["Melee", "Ranged", "Capture"]; -Attack.prototype.statusEffectsSchema = - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - ""; - -Attack.prototype.bonusesSchema = - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - ""; - Attack.prototype.preferredClassesSchema = "" + "" + @@ -130,9 +96,7 @@ "" + "" + "" + - "" + - BuildDamageTypesSchema("damage strength") + - "" + + Attacking.BuildAttackEffectsSchema() + "" + "" + "" + @@ -140,7 +104,6 @@ "" + // TODO: it shouldn't be stretched "" + "" + - Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + @@ -149,9 +112,7 @@ "" + "" + "" + - "" + - BuildDamageTypesSchema("damage strength") + - "" + + Attacking.BuildAttackEffectsSchema() + "" + "" + ""+ @@ -179,10 +140,7 @@ "" + "" + "" + - "" + - BuildDamageTypesSchema("damage strength") + - "" + - Attack.prototype.bonusesSchema + + Attacking.BuildAttackEffectsSchema() + "" + "" + "" + @@ -217,8 +175,6 @@ "" + "" + "" + - Attack.prototype.statusEffectsSchema + - Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + @@ -227,12 +183,11 @@ "" + "" + "" + - "" + + Attacking.BuildAttackEffectsSchema() + "" + "" + // TODO: it shouldn't be stretched "" + "" + - Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + @@ -241,11 +196,8 @@ "" + "" + "" + - "" + - BuildDamageTypesSchema("damage strength") + - "" + + Attacking.BuildAttackEffectsSchema() + "" + // TODO: how do these work? - Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + @@ -387,6 +339,14 @@ 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 @@ 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 @@ 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.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 @@ 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 @@ // 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 @@ 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); }; /** Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js @@ -131,7 +131,7 @@ 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); }; Index: ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js @@ -48,6 +48,24 @@ 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 Index: ps/trunk/binaries/data/mods/public/simulation/components/Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Damage.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Damage.js @@ -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); Index: ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js +++ ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js @@ -33,10 +33,7 @@ "" + "" + "" + - "" + - BuildDamageTypesSchema("damage strength") + - "" + - DeathDamage.prototype.bonusesSchema; + Attacking.BuildAttackEffectsSchema(); DeathDamage.prototype.Init = function() { @@ -44,22 +41,9 @@ 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 @@ 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 }); }; Index: ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js +++ ps/trunk/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); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -389,12 +389,12 @@ 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 @@ } } - 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) Index: ps/trunk/binaries/data/mods/public/simulation/components/Health.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Health.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Health.js @@ -178,8 +178,33 @@ }; /** + * 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 @@ // 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 @@ 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 @@ this.hitpoints -= amount; this.RegisterHealthChanged(oldHitpoints); - return { "killed": false, "change": this.hitpoints - oldHitpoints }; + return { "killed": false, "HPchange": this.hitpoints - oldHitpoints }; }; /** Index: ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js +++ ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js @@ -5,13 +5,18 @@ 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 @@ 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 @@ 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); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js @@ -2958,8 +2958,8 @@ "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 @@ "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 @@ 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); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Damage.js +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Damage.js @@ -1 +0,0 @@ -Engine.RegisterInterface("Damage"); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DamageReceiver.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DamageReceiver.js +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DamageReceiver.js @@ -1,6 +0,0 @@ -Engine.RegisterInterface("DamageReceiver"); - -/** - * Message of the form { "entity": entity, "invulnerability": true/false } - */ -Engine.RegisterMessageType("InvulnerabilityChanged"); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js @@ -0,0 +1 @@ +Engine.RegisterInterface("DelayedDamage"); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Resistance.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Resistance.js +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Resistance.js @@ -0,0 +1,6 @@ +Engine.RegisterInterface("Resistance"); + +/** + * Message of the form { "entity": entity, "invulnerability": true/false } + */ +Engine.RegisterMessageType("InvulnerabilityChanged"); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ ps/trunk/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 @@ } }, "Capture": { - "Value": 8, + "Capture": 8, "MaxRange": 10, }, "Slaughter": {} @@ -150,18 +150,28 @@ 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 @@ }); 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 @@ 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); }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ ps/trunk/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/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 @@ 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 @@ "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 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 @@ 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 @@ 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 @@ "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 @@ 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 @@ 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 @@ "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 @@ 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 @@ 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 @@ "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 @@ "GetEnemies": () => [2] }); - cmpDamage.MissileHit(data, 0); + cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(60)); hitEnts.clear(); @@ -413,10 +415,10 @@ "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 @@ "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 @@ "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 @@ "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 @@ "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 @@ "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; Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js +++ ps/trunk/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 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 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 @@ "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(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -6,7 +6,7 @@ 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"); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js @@ -64,7 +64,7 @@ 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(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 @@ // 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 @@ // 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); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js +++ ps/trunk/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 @@ { 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 @@ 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 @@ 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(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -5,7 +5,7 @@ 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"); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js +++ ps/trunk/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); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js +++ ps/trunk/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 @@ return attackBonus; } -Engine.RegisterGlobal("GetDamageBonus", GetDamageBonus); +Engine.RegisterGlobal("GetAttackBonus", GetAttackBonus); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/DamageTypes.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/DamageTypes.js +++ ps/trunk/binaries/data/mods/public/simulation/helpers/DamageTypes.js @@ -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); Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml @@ -7,7 +7,7 @@ - 2 + 2 4 1000 Field Palisade SiegeWall StoneWall Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml @@ -2,7 +2,7 @@ - 5 + 5 4 1000 Field Palisade SiegeWall StoneWall Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml @@ -7,7 +7,7 @@ - 15 + 15 4 1000 Field Palisade SiegeWall StoneWall Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml @@ -7,7 +7,7 @@ - 2 + 2 4 1000 Field Palisade SiegeWall StoneWall