0ad/binaries/data/mods/public/simulation/components/tests/test_Attack.js

528 lines
14 KiB
JavaScript

AttackEffects = class AttackEffects
{
constructor() {}
Receivers()
{
return [{
"type": "Damage",
"IID": "IID_Health",
"method": "TakeDamage"
},
{
"type": "Capture",
"IID": "IID_Capturable",
"method": "Capture"
},
{
"type": "ApplyStatus",
"IID": "IID_StatusEffectsReceiver",
"method": "ApplyStatus"
}];
}
};
Engine.LoadHelperScript("Attack.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/Diplomacy.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Turretable.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("Attack.js");
let entityID = 903;
function attackComponentTest(defenderClass, isEnemy, test_function)
{
const playerEnt1 = 5;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": () => playerEnt1
});
AddMock(playerEnt1, IID_Player, {
"GetPlayerID": () => 1,
});
AddMock(playerEnt1, IID_Diplomacy, {
"IsEnemy": () => isEnemy
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": () => true
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"GetEffectiveParabolicRange": () => 25,
"GetMaxReachableParabolicHeight": () => 15,
"GetLosVisibility": (target, owner) => "visible"
});
const attacker = entityID;
AddMock(attacker, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 5,
"GetPosition2D": () => new Vector2D(1, 2)
});
AddMock(attacker, IID_Ownership, {
"GetOwner": () => 1
});
const cmpAttack = ConstructComponent(attacker, "Attack", {
"Melee": {
"Damage": {
"Hack": 11,
"Pierce": 5,
"Crush": 0
},
"MinRange": 3,
"MaxRange": 5,
"PreferredClasses": {
"_string": "Civilian"
},
"RestrictedClasses": {
"_string": "Elephant Archer"
},
"Bonuses":
{
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 2
}
}
},
"Ranged": {
"Damage": {
"Hack": 0,
"Pierce": 10,
"Crush": 0
},
"MinRange": 10,
"MaxRange": 80,
"PrepareTime": 300,
"RepeatTime": 500,
"Projectile": {
"Speed": 10,
"Spread": 2,
"Gravity": 1,
"FriendlyFire": "false"
},
"PreferredClasses": {
"_string": "Archer"
},
"RestrictedClasses": {
"_string": "Elephant"
},
"Splash": {
"Shape": "Circular",
"Range": 10,
"FriendlyFire": "false",
"Damage": {
"Hack": 0.0,
"Pierce": 15.0,
"Crush": 35.0
},
"Bonuses": {
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 3
}
}
}
},
"Capture": {
"Capture": 8,
"MaxRange": 10,
},
"Slaughter": {},
"StatusEffect": {
"ApplyStatus": {
"StatusInternalName": {
"StatusName": "StatusShownName",
"ApplierTooltip": "ApplierTooltip",
"ReceiverTooltip": "ReceiverTooltip",
"Duration": 5000,
"Stackability": "Stacks",
"Modifiers": {
"SE": {
"Paths": {
"_string": "Health/Max"
},
"Affects": {
"_string": "Unit"
},
"Add": 10
}
}
}
},
"MinRange": "10",
"MaxRange": "80"
}
});
const defender = ++entityID;
AddMock(defender, IID_Identity, {
"GetClassesList": () => [defenderClass],
"HasClass": className => className == defenderClass,
"GetCiv": () => "civ"
});
AddMock(defender, IID_Ownership, {
"GetOwner": () => 1
});
AddMock(defender, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 0
});
AddMock(defender, IID_Health, {
"GetHitpoints": () => 100
});
AddMock(defender, IID_Resistance, {
});
test_function(attacker, cmpAttack, defender);
}
// Validate template getter functions
attackComponentTest(undefined, true, (attacker, cmpAttack, defender) =>
{
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(), ["Melee", "Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes([]), ["Melee", "Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged", "Capture"]), ["Melee", "Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged"]), ["Melee", "Ranged"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture"]), ["Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "!Melee"]), []);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee"]), ["Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee", "!Ranged"]), ["Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "!Ranged"]), ["Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "Melee", "!Ranged"]), ["Melee", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["Civilian"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80, "parabolic": true });
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 });
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), {
"Damage": {
"Hack": 0,
"Pierce": 10,
"Crush": 0
}
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), {
"Damage": {
"Hack": 0.0,
"Pierce": 15.0,
"Crush": 35.0
},
"Bonuses": {
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 3
}
}
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("StatusEffect"), {
"ApplyStatus": {
"StatusInternalName": {
"Duration": 5000,
"Interval": 0,
"Stackability": "Stacks",
"Modifiers": {
"SE": {
"Paths": {
"_string": "Health/Max"
},
"Affects": {
"_string": "Unit"
},
"Add": 10
}
}
}
}
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), {
"prepare": 300,
"repeat": 500
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Ranged"), 500);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Capture"), {
"prepare": 0,
"repeat": 1000
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRepeatTime("Capture"), 1000);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashData("Ranged"), {
"attackData": {
"Damage": {
"Hack": 0,
"Pierce": 15,
"Crush": 35,
},
"Bonuses": {
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 3
}
}
},
"friendlyFire": false,
"radius": 10,
"shape": "Circular"
});
});
for (const className of ["Infantry", "Cavalry"])
attackComponentTest(className, true, (attacker, cmpAttack, defender) =>
{
TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Melee").Bonuses.BonusCav.Multiplier, 2);
TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Capture").Bonuses || null, null);
const getAttackBonus = (s, t, e, splash) => AttackHelper.GetAttackBonus(s, e, t, cmpAttack.GetAttackEffectsData(t, splash).Bonuses || null);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Melee", defender), className == "Cavalry" ? 2 : 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender), 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender, true), className == "Cavalry" ? 3 : 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Capture", defender), 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Slaughter", defender), 1);
});
// CanAttack rejects elephant attack due to RestrictedClasses
attackComponentTest("Elephant", true, (attacker, cmpAttack, defender) =>
{
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false);
});
function testGetBestAttackAgainst(defenderClass, bestAttack, bestAllyAttack, isBuilding = false)
{
attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) =>
{
if (isBuilding)
AddMock(defender, IID_Capturable, {
"CanCapture": playerID =>
{
TS_ASSERT_EQUALS(playerID, 1);
return true;
}
});
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), defenderClass != "Archer");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false);
const allowCapturing = [true];
if (!isBuilding)
allowCapturing.push(false);
for (const ac of allowCapturing)
TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack);
});
attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) =>
{
if (isBuilding)
AddMock(defender, IID_Capturable, {
"CanCapture": playerID =>
{
TS_ASSERT_EQUALS(playerID, 1);
return true;
}
});
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), false);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false);
const allowCapturing = [true];
if (!isBuilding)
allowCapturing.push(false);
for (const ac of allowCapturing)
TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAllyAttack);
});
}
testGetBestAttackAgainst("Civilian", "Melee", undefined);
testGetBestAttackAgainst("Archer", "Ranged", undefined);
testGetBestAttackAgainst("Domestic", "Slaughter", "Slaughter");
testGetBestAttackAgainst("Structure", "Capture", "Capture", true);
testGetBestAttackAgainst("Structure", "Ranged", undefined, false);
function testAttackPreference()
{
const attacker = 5;
const cmpAttack = ConstructComponent(attacker, "Attack", {
"Melee": {
"Damage": {
"Crush": 0
},
"MinRange": 3,
"MaxRange": 5,
"PreferredClasses": {
"_string": "Civilian Unit+!Ship"
},
"RestrictedClasses": {
"_string": "Elephant Archer"
},
}
});
AddMock(attacker+1, IID_Identity, {
"GetClassesList": () => ["Civilian", "Unit"]
});
AddMock(attacker+2, IID_Identity, {
"GetClassesList": () => ["Unit"]
});
AddMock(attacker+3, IID_Identity, {
"GetClassesList": () => ["Unit", "Ship"]
});
AddMock(attacker+4, IID_Identity, {
"GetClassesList": () => ["SomethingElse"]
});
TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+1), 0);
TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+2), 1);
TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+3), undefined);
TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+4), undefined);
}
testAttackPreference();
function testCanEverReachTarget()
{
const attacker = ++entityID;
AddMock(attacker, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 0,
"GetPosition2D": () => new Vector2D(1, 2)
});
const cmpAttack = ConstructComponent(attacker, "Attack", {
"Melee": {
"Damage": { "Hack": 10, "Pierce": 0, "Crush": 0 },
"MaxRange": 5
},
"Ranged": {
"Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 },
"MaxRange": 30,
"Projectile": { "Speed": 50, "Spread": 1, "Gravity": 1, "FriendlyFire": "false" }
}
});
// Melee target within 3D range
{
const defender = ++entityID;
AddMock(defender, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 0
});
TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Melee"), true);
}
// Melee target too high
{
const defender = ++entityID;
AddMock(defender, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 10
});
TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Melee"), false);
}
// Melee target at same height, within range (close distance)
{
const defender = ++entityID;
AddMock(defender, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 4
});
// sqrt(0² + 4²) = 4 <= 5
TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Melee"), true);
}
// Ranged: target at same height — reachable from current position (check 1)
{
const defender = ++entityID;
AddMock(defender, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 0,
"GetPosition": () => new Vector3D(1, 0, 2)
});
// Need RangeManager mock for IsTargetInRange (check 1) to work
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"GetEffectiveParabolicRange": () => 25,
"GetMaxReachableParabolicHeight": () => 15
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": () => true
});
TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Ranged"), true);
}
// Ranged: target too high for parabolic arc even at closest approach (check 2)
{
const defender = ++entityID;
AddMock(defender, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 20,
"GetPosition": () => new Vector3D(1, 20, 2)
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"GetEffectiveParabolicRange": () => -1, // out of range
"GetMaxReachableParabolicHeight": () => 10
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": () => false
});
// heightDiff = 20 - 0 = 20, maxReachableHeightDiff = 10 → unreachable
TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Ranged"), false);
}
}
testCanEverReachTarget();