mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 13:23:56 -07:00
Fix issues with civ and classes bonuses for splash damage. Even if those bonuses were in splash schema they was not taken into account. Update, fix, clean and complete unit tests for those logic. Reviewed by Mate-86.
Differential Revision: https://code.wildfiregames.com/D792 This was SVN commit r20015.
This commit is contained in:
parent
028ce1d9e9
commit
ff90bb8490
9 changed files with 336 additions and 72 deletions
|
|
@ -164,7 +164,6 @@ m.allowCapture = function(gameState, ent, target)
|
|||
return capture > antiCapture + sumCapturePoints/80;
|
||||
};
|
||||
|
||||
/** copy of GetAttackBonus from Attack.js */
|
||||
m.getAttackBonus = function(ent, target, type)
|
||||
{
|
||||
let attackBonus = 1;
|
||||
|
|
|
|||
|
|
@ -432,33 +432,14 @@ Attack.prototype.GetRange = function(type)
|
|||
return { "max": max, "min": min, "elevationBonus": elevationBonus };
|
||||
};
|
||||
|
||||
// Calculate the attack damage multiplier against a target
|
||||
Attack.prototype.GetAttackBonus = function(type, target)
|
||||
Attack.prototype.GetBonusTemplate = function(type)
|
||||
{
|
||||
let attackBonus = 1;
|
||||
let template = this.template[type];
|
||||
if (!template)
|
||||
template = this.template[type.split(".")[0]].Splash;
|
||||
|
||||
if (template.Bonuses)
|
||||
{
|
||||
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
|
||||
if (!cmpIdentity)
|
||||
return 1;
|
||||
|
||||
// Multiply the bonuses for all matching classes
|
||||
for (let key in template.Bonuses)
|
||||
{
|
||||
let bonus = template.Bonuses[key];
|
||||
if (bonus.Civ && bonus.Civ !== cmpIdentity.GetCiv())
|
||||
continue;
|
||||
if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !cmpIdentity.HasClass(cls)))
|
||||
continue;
|
||||
attackBonus *= bonus.Multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
return attackBonus;
|
||||
return clone(template.Bonuses);
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -528,7 +509,7 @@ Attack.prototype.PerformAttack = function(type, target)
|
|||
"position": realTargetPosition,
|
||||
"direction": missileDirection,
|
||||
"projectileId": id,
|
||||
"multiplier": this.GetAttackBonus(type, target),
|
||||
"bonus": this.GetBonusTemplate(type),
|
||||
"isSplash": false,
|
||||
"attackerOwner": attackerOwner
|
||||
};
|
||||
|
|
@ -538,7 +519,8 @@ Attack.prototype.PerformAttack = function(type, target)
|
|||
data.radius = +this.template.Ranged.Splash.Range;
|
||||
data.shape = this.template.Ranged.Splash.Shape;
|
||||
data.isSplash = true;
|
||||
data.splashStrengths = this.GetAttackStrengths(type+".Splash");
|
||||
data.splashStrengths = this.GetAttackStrengths(type + ".Splash");
|
||||
data.splashBonus = this.GetBonusTemplate(type + ".Splash");
|
||||
}
|
||||
cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000, data);
|
||||
}
|
||||
|
|
@ -547,7 +529,7 @@ Attack.prototype.PerformAttack = function(type, target)
|
|||
if (attackerOwner == -1)
|
||||
return;
|
||||
|
||||
let multiplier = this.GetAttackBonus(type, target);
|
||||
let multiplier = GetDamageBonus(target, this.GetBonusTemplate(type));
|
||||
let cmpHealth = Engine.QueryInterface(target, IID_Health);
|
||||
if (!cmpHealth || cmpHealth.GetHitpoints() == 0)
|
||||
return;
|
||||
|
|
@ -574,7 +556,7 @@ Attack.prototype.PerformAttack = function(type, target)
|
|||
"strengths": this.GetAttackStrengths(type),
|
||||
"target": target,
|
||||
"attacker": this.entity,
|
||||
"multiplier": this.GetAttackBonus(type, target),
|
||||
"multiplier": GetDamageBonus(target, this.GetBonusTemplate(type)),
|
||||
"type": type,
|
||||
"attackerOwner": attackerOwner
|
||||
});
|
||||
|
|
@ -609,7 +591,7 @@ Attack.prototype.PredictTimeToTarget = function(selfPosition, horizSpeed, target
|
|||
return c / (Math.sqrt(disc) - b);
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Attack.prototype.OnValueModification = function(msg)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -97,11 +97,14 @@ Damage.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
|
|||
* @param {boolean} data.isSplash - a flag indicating if it's splash damage.
|
||||
* @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 {Vector3D} data.direction - the unit vector defining the direction.
|
||||
* @param {Object} data.bonus - the attack bonus template from the attacker.
|
||||
* ***When splash damage***
|
||||
* @param {boolean} data.friendlyFire - a flag indicating if allied entities are also damaged.
|
||||
* @param {number} data.radius - the radius of the splash damage.
|
||||
* @param {string} data.shape - the shape of the splash range.
|
||||
* @param {Object} data.splashBonus - the attack bonus template from the attacker.
|
||||
* @param {Object} data.splashStrengths - data of the form { 'hack': number, 'pierce': number, 'crush': number }.
|
||||
*/
|
||||
Damage.prototype.MissileHit = function(data, lateness)
|
||||
{
|
||||
|
|
@ -117,6 +120,7 @@ Damage.prototype.MissileHit = function(data, lateness)
|
|||
"radius": data.radius,
|
||||
"shape": data.shape,
|
||||
"strengths": data.splashStrengths,
|
||||
"splashBonus": data.splashBonus,
|
||||
"direction": data.direction,
|
||||
"playersToDamage": this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire),
|
||||
"type": data.type,
|
||||
|
|
@ -131,6 +135,7 @@ Damage.prototype.MissileHit = function(data, lateness)
|
|||
let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
|
||||
if (cmpDamageReceiver && this.TestCollision(data.target, data.position, lateness))
|
||||
{
|
||||
data.multiplier = GetDamageBonus(data.target, data.bonus);
|
||||
this.CauseDamage(data);
|
||||
cmpProjectileManager.RemoveProjectile(data.projectileId);
|
||||
return;
|
||||
|
|
@ -147,11 +152,12 @@ Damage.prototype.MissileHit = function(data, lateness)
|
|||
{
|
||||
if (!this.TestCollision(ent, data.position, lateness))
|
||||
continue;
|
||||
|
||||
this.CauseDamage({
|
||||
"strengths": data.strengths,
|
||||
"target": ent,
|
||||
"attacker": data.attacker,
|
||||
"multiplier": data.multiplier,
|
||||
"multiplier": GetDamageBonus(ent, data.bonus),
|
||||
"type": data.type,
|
||||
"attackerOwner": data.attackerOwner
|
||||
});
|
||||
|
|
@ -170,7 +176,8 @@ Damage.prototype.MissileHit = function(data, lateness)
|
|||
* @param {Object} data.strengths - data of the form { 'hack': number, 'pierce': number, 'crush': number }.
|
||||
* @param {string} data.type - the type of damage.
|
||||
* @param {number} data.attackerOwner - the player id of the attacker.
|
||||
* @param {Vector3D} [data.direction] - the unit vector defining the direction.
|
||||
* @param {Vector3D} [data.direction] - the unit vector defining the direction. Needed for linear splash damage.
|
||||
* @param {Object} data.splashBonus - the attack bonus template from the attacker.
|
||||
* @param {number[]} data.playersToDamage - the array of player id's to damage.
|
||||
*/
|
||||
Damage.prototype.CauseSplashDamage = function(data)
|
||||
|
|
@ -210,6 +217,10 @@ Damage.prototype.CauseSplashDamage = function(data)
|
|||
{
|
||||
warn("The " + data.shape + " splash damage shape is not implemented!");
|
||||
}
|
||||
|
||||
if (data.splashBonus)
|
||||
damageMultiplier *= GetDamageBonus(ent, data.splashBonus);
|
||||
|
||||
// Call CauseDamage which reduces the hitpoints, posts network command, plays sounds....
|
||||
this.CauseDamage({
|
||||
"strengths": data.strengths,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@ DeathDamage.prototype.GetDeathDamageStrengths = function(type)
|
|||
};
|
||||
};
|
||||
|
||||
DeathDamage.prototype.GetBonusTemplate = function()
|
||||
{
|
||||
if (this.template.Bonuses)
|
||||
return clone(this.template.Bonuses);
|
||||
return null;
|
||||
};
|
||||
|
||||
DeathDamage.prototype.CauseDeathDamage = function()
|
||||
{
|
||||
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
||||
|
|
@ -78,6 +85,7 @@ DeathDamage.prototype.CauseDeathDamage = function()
|
|||
"radius": radius,
|
||||
"shape": this.template.Shape,
|
||||
"strengths": this.GetDeathDamageStrengths("Death"),
|
||||
"splashBonus": this.GetBonusTemplate(),
|
||||
"playersToDamage": playersToDamage,
|
||||
"type": "Death",
|
||||
"attackerOwner": owner
|
||||
|
|
|
|||
|
|
@ -4684,14 +4684,6 @@ UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)
|
|||
return cmpAttack.GetBestAttackAgainst(target, allowCapture);
|
||||
};
|
||||
|
||||
UnitAI.prototype.GetAttackBonus = function(type, target)
|
||||
{
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return 1;
|
||||
return cmpAttack.GetAttackBonus(type, target);
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to find one of the given entities which can be attacked,
|
||||
* and start attacking it.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
Engine.LoadHelperScript("DamageBonus.js");
|
||||
Engine.LoadHelperScript("Player.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/Auras.js");
|
||||
|
|
@ -86,7 +87,7 @@ function attackComponentTest(defenderClass, isEnemy, test_function)
|
|||
"Bonuses": {
|
||||
"BonusCav": {
|
||||
"Classes": "Cavalry",
|
||||
"Multiplier": 2
|
||||
"Multiplier": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -163,11 +164,21 @@ attackComponentTest(undefined, true ,(attacker, cmpAttack, defender) => {
|
|||
|
||||
for (let className of ["Infantry", "Cavalry"])
|
||||
attackComponentTest(className, true, (attacker, cmpAttack, defender) => {
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Melee", defender), className == "Cavalry" ? 2 : 1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Ranged", defender), 1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Ranged.Splash", defender), className == "Cavalry" ? 2 : 1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Capture", defender), 1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackBonus("Slaughter", defender), 1);
|
||||
|
||||
TS_ASSERT_EQUALS(cmpAttack.GetBonusTemplate("Melee").BonusCav.Multiplier, 2);
|
||||
// Check that we don't leak data
|
||||
let bonus = cmpAttack.GetBonusTemplate("Melee");
|
||||
bonus.BonusCav.Multiplier = 2.7;
|
||||
TS_ASSERT_EQUALS(cmpAttack.GetBonusTemplate("Melee").BonusCav.Multiplier, 2);
|
||||
|
||||
TS_ASSERT(cmpAttack.GetBonusTemplate("Capture") === null);
|
||||
|
||||
let getAttackBonus = (t, e) => GetDamageBonus(e, cmpAttack.GetBonusTemplate(t));
|
||||
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Melee", defender), className == "Cavalry" ? 2 : 1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Ranged", defender), 1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Ranged.Splash", defender), className == "Cavalry" ? 3 : 1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Capture", defender), 1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Slaughter", defender), 1);
|
||||
});
|
||||
|
||||
// CanAttack rejects elephant attack due to RestrictedClasses
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
Engine.LoadHelperScript("DamageBonus.js");
|
||||
Engine.LoadHelperScript("Player.js");
|
||||
Engine.LoadHelperScript("Sound.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
|
|
@ -36,8 +37,7 @@ function Test_Generic()
|
|||
let type = "Melee";
|
||||
let damageTaken = false;
|
||||
|
||||
cmpAttack.GetAttackStrengths = (type) => ({ "hack": 0, "pierce": 0, "crush": damage });
|
||||
cmpAttack.GetAttackBonus = (type, target) => 1.0;
|
||||
cmpAttack.GetAttackStrengths = attackType => ({ "hack": 0, "pierce": 0, "crush": damage });
|
||||
|
||||
let data = {
|
||||
"attacker": attacker,
|
||||
|
|
@ -57,24 +57,19 @@ function Test_Generic()
|
|||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
||||
"GetPlayerByID": (id) => atkPlayerEntity,
|
||||
"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) => {},
|
||||
"RemoveProjectile": () => {},
|
||||
"LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {},
|
||||
});
|
||||
|
||||
AddMock(target, IID_Position, {
|
||||
"GetPosition": () => targetPos,
|
||||
"GetPreviousPosition": () => targetPos,
|
||||
"GetPosition2D": () => new Vector2D(3, 3),
|
||||
"GetPosition2D": () => Vector2D.From(targetPos),
|
||||
"IsInWorld": () => true,
|
||||
});
|
||||
|
||||
|
|
@ -117,13 +112,6 @@ function Test_Generic()
|
|||
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);
|
||||
|
|
@ -164,7 +152,9 @@ function TestLinearSplashDamage()
|
|||
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();
|
||||
|
||||
let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
|
||||
|
||||
|
|
@ -186,6 +176,7 @@ function TestLinearSplashDamage()
|
|||
|
||||
AddMock(60, IID_DamageReceiver, {
|
||||
"TakeDamage": (hack, pierce, crush) => {
|
||||
hitEnts.add(60);
|
||||
TS_ASSERT_EQUALS(hack + pierce + crush, 100 * fallOff(2.2, -0.4));
|
||||
return { "killed": false, "change": -(hack + pierce + crush) };
|
||||
}
|
||||
|
|
@ -193,6 +184,7 @@ function TestLinearSplashDamage()
|
|||
|
||||
AddMock(61, IID_DamageReceiver, {
|
||||
TakeDamage: (hack, pierce, crush) => {
|
||||
hitEnts.add(61);
|
||||
TS_ASSERT_EQUALS(hack + pierce + crush, 100 * fallOff(0, 0));
|
||||
return { "killed": false, "change": -(hack + pierce + crush) };
|
||||
}
|
||||
|
|
@ -200,24 +192,34 @@ function TestLinearSplashDamage()
|
|||
|
||||
AddMock(62, IID_DamageReceiver, {
|
||||
"TakeDamage": (hack, pierce, crush) => {
|
||||
hitEnts.add(62);
|
||||
TS_ASSERT_EQUALS(hack + pierce + crush, 0);
|
||||
return { "killed": false, "change": -(hack + pierce + crush) };
|
||||
}
|
||||
});
|
||||
|
||||
cmpDamage.CauseSplashDamage(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_DamageReceiver, {
|
||||
"TakeDamage": (hack, pierce, crush) => {
|
||||
hitEnts.add(60);
|
||||
TS_ASSERT_EQUALS(hack + pierce + crush, 100 * fallOff(1, 2));
|
||||
return { "killed": false, "change": -(hack + pierce + crush) };
|
||||
}
|
||||
});
|
||||
|
||||
cmpDamage.CauseSplashDamage(data);
|
||||
};
|
||||
TS_ASSERT(hitEnts.has(60));
|
||||
TS_ASSERT(hitEnts.has(61));
|
||||
TS_ASSERT(hitEnts.has(62));
|
||||
hitEnts.clear();
|
||||
}
|
||||
|
||||
TestLinearSplashDamage();
|
||||
|
||||
|
|
@ -231,12 +233,12 @@ function TestCircularSplashDamage()
|
|||
let fallOff = function(r)
|
||||
{
|
||||
return 1 - r * r / (radius * radius);
|
||||
}
|
||||
};
|
||||
|
||||
let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"ExecuteQueryAroundPos": () => [60, 61, 62],
|
||||
"ExecuteQueryAroundPos": () => [60, 61, 62, 64],
|
||||
});
|
||||
|
||||
AddMock(60, IID_Position, {
|
||||
|
|
@ -252,12 +254,12 @@ function TestCircularSplashDamage()
|
|||
});
|
||||
|
||||
AddMock(63, IID_Position, {
|
||||
"GetPosition2D": () => new Vector2D(5, -3),
|
||||
"GetPosition2D": () => new Vector2D(10, -10),
|
||||
});
|
||||
|
||||
// Target on the frontier of the shape
|
||||
AddMock(64, IID_Position, {
|
||||
"GetPosition2D": () => new Vector2D(4, -2),
|
||||
"GetPosition2D": () => new Vector2D(9, -4),
|
||||
});
|
||||
|
||||
AddMock(60, IID_DamageReceiver, {
|
||||
|
|
@ -283,8 +285,7 @@ function TestCircularSplashDamage()
|
|||
|
||||
AddMock(63, IID_DamageReceiver, {
|
||||
"TakeDamage": (hack, pierce, crush) => {
|
||||
TS_ASSERT_EQUALS(hack + pierce + crush, 0);
|
||||
return { "killed": false, "change": -(hack + pierce + crush) };
|
||||
TS_ASSERT(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -305,6 +306,232 @@ function TestCircularSplashDamage()
|
|||
"type": "Ranged",
|
||||
"attackerOwner": 1
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
TestCircularSplashDamage();
|
||||
|
||||
function Test_MissileHit()
|
||||
{
|
||||
ResetState();
|
||||
Engine.PostMessage = (ent, iid, message) => {};
|
||||
|
||||
let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
|
||||
|
||||
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",
|
||||
"attacker": 70,
|
||||
"target": 60,
|
||||
"strengths": { "hack": 0, "pierce": 100, "crush": 0 },
|
||||
"position": targetPos,
|
||||
"direction": new Vector3D(1, 0, 0),
|
||||
"projectileId": 9,
|
||||
"bonus": undefined,
|
||||
"isSplash": false,
|
||||
"attackerOwner": 1
|
||||
};
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
||||
"GetPlayerByID": id => id == 1 ? 10 : 11,
|
||||
"GetNumPlayers": () => 2
|
||||
});
|
||||
|
||||
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, {});
|
||||
|
||||
AddMock(60, IID_DamageReceiver, {
|
||||
"TakeDamage": (hack, pierce, crush) => {
|
||||
TS_ASSERT_EQUALS(hack + pierce + crush, 100);
|
||||
hitEnts.add(60);
|
||||
return { "killed": false, "change": -(hack + pierce + crush) };
|
||||
}
|
||||
});
|
||||
|
||||
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]
|
||||
});
|
||||
|
||||
cmpDamage.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_DamageReceiver, {
|
||||
"TakeDamage": (hack, pierce, crush) => {
|
||||
TS_ASSERT_EQUALS(false);
|
||||
return { "killed": false, "change": -(hack + pierce + crush) };
|
||||
}
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"ExecuteQueryAroundPos": () => [61]
|
||||
});
|
||||
|
||||
AddMock(61, IID_Position, {
|
||||
"GetPosition": () => targetPos,
|
||||
"GetPreviousPosition": () => targetPos,
|
||||
"GetPosition2D": () => Vector2D.from3D(targetPos),
|
||||
"IsInWorld": () => true,
|
||||
});
|
||||
|
||||
AddMock(61, IID_Health, {});
|
||||
|
||||
AddMock(61, IID_DamageReceiver, {
|
||||
"TakeDamage": (hack, pierce, crush) => {
|
||||
TS_ASSERT_EQUALS(hack + pierce + crush, 100);
|
||||
hitEnts.add(61);
|
||||
return { "killed": false, "change": -(hack + pierce + crush) };
|
||||
}
|
||||
});
|
||||
|
||||
AddMock(61, IID_Footprint, {
|
||||
"GetShape": () => ({ "type": "circle", "radius": 20 }),
|
||||
});
|
||||
|
||||
cmpDamage.MissileHit(data, 0);
|
||||
TS_ASSERT(hitEnts.has(61));
|
||||
hitEnts.clear();
|
||||
|
||||
// Add a splash damage.
|
||||
|
||||
data.friendlyFire = false;
|
||||
data.radius = 10;
|
||||
data.shape = "Circular";
|
||||
data.isSplash = true;
|
||||
data.splashStrengths = { "hack": 0, "pierce": 0, "crush": 200 };
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"ExecuteQueryAroundPos": () => [61, 62]
|
||||
});
|
||||
|
||||
let dealtDamage = 0;
|
||||
AddMock(61, IID_DamageReceiver, {
|
||||
"TakeDamage": (hack, pierce, crush) => {
|
||||
dealtDamage += hack + pierce + crush;
|
||||
hitEnts.add(61);
|
||||
return { "killed": false, "change": -(hack + pierce + crush) };
|
||||
}
|
||||
});
|
||||
|
||||
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, {});
|
||||
|
||||
AddMock(62, IID_DamageReceiver, {
|
||||
"TakeDamage": (hack, pierce, crush) => {
|
||||
TS_ASSERT_EQUALS(hack + pierce + crush, 200 * 0.75);
|
||||
hitEnts.add(62);
|
||||
return { "killed": false, "change": -(hack + pierce + crush) };
|
||||
}
|
||||
});
|
||||
|
||||
AddMock(62, IID_Footprint, {
|
||||
"GetShape": () => ({ "type": "circle", "radius": 20 }),
|
||||
});
|
||||
|
||||
cmpDamage.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, {
|
||||
"HasClass": cl => cl == "Cavalry"
|
||||
});
|
||||
|
||||
data.bonus = bonus;
|
||||
cmpDamage.MissileHit(data, 0);
|
||||
TS_ASSERT(hitEnts.has(61));
|
||||
TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 200);
|
||||
dealtDamage = 0;
|
||||
hitEnts.clear();
|
||||
|
||||
data.splashBonus = splashBonus;
|
||||
cmpDamage.MissileHit(data, 0);
|
||||
TS_ASSERT(hitEnts.has(61));
|
||||
TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 10000 * 200);
|
||||
dealtDamage = 0;
|
||||
hitEnts.clear();
|
||||
|
||||
data.bonus = undefined;
|
||||
cmpDamage.MissileHit(data, 0);
|
||||
TS_ASSERT(hitEnts.has(61));
|
||||
TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
|
||||
dealtDamage = 0;
|
||||
hitEnts.clear();
|
||||
|
||||
data.bonus = null;
|
||||
cmpDamage.MissileHit(data, 0);
|
||||
TS_ASSERT(hitEnts.has(61));
|
||||
TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
|
||||
dealtDamage = 0;
|
||||
hitEnts.clear();
|
||||
|
||||
data.bonus = {};
|
||||
cmpDamage.MissileHit(data, 0);
|
||||
TS_ASSERT(hitEnts.has(61));
|
||||
TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
|
||||
dealtDamage = 0;
|
||||
hitEnts.clear();
|
||||
}
|
||||
|
||||
Test_MissileHit();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
Engine.LoadHelperScript("DamageBonus.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Damage.js");
|
||||
|
|
@ -41,6 +42,7 @@ let result = {
|
|||
"radius": template.Range,
|
||||
"shape": template.Shape,
|
||||
"strengths": modifiedDamage,
|
||||
"splashBonus": null,
|
||||
"playersToDamage": playersToDamage,
|
||||
"type": "Death",
|
||||
"attackerOwner": player
|
||||
|
|
@ -62,3 +64,9 @@ AddMock(deadEnt, IID_Ownership, {
|
|||
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage);
|
||||
cmpDeathDamage.CauseDeathDamage();
|
||||
|
||||
let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } };
|
||||
cmpDeathDamage.template.Bonuses = splashBonus;
|
||||
result.splashBonus = splashBonus;
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage);
|
||||
cmpDeathDamage.CauseDeathDamage();
|
||||
|
|
|
|||
26
binaries/data/mods/public/simulation/helpers/DamageBonus.js
Normal file
26
binaries/data/mods/public/simulation/helpers/DamageBonus.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Calculate the attack damage multiplier against a target.
|
||||
*/
|
||||
function GetDamageBonus(target, template)
|
||||
{
|
||||
let attackBonus = 1;
|
||||
|
||||
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
|
||||
if (!cmpIdentity)
|
||||
return 1;
|
||||
|
||||
// Multiply the bonuses for all matching classes
|
||||
for (let key in template)
|
||||
{
|
||||
let bonus = template[key];
|
||||
if (bonus.Civ && bonus.Civ !== cmpIdentity.GetCiv())
|
||||
continue;
|
||||
if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !cmpIdentity.HasClass(cls)))
|
||||
continue;
|
||||
attackBonus *= bonus.Multiplier;
|
||||
}
|
||||
|
||||
return attackBonus;
|
||||
}
|
||||
|
||||
Engine.RegisterGlobal("GetDamageBonus", GetDamageBonus);
|
||||
Loading…
Reference in a new issue