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 @@ -178,6 +178,8 @@ // Get nearby entities and define variables let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage); let damageMultiplier = 1; + let direction = Vector2D.from3D(data.direction); + // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin. for (let ent of nearEnts) { @@ -189,17 +191,18 @@ // Get position of entity relative to splash origin. let relativePos = entityPosition.sub(data.origin); + // Get the position relative to the missile 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; - // Effectivly rotate the axis to align with the missile direction. - let parallelDist = relativePos.dot(data.direction); // z axis - let perpDist = Math.abs(relativePos.cross(data.direction)); // y axis - - // Check that the unit is within the distance at which it will get damaged. - if (parallelDist > -width && perpDist < width) // If in radius, quadratic falloff in both directions - damageMultiplier = (data.radius * data.radius - parallelDist * parallelDist) / (data.radius * data.radius) * - (width * width - perpDist * perpDist) / (width * width); + // 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; } 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 @@ -17,116 +17,206 @@ Engine.LoadComponentScript("Damage.js"); Engine.LoadComponentScript("Timer.js"); -let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); -let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); -cmpTimer.OnUpdate({ turnLength: 1 }); -let attacker = 11; -let atkPlayerEntity = 1; -let attackerOwner = 6; -let cmpAttack = ConstructComponent(attacker, "Attack", { "Ranged": { "ProjectileSpeed": 500, "Spread": 0.5, "MaxRange": 50, "MinRange": 0 } } ); -let damage = 5; -let target = 21; -let targetOwner = 7; -let targetPos = new Vector3D(3, 0, 3); - -let type = "Melee"; -let damageTaken = false; - -cmpAttack.GetAttackStrengths = (type) => ({ "hack": 0, "pierce": 0, "crush": damage }); -cmpAttack.GetAttackBonus = (type, target) => 1.0; - -let data = { - "attacker": attacker, - "target": target, - "type": "Melee", - "strengths": { "hack": 0, "pierce": 0, "crush": damage }, - "multiplier": 1.0, - "attackerOwner": attackerOwner, - "position": targetPos, - "isSplash": false, - "projectileId": 9 -}; +let Test_Generic = function() +{ + ResetState(); + + let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); + let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); + cmpTimer.OnUpdate({ turnLength: 1 }); + let attacker = 11; + let atkPlayerEntity = 1; + let attackerOwner = 6; + let cmpAttack = ConstructComponent(attacker, "Attack", { "Ranged": { "ProjectileSpeed": 500, "Spread": 0.5, "MaxRange": 50, "MinRange": 0 } } ); + let damage = 5; + let target = 21; + let targetOwner = 7; + let targetPos = new Vector3D(3, 0, 3); + + let type = "Melee"; + let damageTaken = false; + + cmpAttack.GetAttackStrengths = (type) => ({ "hack": 0, "pierce": 0, "crush": damage }); + cmpAttack.GetAttackBonus = (type, target) => 1.0; + + let data = { + "attacker": attacker, + "target": target, + "type": "Melee", + "strengths": { "hack": 0, "pierce": 0, "crush": damage }, + "multiplier": 1.0, + "attackerOwner": attackerOwner, + "position": targetPos, + "isSplash": false, + "projectileId": 9, + "direction": new Vector3D(1,0,0) + }; + + AddMock(atkPlayerEntity, IID_Player, { + GetEnemies: () => [targetOwner] + }); + + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + GetPlayerByID: (id) => atkPlayerEntity, + GetNumPlayers: () => 5 + }); + + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + ExecuteQueryAroundPos: () => [target], + GetElevationAdaptedRange: (pos, rot, max, bonus, a) => max, + }); + + AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { + RemoveProjectile: () => {}, + LaunchProjectileAtPoint: (ent, pos, speed, gravity) => {}, + }); + + AddMock(target, IID_Position, { + GetPosition: () => targetPos, + GetPreviousPosition: () => targetPos, + GetPosition2D: () => new Vector2D(3, 3), + IsInWorld: () => true, + }); + + AddMock(target, IID_Health, {}); + + AddMock(target, IID_DamageReceiver, { + TakeDamage: (hack, pierce, crush) => { damageTaken = true; return { "killed": false, "change": -crush }; }, + }); + + Engine.PostMessage = function(ent, iid, message) + { + TS_ASSERT_UNEVAL_EQUALS({ "attacker": attacker, "target": target, "type": type, "damage": damage, "attackerOwner": attackerOwner }, message); + }; + + AddMock(target, IID_Footprint, { + GetShape: () => ({ "type": "circle", "radius": 20 }), + }); + + AddMock(attacker, IID_Ownership, { + GetOwner: () => attackerOwner, + }); + + AddMock(attacker, IID_Position, { + GetPosition: () => new Vector3D(2, 0, 3), + GetRotation: () => new Vector3D(1, 2, 3), + IsInWorld: () => true, + }); + + function TestDamage() + { + cmpTimer.OnUpdate({ turnLength: 1 }); + TS_ASSERT(damageTaken); + damageTaken = false; + } + + cmpDamage.CauseDamage(data); + TestDamage(); + + type = data.type = "Ranged"; + cmpDamage.CauseDamage(data); + TestDamage(); + + data.friendlyFire = false; + data.range = 10; + data.shape = "Circular"; + data.isSplash = true; + cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", 1000, data); + TestDamage(); + + // Check for damage still being dealt if the attacker dies + cmpAttack.PerformAttack("Ranged", target); + Engine.DestroyEntity(attacker); + TestDamage(); + + atkPlayerEntity = 1; + 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]); +} -AddMock(atkPlayerEntity, IID_Player, { - GetEnemies: () => [targetOwner] -}); - -AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - GetPlayerByID: (id) => atkPlayerEntity, - GetNumPlayers: () => 5 -}); - -AddMock(SYSTEM_ENTITY, IID_RangeManager, { - ExecuteQueryAroundPos: () => [target], - GetElevationAdaptedRange: (pos, rot, max, bonus, a) => max, -}); - -AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { - RemoveProjectile: () => {}, - LaunchProjectileAtPoint: (ent, pos, speed, gravity) => {}, -}); - -AddMock(target, IID_Position, { - GetPosition: () => targetPos, - GetPreviousPosition: () => targetPos, - GetPosition2D: () => new Vector2D(3, 3), - IsInWorld: () => true, -}); - -AddMock(target, IID_Health, {}); - -AddMock(target, IID_DamageReceiver, { - TakeDamage: (hack, pierce, crush) => { damageTaken = true; return { "killed": false, "change": -crush }; }, -}); +Test_Generic(); -Engine.PostMessage = function(ent, iid, message) +let TestLinearSplashDamage = function() { - TS_ASSERT_UNEVAL_EQUALS({ "attacker": attacker, "target": target, "type": type, "damage": damage, "attackerOwner": attackerOwner }, message); -}; + ResetState(); + Engine.PostMessage = (ent, iid, message) => {}; -AddMock(target, IID_Footprint, { - GetShape: () => ({ "type": "circle", "radius": 20 }), -}); - -AddMock(attacker, IID_Ownership, { - GetOwner: () => attackerOwner, -}); - -AddMock(attacker, IID_Position, { - GetPosition: () => new Vector3D(2, 0, 3), - GetRotation: () => new Vector3D(1, 2, 3), - IsInWorld: () => true, -}); + const attacker = 50; + const attackerOwner = 1; -function TestDamage() -{ - cmpTimer.OnUpdate({ turnLength: 1 }); - TS_ASSERT(damageTaken); - damageTaken = false; -} + const origin = new Vector2D(0, 0); -cmpDamage.CauseDamage(data); -TestDamage(); + let data = { + "attacker": attacker, + "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) + { + return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius)); + } + + let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage"); + + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + ExecuteQueryAroundPos: () => [60, 61, 62], + }); + + AddMock(60, IID_Position, { + GetPosition2D: () => new Vector2D(2.2, -0.4), + }); + + AddMock(61, IID_Position, { + GetPosition2D: () => new Vector2D(0, 0), + }); + + AddMock(62, IID_Position, { + GetPosition2D: () => new Vector2D(5, 2), + }); + + AddMock(60, IID_DamageReceiver, { + TakeDamage: (hack, pierce, crush) => { + TS_ASSERT_EQUALS(hack + pierce + crush, 100 * fallOff(2.2, -0.4)); + return { "killed": false, "change": -(hack + pierce + crush) }; + } + }); + + AddMock(61, IID_DamageReceiver, { + TakeDamage: (hack, pierce, crush) => { + TS_ASSERT_EQUALS(hack + pierce + crush, 100 * fallOff(0, 0)); + return { "killed": false, "change": -(hack + pierce + crush) }; + } + }); + + AddMock(62, IID_DamageReceiver, { + TakeDamage: (hack, pierce, crush) => { + TS_ASSERT_EQUALS(hack + pierce + crush, 0); + return { "killed": false, "change": -(hack + pierce + crush) }; + } + }); + + cmpDamage.CauseSplashDamage(data); + + data.direction = new Vector3D(0.6, 747, 0.8); + + AddMock(60, IID_DamageReceiver, { + TakeDamage: (hack, pierce, crush) => { + TS_ASSERT_EQUALS(hack + pierce + crush, 100 * fallOff(1, 2)); + return { "killed": false, "change": -(hack + pierce + crush) }; + } + }); + + cmpDamage.CauseSplashDamage(data); +}; -type = data.type = "Ranged"; -cmpDamage.CauseDamage(data); -TestDamage(); - -data.friendlyFire = false; -data.range = 10; -data.shape = "Circular"; -data.isSplash = true; -cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", 1000, data); -TestDamage(); - -// Check for damage still being dealt if the attacker dies -cmpAttack.PerformAttack("Ranged", target); -Engine.DestroyEntity(attacker); -TestDamage(); - -atkPlayerEntity = 1; -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]); +TestLinearSplashDamage();