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,9 @@ */ Attack.prototype.PerformAttack = function(type, target) { + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return; let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); let data = { @@ -510,6 +513,8 @@ "target": target, "attacker": this.entity, "attackerOwner": attackerOwner, + "attackerPosition": cmpPosition.GetPosition(), + "attackHeightOffset": this.GetRange(type).elevationBonus }; // If this is a ranged attack, then launch a projectile @@ -528,10 +533,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; @@ -539,7 +540,7 @@ let targetVelocity = Vector3D.sub(targetPosition, cmpTargetPosition.GetPreviousPosition()).div(turnLength); - let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); + let timeToTarget = PositionHelper.PredictTimeToTarget(data.attackerPosition, horizSpeed, targetPosition, targetVelocity); // 'Cheat' and use UnitMotion to predict the position in the near-future. // This avoids 'dancing' issues with units zigzagging over very short distances. @@ -572,7 +573,7 @@ // Add inaccuracy based on spread. let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template[type].Projectile.Spread, this.entity) * - predictedPosition.horizDistanceTo(selfPosition) / 100; + predictedPosition.horizDistanceTo(data.attackerPosition) / 100; let randNorm = randomNormal2D(); let offsetX = randNorm[0] * distanceModifiedSpread; @@ -581,10 +582,10 @@ let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, predictedHeight, predictedPosition.z + offsetZ); // Recalculate when the missile will hit the target position. - let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition); + let realHorizDistance = realTargetPosition.horizDistanceTo(data.attackerPosition); timeToTarget = realHorizDistance / horizSpeed; - let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance); + let missileDirection = Vector3D.sub(realTargetPosition, data.attackerPosition).div(realHorizDistance); // Launch the graphical projectile. let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); @@ -599,7 +600,7 @@ // TODO: Use unit rotation to implement x/z offsets. let deltaLaunchPoint = new Vector3D(0, +this.template[type].Projectile.LaunchPoint["@y"], 0); - let launchPoint = Vector3D.add(selfPosition, deltaLaunchPoint); + let launchPoint = Vector3D.add(data.attackerPosition, deltaLaunchPoint); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) 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, 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 @@ -72,6 +72,8 @@ "target": target, "attacker": attacker, "attackerOwner": attackerOwner, + "attackerPosition": targetPos, + "attackHeightOffset": 0, "position": targetPos, "projectileId": 9, "direction": new Vector3D(1, 0, 0) @@ -185,6 +187,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 +226,20 @@ AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, -0.5), + "GetPosition": () => new Vector3D(3, 0, -0.5), + "IsInWorld": () => true }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), + "GetPosition": () => new Vector3D(0, 0, 0), + "IsInWorld": () => true }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(5, 2), + "GetPosition": () => new Vector3D(5, 0, 2), + "IsInWorld": () => true }); AddMock(60, IID_Health, { @@ -321,28 +331,40 @@ AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, 4), + "GetPosition": () => new Vector3D(3, 0, 4), + "IsInWorld": () => true }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), + "GetPosition": () => new Vector3D(0, 0, 0), + "IsInWorld": () => true }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(3.6, 3.2), + "GetPosition": () => new Vector3D(3.6, 0, 3.2), + "IsInWorld": () => true }); AddMock(63, IID_Position, { "GetPosition2D": () => new Vector2D(10, -10), + "GetPosition": () => new Vector3D(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 Vector3D(9, 0, -4), + "IsInWorld": () => true }); // Big target far away (see distance above). AddMock(65, IID_Position, { "GetPosition2D": () => new Vector2D(23, 4), + "GetPosition": () => new Vector3D(23, 0, 4), + "IsInWorld": () => true }); AddMock(60, IID_Health, { @@ -393,6 +415,8 @@ "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": 50, "attackerOwner": attackerOwner, + "attackerPosition": new Vector3D(0, 0, 0), + "attackHeightOffset": 0, "origin": new Vector2D(3, 4), "radius": radius, "shape": "Circular", @@ -429,6 +453,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 @@ -156,12 +156,15 @@ * @param {number} target - The target of the attack. * @param {Object} effectData - The effects calculate the effect for. * @param {string} effectType - The type of effect to apply (e.g. Damage, Capture or ApplyStatus). + * @param {Vector3D} attackerPosition - The position of the attacker when performing the attack. + * @param {number} attackHeightOffset - The offset of the origin of the attack, + * relative to the ground. * @param {number} bonusMultiplier - The factor to multiply the total effect with. * @param {Object} cmpResistance - Optionally the resistance component of the target. * * @return {number} - The total value of the effect. */ -Attacking.prototype.GetTotalAttackEffects = function(target, effectData, effectType, bonusMultiplier, cmpResistance) +Attacking.prototype.GetTotalAttackEffects = function(target, effectData, effectType, attackerPosition, attackHeightOffset, bonusMultiplier, cmpResistance) { let total = 0; if (!cmpResistance) @@ -170,8 +173,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); @@ -230,6 +236,9 @@ * @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 {Vector3D} data.attackerPosition - The position of the attacker when the attack was initiated. + * @param {number} data.attackHeightOffset - The offset of the origin of the attack, + * relative to the ground. * @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. @@ -296,6 +305,9 @@ * @param {Object} data.effectData - The effects use. * @param {number} data.attacker - The entityID that attacked us. * @param {number} data.attackerOwner - The playerID that owned the attacker when the attack was performed. + * @param {Vector3D} data.attackerPosition - The position of the attacker when the attack was initiated. + * @param {number} data.attackHeightOffset - The offset of the origin of the attack, + * relative to the ground. * @param {number} bonusMultiplier - The factor to multiply the total effect with, defaults to 1. * * @return {boolean} - Whether we handled the attack. @@ -318,7 +330,19 @@ if (!cmpReceiver) continue; - Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, data.attackData, receiver.type, bonusMultiplier, cmpResistance), data.attacker, data.attackerOwner)); + Object.assign(targetState, + cmpReceiver[receiver.method]( + this.GetTotalAttackEffects(target, + data.attackData, + receiver.type, + data.attackerPosition, + data.attackHeightOffset, + bonusMultiplier, + cmpResistance + ), + data.attacker, + data.attackerOwner + )); } if (!Object.keys(targetState).length) @@ -377,6 +401,28 @@ return attackBonus; }; +/** + * Calculate the damage bonus for entities with a height difference. + * For an explanation of the numbers, see Phab: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 attack, + * relative to the ground. + * + * @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);