Index: ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js (revision 24004) +++ ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js (revision 24005) @@ -1,90 +1,92 @@ function DelayedDamage() {} DelayedDamage.prototype.Schema = ""; DelayedDamage.prototype.Init = function() { }; /** * When missiles miss their target, other units in MISSILE_HIT_RADIUS range are considered. * Large missiles should probably implement splash damage anyways, * so keep this value low for performance. */ DelayedDamage.prototype.MISSILE_HIT_RADIUS = 2; /** * 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. * @param {boolean} data.friendlyFire - A flag indicating whether allied entities can also be damaged. * ***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, "friendlyFire": data.splash.friendlyFire }); let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); + let target = data.target; + // Since we can't damage mirages, replace a miraged target by the real target. + let cmpMirage = Engine.QueryInterface(data.target, IID_Mirage); + if (cmpMirage) + target = cmpMirage.GetParent(); + // Deal direct damage if we hit the main target // and we could handle the attack. - if (Attacking.TestCollision(data.target, data.position, lateness) && - Attacking.HandleAttackEffects(data.target, data.type, data.attackData, data.attacker, data.attackerOwner)) + if (Attacking.TestCollision(target, data.position, lateness) && + Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner)) { cmpProjectileManager.RemoveProjectile(data.projectileId); return; } - let targetPosition = Attacking.InterpolatedLocation(data.target, lateness); - if (!targetPosition) - return; - // If we didn't hit the main target look for nearby units. let ents = Attacking.EntitiesNearPoint(Vector2D.from3D(data.position), this.MISSILE_HIT_RADIUS, Attacking.GetPlayersToDamage(data.attackerOwner, data.friendlyFire)); for (let ent of ents) { if (!Attacking.TestCollision(ent, data.position, lateness) || !Attacking.HandleAttackEffects(ent, data.type, data.attackData, data.attacker, data.attackerOwner)) continue; cmpProjectileManager.RemoveProjectile(data.projectileId); break; } }; Engine.RegisterSystemComponentType(IID_DelayedDamage, "DelayedDamage", DelayedDamage); Index: ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js (revision 24004) +++ ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js (revision 24005) @@ -1,227 +1,232 @@ const VIS_HIDDEN = 0; const VIS_FOGGED = 1; const VIS_VISIBLE = 2; function Mirage() {} Mirage.prototype.Schema = "Mirage entities replace real entities in the fog-of-war." + ""; Mirage.prototype.Init = function() { this.player = null; this.parent = INVALID_ENTITY; this.miragedIids = new Set(); this.classesList = []; this.numBuilders = 0; this.buildTime = {}; this.maxHitpoints = null; this.hitpoints = null; this.repairable = null; this.unhealable = null; this.injured = null; this.capturePoints = []; this.maxCapturePoints = 0; this.maxAmount = null; this.amount = null; this.type = null; this.isInfinite = null; this.killBeforeGather = null; this.maxGatherers = null; this.numGatherers = null; this.traders = null; this.marketType = null; this.internationalBonus = null; }; Mirage.prototype.SetParent = function(ent) { this.parent = ent; }; -Mirage.prototype.GetPlayer = function() +Mirage.prototype.GetParent = function() { - return this.player; + return this.parent; }; Mirage.prototype.SetPlayer = function(player) { this.player = player; }; +Mirage.prototype.GetPlayer = function() +{ + return this.player; +}; + Mirage.prototype.Mirages = function(iid) { return this.miragedIids.has(iid); }; // ============================ // Parent entity data Mirage.prototype.CopyIdentity = function(cmpIdentity) { this.miragedIids.add(IID_Identity); // In almost all cases we want to ignore mirage entities when querying Identity components of owned entities. // To avoid adding a test everywhere, we don't transfer the classeslist in the template but here. // We clone this since the classes list is not synchronized and since the mirage should be a snapshot of the entity at the given time. this.classesList = clone(cmpIdentity.GetClassesList()); }; Mirage.prototype.GetClassesList = function() { return this.classesList; }; // Foundation data Mirage.prototype.CopyFoundation = function(cmpFoundation) { this.miragedIids.add(IID_Foundation); this.numBuilders = cmpFoundation.GetNumBuilders(); this.buildTime = cmpFoundation.GetBuildTime(); }; Mirage.prototype.GetNumBuilders = function() { return this.numBuilders; }; Mirage.prototype.GetBuildTime = function() { return this.buildTime; }; // Repairable data (numBuilders and buildTime shared with foundation as entities can't have both) Mirage.prototype.CopyRepairable = function(cmpRepairable) { this.miragedIids.add(IID_Repairable); this.numBuilders = cmpRepairable.GetNumBuilders(); this.buildTime = cmpRepairable.GetBuildTime(); }; // Health data Mirage.prototype.CopyHealth = function(cmpHealth) { this.miragedIids.add(IID_Health); this.maxHitpoints = cmpHealth.GetMaxHitpoints(); this.hitpoints = cmpHealth.GetHitpoints(); this.repairable = cmpHealth.IsRepairable(); this.injured = cmpHealth.IsInjured(); this.unhealable = cmpHealth.IsUnhealable(); }; Mirage.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; }; Mirage.prototype.GetHitpoints = function() { return this.hitpoints; }; Mirage.prototype.IsRepairable = function() { return this.repairable; }; Mirage.prototype.IsInjured = function() { return this.injured; }; Mirage.prototype.IsUnhealable = function() { return this.unhealable; }; // Capture data Mirage.prototype.CopyCapturable = function(cmpCapturable) { this.miragedIids.add(IID_Capturable); this.capturePoints = clone(cmpCapturable.GetCapturePoints()); this.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); }; Mirage.prototype.GetMaxCapturePoints = function() { return this.maxCapturePoints; }; Mirage.prototype.GetCapturePoints = function() { return this.capturePoints; }; Mirage.prototype.CanCapture = Capturable.prototype.CanCapture; // ResourceSupply data Mirage.prototype.CopyResourceSupply = function(cmpResourceSupply) { this.miragedIids.add(IID_ResourceSupply); this.maxAmount = cmpResourceSupply.GetMaxAmount(); this.amount = cmpResourceSupply.GetCurrentAmount(); this.type = cmpResourceSupply.GetType(); this.isInfinite = cmpResourceSupply.IsInfinite(); this.killBeforeGather = cmpResourceSupply.GetKillBeforeGather(); this.maxGatherers = cmpResourceSupply.GetMaxGatherers(); this.numGatherers = cmpResourceSupply.GetNumGatherers(); }; Mirage.prototype.GetMaxAmount = function() { return this.maxAmount; }; Mirage.prototype.GetCurrentAmount = function() { return this.amount; }; Mirage.prototype.GetType = function() { return this.type; }; Mirage.prototype.IsInfinite = function() { return this.isInfinite; }; Mirage.prototype.GetKillBeforeGather = function() { return this.killBeforeGather; }; Mirage.prototype.GetMaxGatherers = function() { return this.maxGatherers; }; Mirage.prototype.GetNumGatherers = function() { return this.numGatherers; }; // Market data Mirage.prototype.CopyMarket = function(cmpMarket) { this.miragedIids.add(IID_Market); this.traders = new Set(); for (let trader of cmpMarket.GetTraders()) { let cmpTrader = Engine.QueryInterface(trader, IID_Trader); let cmpOwnership = Engine.QueryInterface(trader, IID_Ownership); if (!cmpTrader || !cmpOwnership) { cmpMarket.RemoveTrader(trader); continue; } if (this.player != cmpOwnership.GetOwner()) continue; cmpTrader.SwitchMarket(cmpMarket.entity, this.entity); cmpMarket.RemoveTrader(trader); this.AddTrader(trader); } this.marketType = cmpMarket.GetType(); this.internationalBonus = cmpMarket.GetInternationalBonus(); }; Mirage.prototype.HasType = function(type) { return this.marketType.has(type); }; Mirage.prototype.GetInternationalBonus = function() { return this.internationalBonus; }; Mirage.prototype.AddTrader = function(trader) { this.traders.add(trader); }; Mirage.prototype.RemoveTrader = function(trader) { this.traders.delete(trader); }; Mirage.prototype.UpdateTraders = function(msg) { let cmpMarket = Engine.QueryInterface(this.parent, IID_Market); if (!cmpMarket) // The parent market does not exist anymore { for (let trader of this.traders) { let cmpTrader = Engine.QueryInterface(trader, IID_Trader); if (cmpTrader) cmpTrader.RemoveMarket(this.entity); } return; } // The market becomes visible, switch all traders from the mirage to the market for (let trader of this.traders) { let cmpTrader = Engine.QueryInterface(trader, IID_Trader); if (!cmpTrader) continue; cmpTrader.SwitchMarket(this.entity, cmpMarket.entity); this.RemoveTrader(trader); cmpMarket.AddTrader(trader); } }; // ============================ Mirage.prototype.OnVisibilityChanged = function(msg) { // Mirages get VIS_HIDDEN when the original entity becomes VIS_VISIBLE. if (msg.player != this.player || msg.newVisibility != VIS_HIDDEN) return; if (this.miragedIids.has(IID_Market)) this.UpdateTraders(msg); if (this.parent == INVALID_ENTITY) Engine.DestroyEntity(this.entity); else Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": this.parent }); }; Engine.RegisterComponentType(IID_Mirage, "Mirage", Mirage); 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 (revision 24004) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 24005) @@ -1,621 +1,635 @@ Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/DelayedDamage.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Attack.js"); Engine.LoadComponentScript("DelayedDamage.js"); Engine.LoadComponentScript("Timer.js"); function Test_Generic() { ResetState(); 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": { "Damage": { "Crush": 5, }, "MaxRange": 50, "MinRange": 0, "Delay": 0, "Projectile": { "Speed": 75.0, "Spread": 0.5, "Gravity": 9.81, "FriendlyFire": "false", "LaunchPoint": { "@y": 3 } } } }); let damage = 5; let target = 21; let targetOwner = 7; let targetPos = new Vector3D(3, 0, 3); let type = "Melee"; let damageTaken = false; cmpAttack.GetAttackStrengths = attackType => ({ "Hack": 0, "Pierce": 0, "Crush": damage }); let data = { "type": "Melee", "attackData": { "Damage": { "Hack": 0, "Pierce": 0, "Crush": damage }, }, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "position": targetPos, "projectileId": 9, "direction": new Vector3D(1, 0, 0) }; AddMock(atkPlayerEntity, IID_Player, { "GetEnemies": () => [targetOwner] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => atkPlayerEntity, "GetAllPlayers": () => [0, 1, 2, 3, 4] }); AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { "RemoveProjectile": () => {}, "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(target, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, }); AddMock(target, IID_Health, { "TakeDamage": (amount, __, ___) => { damageTaken = true; return { "healthChange": -amount }; }, }); AddMock(SYSTEM_ENTITY, IID_DelayedDamage, { "MissileHit": () => { damageTaken = true; }, }); Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "type": type, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "damage": damage, "capture": 0, "statusEffects": [], "fromStatusEffect": false }, 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; } Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner); TestDamage(); data.type = "Ranged"; type = data.type; Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner); 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(Attacking.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]); TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]); } Test_Generic(); function TestLinearSplashDamage() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; const attacker = 50; const attackerOwner = 1; 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", "direction": new Vector3D(1, 747, 0), "friendlyFire": false, }; 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(); AddMock(attackerOwner, IID_Player, { "GetEnemies": () => [2] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => attackerOwner, "GetAllPlayers": () => [0, 1, 2] }); 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_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(60); TS_ASSERT_EQUALS(amount, 100 * fallOff(2.2, -0.4)); return { "healthChange": -amount }; } }); AddMock(61, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(61); TS_ASSERT_EQUALS(amount, 100 * fallOff(0, 0)); return { "healthChange": -amount }; } }); AddMock(62, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(62); TS_ASSERT_EQUALS(amount, 0); return { "healthChange": -amount }; } }); Attacking.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); data.direction = new Vector3D(0.6, 747, 0.8); AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(60); TS_ASSERT_EQUALS(amount, 100 * fallOff(1, 2)); return { "healthChange": -amount }; } }); Attacking.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); } TestLinearSplashDamage(); function TestCircularSplashDamage() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; const radius = 10; let attackerOwner = 1; let fallOff = function(r) { return 1 - r * r / (radius * radius); }; AddMock(attackerOwner, IID_Player, { "GetEnemies": () => [2] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => attackerOwner, "GetAllPlayers": () => [0, 1, 2] }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62, 64], }); AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, 4), }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(3.6, 3.2), }); AddMock(63, IID_Position, { "GetPosition2D": () => new Vector2D(10, -10), }); // Target on the frontier of the shape AddMock(64, IID_Position, { "GetPosition2D": () => new Vector2D(9, -4), }); AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 100 * fallOff(0)); return { "healthChange": -amount }; } }); AddMock(61, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 100 * fallOff(5)); return { "healthChange": -amount }; } }); AddMock(62, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 100 * fallOff(1)); return { "healthChange": -amount }; } }); AddMock(63, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT(false); } }); let cmphealth = AddMock(64, IID_Health, { "TakeDamage": (amount, __, ___) => { TS_ASSERT_EQUALS(amount, 0); return { "healthChange": -amount }; } }); let spy = new Spy(cmphealth, "TakeDamage"); Attacking.CauseDamageOverArea({ "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": 50, "attackerOwner": attackerOwner, "origin": new Vector2D(3, 4), "radius": radius, "shape": "Circular", "friendlyFire": false, }); TS_ASSERT_EQUALS(spy._called, 1); } TestCircularSplashDamage(); function Test_MissileHit() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; let cmpDelayedDamage = ConstructComponent(SYSTEM_ENTITY, "DelayedDamage"); let target = 60; let targetOwner = 1; let targetPos = new Vector3D(3, 10, 0); let hitEnts = new Set(); AddMock(SYSTEM_ENTITY, IID_Timer, { "GetLatestTurnLength": () => 500 }); const radius = 10; let data = { "type": "Ranged", "attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } }, "target": 60, "attacker": 70, "attackerOwner": 1, "position": targetPos, "direction": new Vector3D(1, 0, 0), "projectileId": 9, "friendlyFire": "false", }; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id == 1 ? 10 : 11, "GetAllPlayers": () => [0, 1] }); AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { "RemoveProjectile": () => {}, "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(60, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, }); AddMock(60, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(60); TS_ASSERT_EQUALS(amount, 100); return { "healthChange": -amount }; } }); AddMock(60, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); AddMock(70, IID_Ownership, { "GetOwner": () => 1, }); AddMock(70, IID_Position, { "GetPosition": () => new Vector3D(0, 0, 0), "GetRotation": () => new Vector3D(0, 0, 0), "IsInWorld": () => true, }); AddMock(10, IID_Player, { "GetEnemies": () => [2] }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(60)); hitEnts.clear(); - // The main target is not hit but another one is hit. - - AddMock(60, IID_Position, { - "GetPosition": () => new Vector3D(900, 10, 0), - "GetPreviousPosition": () => new Vector3D(900, 10, 0), - "GetPosition2D": () => new Vector2D(900, 0), - "IsInWorld": () => true, - }); - - AddMock(60, IID_Health, { - "TakeDamage": (amount, __, ___) => { - TS_ASSERT_EQUALS(false); - return { "healthChange": -amount }; - } - }); - - AddMock(SYSTEM_ENTITY, IID_RangeManager, { - "ExecuteQueryAroundPos": () => [61] + // Target is a mirage: hit the parent. + AddMock(60, IID_Mirage, { + "GetParent": () => 61 }); AddMock(61, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.from3D(targetPos), - "IsInWorld": () => true, + "IsInWorld": () => true }); AddMock(61, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(61); TS_ASSERT_EQUALS(amount, 100); return { "healthChange": -amount }; } }); AddMock(61, IID_Footprint, { - "GetShape": () => ({ "type": "circle", "radius": 20 }), + "GetShape": () => ({ "type": "circle", "radius": 20 }) + }); + + cmpDelayedDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(61)); + hitEnts.clear(); + + // Make sure we don't corrupt other tests. + DeleteMock(60, IID_Mirage); + cmpDelayedDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(60)); + hitEnts.clear(); + + // The main target is not hit but another one is hit. + AddMock(60, IID_Position, { + "GetPosition": () => new Vector3D(900, 10, 0), + "GetPreviousPosition": () => new Vector3D(900, 10, 0), + "GetPosition2D": () => new Vector2D(900, 0), + "IsInWorld": () => true + }); + + AddMock(60, IID_Health, { + "TakeDamage": (amount, __, ___) => { + TS_ASSERT_EQUALS(false); + return { "healthChange": -amount }; + } + }); + + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "ExecuteQueryAroundPos": () => [61] }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); hitEnts.clear(); // Add a splash damage. 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_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(61); dealtDamage += amount; return { "healthChange": -amount }; } }); AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), "GetPosition2D": () => new Vector2D(8, 0), "IsInWorld": () => true, }); AddMock(62, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(62); TS_ASSERT_EQUALS(amount, 200 * 0.75); return { "healthChange": -amount }; } }); AddMock(62, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); dealtDamage = 0; TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); // Add some hard counters bonus. Engine.DestroyEntity(62); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61] }); let bonus= { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } }; let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 10000 } }; AddMock(61, IID_Identity, { "GetClassesList": () => ["Cavalry"], "GetCiv": () => "civ" }); 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.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.attackData.Bonuses = undefined; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); 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.attackData.Bonuses = {}; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); // Test splash damage with friendly fire. data.splash = {}; data.splash.friendlyFire = true; data.splash.radius = 10; data.splash.shape = "Circular"; data.splash.attackData = { "Damage": { "Pierce": 0, "Crush": 200 } }; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61, 62] }); dealtDamage = 0; AddMock(61, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(61); dealtDamage += amount; return { "healthChange": -amount }; } }); AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), "GetPosition2D": () => new Vector2D(8, 0), "IsInWorld": () => true, }); AddMock(62, IID_Health, { "TakeDamage": (amount, __, ___) => { hitEnts.add(62); TS_ASSERT_EQUALS(amount, 200 * 0.75); return { "healtChange": -amount }; } }); AddMock(62, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); dealtDamage = 0; TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); } Test_MissileHit();