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();