Index: binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/Attack.js +++ binaries/data/mods/public/simulation/components/Attack.js @@ -502,6 +502,10 @@ */ Attack.prototype.PerformAttack = function(type, target) { + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return; + let selfPosition = cmpPosition.GetPosition(); let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); // If this is a ranged attack, then launch a projectile @@ -520,10 +524,6 @@ // We will try to estimate the position of the target, where we can hit it. // We first estimate the time-till-hit by extrapolating linearly the movement // of the last turn. We compute the time till an arrow will intersect the target. - let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); - if (!cmpPosition || !cmpPosition.IsInWorld()) - return; - let selfPosition = cmpPosition.GetPosition(); let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; @@ -619,6 +619,8 @@ "target": target, "attacker": this.entity, "attackerOwner": attackerOwner, + "attackerPosition": selfPosition, + "attackHeightOffset": this.GetRange(type).elevationBonus, "position": realTargetPosition, "direction": missileDirection, "projectileId": id, @@ -630,7 +632,7 @@ cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", +this.template[type].Delay + timeToTarget * 1000, data); } else - Attacking.HandleAttackEffects(target, type, this.GetAttackEffectsData(type), this.entity, attackerOwner); + Attacking.HandleAttackEffects(target, type, this.GetAttackEffectsData(type), this.entity, attackerOwner, selfPosition, this.GetRange(type).elevationBonus); }; Attack.prototype.OnValueModification = function(msg) Index: binaries/data/mods/public/simulation/components/DelayedDamage.js =================================================================== --- binaries/data/mods/public/simulation/components/DelayedDamage.js +++ binaries/data/mods/public/simulation/components/DelayedDamage.js @@ -50,6 +50,8 @@ "attackData": data.splash.attackData, "attacker": data.attacker, "attackerOwner": data.attackerOwner, + "attackerPosition": data.attackerPosition, + "attackHeightOffset": data.attackHeightOffset, "origin": Vector2D.from3D(data.position), "radius": data.splash.radius, "shape": data.splash.shape, @@ -68,7 +70,7 @@ // Deal direct damage if we hit the main target // and we could handle the attack. if (PositionHelper.TestCollision(target, data.position, lateness) && - Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner)) + Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner, data.attackerPosition, data.attackHeightOffset)) { cmpProjectileManager.RemoveProjectile(data.projectileId); return; @@ -81,7 +83,7 @@ for (let ent of ents) { if (!PositionHelper.TestCollision(ent, data.position, lateness) || - !Attacking.HandleAttackEffects(ent, data.type, data.attackData, data.attacker, data.attackerOwner)) + !Attacking.HandleAttackEffects(ent, data.type, data.attackData, data.attacker, data.attackerOwner, data.attackerPosition, data.attackHeightOffset)) continue; cmpProjectileManager.RemoveProjectile(data.projectileId); Index: binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -147,12 +147,12 @@ damageTaken = false; } - Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner); + Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner, new Vector3D(2, 0, 3), 0); TestDamage(); data.type = "Ranged"; type = data.type; - Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner); + Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner, new Vector3D(2, 0, 3), 0); TestDamage(); // Check for damage still being dealt if the attacker dies @@ -185,6 +185,8 @@ "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": attacker, "attackerOwner": attackerOwner, + "attackerPosition": new Vector3D(0, 0, 0), + "attackHeightOffset": 0, "origin": origin, "radius": 10, "shape": "Linear", @@ -222,14 +224,20 @@ AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, -0.5), + "GetPosition": () => new Vector2D(3, 0, -0.5), + "IsInWorld": () => true }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), + "GetPosition": () => new Vector2D(0, 0, 0), + "IsInWorld": () => true }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(5, 2), + "GetPosition": () => new Vector2D(5, 0, 2), + "IsInWorld": () => true }); AddMock(60, IID_Health, { @@ -321,28 +329,40 @@ AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, 4), + "GetPosition": () => new Vector2D(3, 0, 4), + "IsInWorld": () => true }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), + "GetPosition": () => new Vector2D(0, 0, 0), + "IsInWorld": () => true }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(3.6, 3.2), + "GetPosition": () => new Vector2D(3.6, 0, 3.2), + "IsInWorld": () => true }); AddMock(63, IID_Position, { "GetPosition2D": () => new Vector2D(10, -10), + "GetPosition": () => new Vector2D(10, 0, -10), + "IsInWorld": () => true }); // Target on the frontier of the shape (see distance above). AddMock(64, IID_Position, { "GetPosition2D": () => new Vector2D(9, -4), + "GetPosition": () => new Vector2D(9, 0, -4), + "IsInWorld": () => true }); // Big target far away (see distance above). AddMock(65, IID_Position, { "GetPosition2D": () => new Vector2D(23, 4), + "GetPosition": () => new Vector2D(23, 0, 4), + "IsInWorld": () => true }); AddMock(60, IID_Health, { @@ -393,6 +413,8 @@ "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": 50, "attackerOwner": attackerOwner, + "attackerPosition": new Vector2D(0, 0, 0), + "attackHeightOffset": 0, "origin": new Vector2D(3, 4), "radius": radius, "shape": "Circular", @@ -429,6 +451,8 @@ "target": 60, "attacker": 70, "attackerOwner": 1, + "attackerPosition": new Vector3D(0, 10, 0), + "attackHeightOffset": 0, "position": targetPos, "direction": new Vector3D(1, 0, 0), "projectileId": 9, Index: binaries/data/mods/public/simulation/helpers/Attacking.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Attacking.js +++ binaries/data/mods/public/simulation/helpers/Attacking.js @@ -161,7 +161,7 @@ * * @return {number} - The total value of the effect. */ -Attacking.prototype.GetTotalAttackEffects = function(target, effectData, effectType, bonusMultiplier, cmpResistance) +Attacking.prototype.GetTotalAttackEffects = function(target, attackerPosition, attackHeightOffset, effectData, effectType, bonusMultiplier, cmpResistance) { let total = 0; if (!cmpResistance) @@ -170,8 +170,11 @@ let resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {}; if (effectType == "Damage") + { + bonusMultiplier *= this.GetElevationDamageMultiplier(target, attackerPosition, attackHeightOffset); for (let type in effectData.Damage) total += effectData.Damage[type] * Math.pow(0.9, resistanceStrengths.Damage ? resistanceStrengths.Damage[type] || 0 : 0); + } else if (effectType == "Capture") { total = effectData.Capture * Math.pow(0.9, resistanceStrengths.Capture || 0); @@ -283,7 +286,7 @@ // so the multiplier can end up below 0. damageMultiplier = Math.max(0, damageMultiplier); - this.HandleAttackEffects(ent, data.type + ".Splash", data.attackData, data.attacker, data.attackerOwner, damageMultiplier); + this.HandleAttackEffects(ent, data.type + ".Splash", data.attackData, data.attacker, data.attackerOwner, data.attackerPosition, data.attackHeightOffset, damageMultiplier); } }; /** @@ -294,11 +297,13 @@ * @param {Object} effectData - The effects use. * @param {number} attacker - The entityID that attacked us. * @param {number} attackerOwner - The playerID that owned the attacker when the attack was performed. + * @param {Vector3D} attackerPosition - The position of the attacker when they performed the attack, used for elevation damage bonus. + * @param {number} attackHeightOffset - The vertical offset of the attack of the attacker. * @param {number} bonusMultiplier - The factor to multiply the total effect with, defaults to 1. * * @return {boolean} - Whether we handled the attack. */ -Attacking.prototype.HandleAttackEffects = function(target, attackType, attackData, attacker, attackerOwner, bonusMultiplier = 1) +Attacking.prototype.HandleAttackEffects = function(target, attackType, attackData, attacker, attackerOwner, attackerPosition, attackHeightOffset, bonusMultiplier = 1) { let cmpResistance = Engine.QueryInterface(target, IID_Resistance); if (cmpResistance && cmpResistance.IsInvulnerable()) @@ -316,7 +321,7 @@ if (!cmpReceiver) continue; - Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, attackData, receiver.type, bonusMultiplier, cmpResistance), attacker, attackerOwner)); + Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, attackerPosition, attackHeightOffset, attackData, receiver.type, bonusMultiplier, cmpResistance), attacker, attackerOwner)); } if (!Object.keys(targetState).length) @@ -375,6 +380,27 @@ return attackBonus; }; +/** + * Calculate the damage bonus for entities with a height difference. + * For an explanation of the numbers, see D781. + * + * @param {number} target - The entity ID of the target. + * @param {Vector3D} attackerPosition - The original position of the attacker. + * @param {number} attackHeightOffset - The attack height offset of the attacker. + * + * @return {number} - The factor that the damage will be multiplied with. + */ +Attacking.prototype.GetElevationDamageMultiplier = function(target, attackerPosition, attackHeightOffset) +{ + let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); + if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) + return 1; + let targetPosition = cmpTargetPosition.GetPosition(); + let elevationDifference = attackerPosition.y + attackHeightOffset - targetPosition.y; + + return Math.max(0.1, 1 + 0.01 * elevationDifference); +}; + var AttackingInstance = new Attacking(); Engine.RegisterGlobal("Attacking", AttackingInstance); Index: binaries/data/mods/public/simulation/helpers/tests/test_Attacking.js =================================================================== --- binaries/data/mods/public/simulation/helpers/tests/test_Attacking.js +++ binaries/data/mods/public/simulation/helpers/tests/test_Attacking.js @@ -59,7 +59,7 @@ "Capture": x => { this.resultString += x; }, }); - Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER); + Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, new Vector3D(0, 0, 0), 0); TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) !== -1); TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) !== -1); @@ -73,7 +73,7 @@ "Capture": x => { this.resultString += x; }, }); - Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER); + Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, new Vector3D(0, 0, 0), 0); TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) === -1); TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) !== -1); @@ -85,7 +85,7 @@ "GetMaxHitpoints": () => 1, }); - Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER); + Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, new Vector3D(0, 0, 0), 0); TS_ASSERT(this.resultString.indexOf(this.attackData.Damage) !== -1); TS_ASSERT(this.resultString.indexOf(this.attackData.Capture) === -1); } @@ -95,14 +95,14 @@ */ testAttackedMessage() { Engine.PostMessage = () => TS_ASSERT(false); - Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER); + Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, new Vector3D(0, 0, 0), 0); AddMock(this.TESTED_ENTITY_ID, IID_Capturable, { "Capture": () => ({ "captureChange": 0 }), }); let count = 0; Engine.PostMessage = () => count++; - Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER); + Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, new Vector3D(0, 0, 0), 0); TS_ASSERT_EQUALS(count, 1); AddMock(this.TESTED_ENTITY_ID, IID_Health, { @@ -112,7 +112,7 @@ }); count = 0; Engine.PostMessage = () => count++; - Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER); + Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, new Vector3D(0, 0, 0), 0); TS_ASSERT_EQUALS(count, 1); } @@ -127,7 +127,7 @@ }); let spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus"); - Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, 2); + Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, new Vector3D(0, 0, 0), 0, 2); TS_ASSERT_EQUALS(spy._called, 1); } @@ -148,7 +148,7 @@ }, }); - Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, 2); + Attacking.HandleAttackEffects(this.TESTED_ENTITY_ID, "Test", this.attackData, INVALID_ENTITY, INVALID_PLAYER, new Vector3D(0, 0, 0), 0, 2); } }