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 @@ -445,7 +445,17 @@ */ Attack.prototype.PerformAttack = function(type, target) { - let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return; + let selfPosition = cmpPosition.GetPosition(); + + let attackerData = { + "owner": Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(), + "entity": this.entity, + "position": selfPosition, + "elevationBonus": this.GetRange(type).elevationBonus + }; // If this is a ranged attack, then launch a projectile if (type == "Ranged") @@ -460,10 +470,6 @@ let gravity = +this.template[type].Projectile.Gravity; // horizSpeed /= 2; gravity /= 2; // slow it down for testing - 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; @@ -530,8 +536,7 @@ "type": type, "attackData": this.GetAttackEffectsData(type), "target": target, - "attacker": this.entity, - "attackerOwner": attackerOwner, + "attackerData": attackerData, "position": realTargetPosition, "direction": missileDirection, "projectileId": id, @@ -547,7 +552,7 @@ cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", +this.template[type].Delay + timeToTarget * 1000, data); } else - Attacking.HandleAttackEffects(type, this.GetAttackEffectsData(type), target, this.entity, attackerOwner); + Attacking.HandleAttackEffects(type, this.GetAttackEffectsData(type), target, attackerData); }; /** 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 @@ -41,13 +41,12 @@ Attacking.CauseDamageOverArea({ "type": data.type, "attackData": data.splash.attackData, - "attacker": data.attacker, - "attackerOwner": data.attackerOwner, + "attackerData": data.attackerData, "origin": Vector2D.from3D(data.position), "radius": data.splash.radius, "shape": data.splash.shape, "direction": data.direction, - "playersToDamage": Attacking.GetPlayersToDamage(data.attackerOwner, data.splash.friendlyFire) + "playersToDamage": Attacking.GetPlayersToDamage(data.attackerData.owner, data.splash.friendlyFire) }); } @@ -59,7 +58,7 @@ { cmpProjectileManager.RemoveProjectile(data.projectileId); - Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); + Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attackerData); return; } @@ -68,14 +67,14 @@ return; // If we didn't hit the main target look for nearby units - let cmpPlayer = QueryPlayerIDInterface(data.attackerOwner); + let cmpPlayer = QueryPlayerIDInterface(data.attackerData.owner); 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); + Attacking.HandleAttackEffects(data.type, data.attackData, ent, data.attackerData); cmpProjectileManager.RemoveProjectile(data.projectileId); break; 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 @@ -60,8 +60,12 @@ "Damage": { "Hack": 0, "Pierce": 0, "Crush": damage }, }, "target": target, - "attacker": attacker, - "attackerOwner": attackerOwner, + "attackerData": { + "entity": attacker, + "owner": attackerOwner, + "position": new Vector3D(3, 0, 3), + "elevationBonus": 0 + }, "position": targetPos, "projectileId": 9, "direction": new Vector3D(1, 0, 0) @@ -127,12 +131,12 @@ damageTaken = false; } - Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); + Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attackerData); TestDamage(); data.type = "Ranged"; type = data.type; - Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); + Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attackerData); TestDamage(); // Check for damage still being dealt if the attacker dies @@ -163,8 +167,12 @@ let data = { "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, - "attacker": attacker, - "attackerOwner": attackerOwner, + "attackerData": { + "entity": attacker, + "owner": attackerOwner, + "position": new Vector3D(3, 0, 3), + "elevationBonus": 0 + }, "origin": origin, "radius": 10, "shape": "Linear", @@ -185,14 +193,20 @@ AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(2.2, -0.4), + "GetPosition": () => new Vector2D(2.2, 0, -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(5, 2), + "GetPosition": () => new Vector2D(5, 0, 2), + "IsInWorld": () => true }); AddMock(60, IID_Health, { @@ -262,23 +276,33 @@ 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 AddMock(64, IID_Position, { "GetPosition2D": () => new Vector2D(9, -4), + "GetPosition": () => new Vector2D(9, 0, -4), + "IsInWorld": () => true }); AddMock(60, IID_Resistance, { @@ -318,8 +342,12 @@ Attacking.CauseDamageOverArea({ "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, - "attacker": 50, - "attackerOwner": 1, + "attackerData": { + "entity": 50, + "owner": 1, + "position": new Vector3D(3, 0, 3), + "elevationBonus": 0 + }, "origin": new Vector2D(3, 4), "radius": radius, "shape": "Circular", @@ -351,8 +379,12 @@ "type": "Ranged", "attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } }, "target": 60, - "attacker": 70, - "attackerOwner": 1, + "attackerData": { + "entity": 70, + "owner": 1, + "position": new Vector3D(3, 10, 3), + "elevationBonus": 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 @@ -222,11 +222,11 @@ warn("The " + data.shape + " splash damage shape is not implemented!"); } - this.HandleAttackEffects(data.type + ".Splash", data.attackData, ent, data.attacker, data.attackerOwner, damageMultiplier); + this.HandleAttackEffects(data.type + ".Splash", data.attackData, ent, data.attackerData, damageMultiplier); } }; -Attacking.prototype.HandleAttackEffects = function(attackType, attackData, target, attacker, attackerOwner, bonusMultiplier = 1) +Attacking.prototype.HandleAttackEffects = function(attackType, attackData, target, attackerData, bonusMultiplier = 1) { let targetState = {}; for (let effectType of g_EffectTypes) @@ -234,28 +234,31 @@ if (!attackData[effectType]) continue; - bonusMultiplier *= !attackData.Bonuses ? 1 : GetAttackBonus(attacker, target, attackType, attackData.Bonuses); + if (effectType == "Damage") + bonusMultiplier *= this.GetElevationDamageBonus(target, attackerData); + + bonusMultiplier *= !attackData.Bonuses ? 1 : GetAttackBonus(attackerData.entity, target, attackType, attackData.Bonuses); let receiver = g_EffectReceiver[effectType]; let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]); if (!cmpReceiver) continue; - Object.assign(targetState, cmpReceiver[receiver.method](attackData[effectType], attacker, attackerOwner, bonusMultiplier)); + Object.assign(targetState, cmpReceiver[receiver.method](attackData[effectType], attackerData.entity, attackerData.owner, bonusMultiplier)); } - let cmpPromotion = Engine.QueryInterface(attacker, IID_Promotion); + let cmpPromotion = Engine.QueryInterface(attackerData.entity, IID_Promotion); if (cmpPromotion && targetState.xp) cmpPromotion.IncreaseXp(targetState.xp); if (targetState.killed) - this.TargetKilled(attacker, target, attackerOwner); + this.TargetKilled(attackerData.entity, target, attackerData.owner); Engine.PostMessage(target, MT_Attacked, { "type": attackType, "target": target, - "attacker": attacker, - "attackerOwner": attackerOwner, + "attacker": attackerData.entity, + "attackerOwner": attackerData.owner, "damage": -(targetState.HPchange || 0), "capture": targetState.captureChange || 0, "statusEffects": targetState.inflictedStatuses || [], @@ -279,6 +282,24 @@ }; /** + * Calculate the damage bonus for entities with a height difference. + * For an explanation of the numbers, see D781. + * @param {string} attackType - The attack type, needed for the initial height of the attack. + * @param {number} elevationDifference - The height difference between the source of attack and the target. + * @return {number} - The factor that the damage is multiplied with. + */ +Attacking.prototype.GetElevationDamageBonus = function(target, attackerData) +{ + let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); + if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) + return 1; + let targetPosition = cmpTargetPosition.GetPosition(); + let elevationDifference = (attackerData.position.y + attackerData.elevationBonus) - targetPosition.y; + + return Math.max(0.1, 1 + 0.01 * (elevationDifference)); +}; + +/** * 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.