mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
1262 lines
33 KiB
JavaScript
1262 lines
33 KiB
JavaScript
Engine.LoadHelperScript("Player.js");
|
|
Engine.LoadHelperScript("Position.js");
|
|
Engine.LoadHelperScript("Sound.js");
|
|
Engine.LoadComponentScript("interfaces/Auras.js");
|
|
Engine.LoadComponentScript("interfaces/Builder.js");
|
|
Engine.LoadComponentScript("interfaces/BuildingAI.js");
|
|
Engine.LoadComponentScript("interfaces/Capturable.js");
|
|
Engine.LoadComponentScript("interfaces/Diplomacy.js");
|
|
Engine.LoadComponentScript("interfaces/Garrisonable.js");
|
|
Engine.LoadComponentScript("interfaces/Resistance.js");
|
|
Engine.LoadComponentScript("interfaces/Formation.js");
|
|
Engine.LoadComponentScript("interfaces/Heal.js");
|
|
Engine.LoadComponentScript("interfaces/Health.js");
|
|
Engine.LoadComponentScript("interfaces/Pack.js");
|
|
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
|
|
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
|
|
Engine.LoadComponentScript("interfaces/Timer.js");
|
|
Engine.LoadComponentScript("interfaces/Turretable.js");
|
|
Engine.LoadComponentScript("interfaces/UnitAI.js");
|
|
Engine.LoadComponentScript("Formation.js");
|
|
Engine.LoadComponentScript("UnitAI.js");
|
|
|
|
/**
|
|
* Fairly straightforward test that entity renaming is handled
|
|
* by unitAI states. These ought to be augmented with integration tests, ideally.
|
|
*/
|
|
function TestTargetEntityRenaming(init_state, post_state, setup)
|
|
{
|
|
ResetState();
|
|
const player_ent = 5;
|
|
const target_ent = 6;
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_Timer, {
|
|
"SetInterval": () => {},
|
|
"SetTimeout": () => {}
|
|
});
|
|
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
|
"IsInTargetRange": () => false
|
|
});
|
|
|
|
const unitAI = ConstructComponent(player_ent, "UnitAI", {
|
|
"FormationController": "false",
|
|
"DefaultStance": "aggressive",
|
|
"FleeDistance": 10
|
|
});
|
|
unitAI.OnCreate();
|
|
|
|
setup(unitAI, player_ent, target_ent);
|
|
|
|
TS_ASSERT_EQUALS(unitAI.GetCurrentState(), init_state);
|
|
|
|
unitAI.OnGlobalEntityRenamed({
|
|
"entity": target_ent,
|
|
"newentity": target_ent + 1
|
|
});
|
|
|
|
TS_ASSERT_EQUALS(unitAI.GetCurrentState(), post_state);
|
|
}
|
|
|
|
TestTargetEntityRenaming(
|
|
"INDIVIDUAL.GARRISON.APPROACHING", "INDIVIDUAL.IDLE",
|
|
(unitAI, player_ent, target_ent) =>
|
|
{
|
|
unitAI.CanGarrison = (target) => target == target_ent;
|
|
unitAI.MoveToTargetRange = (target) => target == target_ent;
|
|
unitAI.AbleToMove = () => true;
|
|
|
|
unitAI.Garrison(target_ent, false);
|
|
}
|
|
);
|
|
|
|
TestTargetEntityRenaming(
|
|
"INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.REPAIR.REPAIRING",
|
|
(unitAI, player_ent, target_ent) =>
|
|
{
|
|
|
|
AddMock(player_ent, IID_Builder, {
|
|
"StartRepairing": () => true,
|
|
"StopRepairing": () => {}
|
|
});
|
|
|
|
QueryBuilderListInterface = () => {};
|
|
unitAI.CheckTargetRange = () => true;
|
|
unitAI.CanRepair = (target) => target == target_ent;
|
|
|
|
unitAI.Repair(target_ent, false, false);
|
|
}
|
|
);
|
|
|
|
|
|
TestTargetEntityRenaming(
|
|
"INDIVIDUAL.FLEEING", "INDIVIDUAL.FLEEING",
|
|
(unitAI, player_ent, target_ent) =>
|
|
{
|
|
PositionHelper.DistanceBetweenEntities = () => 10;
|
|
unitAI.CheckTargetRangeExplicit = () => false;
|
|
|
|
AddMock(player_ent, IID_UnitMotion, {
|
|
"MoveToTargetRange": () => true,
|
|
"GetRunMultiplier": () => 1,
|
|
"SetSpeedMultiplier": () => {},
|
|
"GetAcceleration": () => 1,
|
|
"StopMoving": () => {}
|
|
});
|
|
|
|
unitAI.Flee(target_ent, false);
|
|
}
|
|
);
|
|
|
|
/* Regression test.
|
|
* Tests the FSM behaviour of a unit when walking as part of a formation,
|
|
* then exiting the formation.
|
|
* mode == 0: There is no enemy unit nearby.
|
|
* mode == 1: There is a live enemy unit nearby.
|
|
* mode == 2: There is a dead enemy unit nearby.
|
|
*/
|
|
function TestFormationExiting(mode)
|
|
{
|
|
ResetState();
|
|
|
|
var playerEntity = 5;
|
|
var unit = 10;
|
|
var enemy = 20;
|
|
var controller = 30;
|
|
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_Timer, {
|
|
"SetInterval": function() { },
|
|
"SetTimeout": function() { },
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
|
"CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize)
|
|
{
|
|
return 1;
|
|
},
|
|
"EnableActiveQuery": function(id) { },
|
|
"ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; },
|
|
"DisableActiveQuery": function(id) { },
|
|
"GetEntityFlagMask": function(identifier) { },
|
|
"GetLosVisibility": (ent, player) => "visible"
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
|
"GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
|
"GetPlayerByID": function(id) { return playerEntity; },
|
|
"GetNumPlayers": function() { return 2; },
|
|
});
|
|
|
|
AddMock(playerEntity, IID_Diplomacy, {
|
|
"IsAlly": function() { return false; },
|
|
"IsEnemy": function() { return true; },
|
|
"GetEnemies": function() { return [2]; },
|
|
});
|
|
|
|
var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
|
|
|
|
AddMock(unit, IID_Identity, {
|
|
"GetClassesList": function() { return []; },
|
|
});
|
|
|
|
AddMock(unit, IID_Ownership, {
|
|
"GetOwner": function() { return 1; },
|
|
});
|
|
|
|
AddMock(unit, IID_Position, {
|
|
"GetTurretParent": function() { return INVALID_ENTITY; },
|
|
"GetPosition": function() { return new Vector3D(); },
|
|
"GetPosition2D": function() { return new Vector2D(); },
|
|
"GetRotation": function() { return { "y": 0 }; },
|
|
"IsInWorld": function() { return true; },
|
|
});
|
|
|
|
AddMock(unit, IID_UnitMotion, {
|
|
"GetWalkSpeed": () => 1,
|
|
"GetAcceleration": () => 1,
|
|
"SetSpeedMultiplier": () => {},
|
|
"MoveToFormationOffset": (target, x, z) => {},
|
|
"MoveToTargetRange": (target, min, max) => true,
|
|
"PossiblyAtDestination": () => false,
|
|
"SetMemberOfFormation": () => {},
|
|
"StopMoving": () => {},
|
|
"SetFacePointAfterMove": () => {},
|
|
"GetFacePointAfterMove": () => true,
|
|
"GetPassabilityClassName": () => "default"
|
|
});
|
|
|
|
AddMock(unit, IID_Vision, {
|
|
"GetRange": function() { return 10; },
|
|
});
|
|
|
|
AddMock(unit, IID_Attack, {
|
|
"GetRange": function() { return { "max": 10, "min": 0 }; },
|
|
"GetFullAttackRange": function() { return { "max": 40, "min": 0 }; },
|
|
"GetBestAttackAgainst": function(t) { return "melee"; },
|
|
"GetPreference": function(t) { return 0; },
|
|
"GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; },
|
|
"CanAttack": function(v) { return true; },
|
|
"CompareEntitiesByPreference": function(a, b) { return 0; },
|
|
"IsTargetInRange": () => true,
|
|
"StartAttacking": () => true
|
|
});
|
|
|
|
unitAI.OnCreate();
|
|
|
|
unitAI.SetupAttackRangeQuery(1);
|
|
|
|
|
|
if (mode == 1)
|
|
{
|
|
AddMock(enemy, IID_Health, {
|
|
"GetHitpoints": function() { return 10; },
|
|
});
|
|
AddMock(enemy, IID_UnitAI, {
|
|
"IsAnimal": () => "false",
|
|
"IsDangerousAnimal": () => "false"
|
|
});
|
|
}
|
|
else if (mode == 2)
|
|
AddMock(enemy, IID_Health, {
|
|
"GetHitpoints": function() { return 0; },
|
|
});
|
|
|
|
const controllerFormation = ConstructComponent(controller, "Formation", {
|
|
"FormationShape": "square",
|
|
"ShiftRows": "false",
|
|
"SortingClasses": "",
|
|
"WidthDepthRatio": 1,
|
|
"UnitSeparationWidthMultiplier": 1,
|
|
"UnitSeparationDepthMultiplier": 1,
|
|
"SpeedMultiplier": 1,
|
|
"Sloppiness": 0
|
|
});
|
|
const controllerAI = ConstructComponent(controller, "UnitAI", {
|
|
"FormationController": "true",
|
|
"DefaultStance": "aggressive"
|
|
});
|
|
|
|
AddMock(controller, IID_Position, {
|
|
"JumpTo": function(x, z) { this.x = x; this.z = z; },
|
|
"TurnTo": function() {},
|
|
"GetTurretParent": function() { return INVALID_ENTITY; },
|
|
"GetPosition": function() { return new Vector3D(this.x, 0, this.z); },
|
|
"GetPosition2D": function() { return new Vector2D(this.x, this.z); },
|
|
"GetRotation": function() { return { "y": 0 }; },
|
|
"IsInWorld": function() { return true; },
|
|
"MoveOutOfWorld": () => {}
|
|
});
|
|
|
|
AddMock(controller, IID_UnitMotion, {
|
|
"GetWalkSpeed": () => 1,
|
|
"StopMoving": () => {},
|
|
"SetSpeedMultiplier": () => {},
|
|
"SetAcceleration": (accel) => {},
|
|
"SetPassabilityClassName": (name) => {},
|
|
"MoveToPointRange": () => true,
|
|
"SetFacePointAfterMove": () => {},
|
|
"GetFacePointAfterMove": () => true,
|
|
"GetPassabilityClassName": () => "default"
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_Pathfinder, {
|
|
"GetClearance": () => 1,
|
|
"GetPassabilityClass": () => 16
|
|
});
|
|
|
|
controllerAI.OnCreate();
|
|
|
|
|
|
TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.IDLE");
|
|
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
|
|
|
|
controllerFormation.SetMembers([unit]);
|
|
controllerAI.Walk(100, 100, false);
|
|
|
|
TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.WALKING");
|
|
TS_ASSERT_EQUALS(unitAI.fsmStateName, "FORMATIONMEMBER.WALKING");
|
|
|
|
controllerFormation.Disband();
|
|
|
|
unitAI.UnitFsm.ProcessMessage(unitAI, { "type": "Timer" });
|
|
|
|
if (mode == 0)
|
|
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
|
|
else if (mode == 1)
|
|
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
|
|
else if (mode == 2)
|
|
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
|
|
else
|
|
TS_FAIL("invalid mode");
|
|
}
|
|
|
|
function TestMoveIntoFormationWhileAttacking()
|
|
{
|
|
ResetState();
|
|
|
|
var playerEntity = 5;
|
|
var controller = 10;
|
|
var enemy = 20;
|
|
var unit = 30;
|
|
var units = [];
|
|
var unitCount = 8;
|
|
var unitAIs = [];
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_Timer, {
|
|
"SetInterval": function() { },
|
|
"SetTimeout": function() { },
|
|
});
|
|
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
|
"CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize)
|
|
{
|
|
return 1;
|
|
},
|
|
"EnableActiveQuery": function(id) { },
|
|
"ResetActiveQuery": function(id) { return [enemy]; },
|
|
"DisableActiveQuery": function(id) { },
|
|
"GetEntityFlagMask": function(identifier) { },
|
|
"GetLosVisibility": (target, player) => "visible",
|
|
"GetLosVisibilityPosition": (x, z, player) => "visible"
|
|
});
|
|
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
|
"GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
|
"GetPlayerByID": function(id) { return playerEntity; },
|
|
"GetNumPlayers": function() { return 2; },
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
|
"IsInTargetRange": (ent, target, min, max) => true
|
|
});
|
|
|
|
AddMock(playerEntity, IID_Diplomacy, {
|
|
"IsAlly": function() { return false; },
|
|
"IsEnemy": function() { return true; },
|
|
"GetEnemies": function() { return [2]; },
|
|
});
|
|
|
|
// create units
|
|
for (var i = 0; i < unitCount; i++)
|
|
{
|
|
|
|
units.push(unit + i);
|
|
|
|
var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
|
|
|
|
AddMock(unit + i, IID_Identity, {
|
|
"GetClassesList": function() { return []; },
|
|
});
|
|
|
|
AddMock(unit + i, IID_Ownership, {
|
|
"GetOwner": function() { return 1; },
|
|
});
|
|
|
|
AddMock(unit + i, IID_Position, {
|
|
"GetTurretParent": function() { return INVALID_ENTITY; },
|
|
"GetPosition": function() { return new Vector3D(); },
|
|
"GetPosition2D": function() { return new Vector2D(); },
|
|
"GetRotation": function() { return { "y": 0 }; },
|
|
"IsInWorld": function() { return true; },
|
|
});
|
|
|
|
AddMock(unit + i, IID_UnitMotion, {
|
|
"GetWalkSpeed": () => 1,
|
|
"GetAcceleration": () => 1,
|
|
"SetSpeedMultiplier": () => {},
|
|
"MoveToFormationOffset": (target, x, z) => {},
|
|
"MoveToTargetRange": (target, min, max) => true,
|
|
"PossiblyAtDestination": () => false,
|
|
"SetMemberOfFormation": () => {},
|
|
"StopMoving": () => {},
|
|
"SetFacePointAfterMove": () => {},
|
|
"GetFacePointAfterMove": () => true,
|
|
"GetPassabilityClassName": () => "default"
|
|
});
|
|
|
|
AddMock(unit + i, IID_Vision, {
|
|
"GetRange": function() { return 10; },
|
|
});
|
|
|
|
AddMock(unit + i, IID_Attack, {
|
|
"GetRange": function() { return { "max": 10, "min": 0 }; },
|
|
"GetFullAttackRange": function() { return { "max": 40, "min": 0 }; },
|
|
"GetBestAttackAgainst": function(t) { return "melee"; },
|
|
"GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; },
|
|
"CanAttack": function(v) { return true; },
|
|
"CompareEntitiesByPreference": function(a, b) { return 0; },
|
|
"IsTargetInRange": () => true,
|
|
"StartAttacking": () => true,
|
|
"StopAttacking": () => {}
|
|
});
|
|
|
|
unitAI.OnCreate();
|
|
|
|
unitAI.SetupAttackRangeQuery(1);
|
|
|
|
unitAIs.push(unitAI);
|
|
}
|
|
|
|
// create enemy
|
|
AddMock(enemy, IID_Health, {
|
|
"GetHitpoints": function() { return 40; },
|
|
});
|
|
|
|
AddMock(enemy, IID_Ownership, {
|
|
"GetOwner": () => 2 // Different player ID (enemy)
|
|
});
|
|
|
|
const controllerFormation = ConstructComponent(controller, "Formation", {
|
|
"FormationShape": "square",
|
|
"ShiftRows": "false",
|
|
"SortingClasses": "",
|
|
"WidthDepthRatio": 1,
|
|
"UnitSeparationWidthMultiplier": 1,
|
|
"UnitSeparationDepthMultiplier": 1,
|
|
"SpeedMultiplier": 1,
|
|
"Sloppiness": 0
|
|
});
|
|
const controllerAI = ConstructComponent(controller, "UnitAI", {
|
|
"FormationController": "true",
|
|
"DefaultStance": "aggressive"
|
|
});
|
|
|
|
AddMock(controller, IID_Ownership, {
|
|
"GetOwner": () => 1
|
|
});
|
|
|
|
AddMock(controller, IID_Position, {
|
|
"GetTurretParent": () => INVALID_ENTITY,
|
|
"JumpTo": function(x, z) { this.x = x; this.z = z; },
|
|
"TurnTo": function() {},
|
|
"GetPosition": function(){ return new Vector3D(this.x, 0, this.z); },
|
|
"GetPosition2D": function(){ return new Vector2D(this.x, this.z); },
|
|
"GetRotation": () => ({ "y": 0 }),
|
|
"IsInWorld": () => true,
|
|
"MoveOutOfWorld": () => {},
|
|
});
|
|
|
|
AddMock(controller, IID_UnitMotion, {
|
|
"GetWalkSpeed": () => 1,
|
|
"SetSpeedMultiplier": (speed) => {},
|
|
"SetAcceleration": (accel) => {},
|
|
"SetPassabilityClassName": (name) => {},
|
|
"MoveToPointRange": (x, z, minRange, maxRange) => {},
|
|
"StopMoving": () => {},
|
|
"SetFacePointAfterMove": () => {},
|
|
"GetFacePointAfterMove": () => true,
|
|
"GetPassabilityClassName": () => "default"
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_Pathfinder, {
|
|
"GetClearance": () => 1,
|
|
"GetPassabilityClass": () => 16
|
|
});
|
|
|
|
AddMock(controller, IID_Attack, {
|
|
"GetRange": function() { return { "max": 10, "min": 0 }; },
|
|
"CanAttackAsFormation": function() { return false; },
|
|
});
|
|
|
|
controllerAI.OnCreate();
|
|
|
|
controllerFormation.SetMembers(units);
|
|
|
|
controllerAI.Attack(enemy);
|
|
|
|
for (const ent of unitAIs)
|
|
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
|
|
|
|
controllerAI.MoveIntoFormation({ "name": "Circle" });
|
|
|
|
// let all units be in position
|
|
for (const ent of unitAIs)
|
|
controllerFormation.SetFinishedEntity(ent);
|
|
|
|
for (const ent of unitAIs)
|
|
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
|
|
|
|
controllerFormation.Disband();
|
|
}
|
|
|
|
TestFormationExiting(0);
|
|
TestFormationExiting(1);
|
|
TestFormationExiting(2);
|
|
|
|
TestMoveIntoFormationWhileAttacking();
|
|
|
|
|
|
function TestWalkAndFightTargets()
|
|
{
|
|
const ent = 10;
|
|
const unitAI = ConstructComponent(ent, "UnitAI", {
|
|
"FormationController": "false",
|
|
"DefaultStance": "aggressive",
|
|
"FleeDistance": 10
|
|
});
|
|
unitAI.OnCreate();
|
|
unitAI.losAttackRangeQuery = true;
|
|
|
|
// The result is stored here
|
|
let result;
|
|
unitAI.PushOrderFront = function(type, order)
|
|
{
|
|
if (type === "Attack" && order?.target)
|
|
result = order.target;
|
|
};
|
|
|
|
// Create some targets.
|
|
AddMock(ent+1, IID_UnitAI, { "IsAnimal": () => true, "IsDangerousAnimal": () => false });
|
|
AddMock(ent+2, IID_Ownership, { "GetOwner": () => 2 });
|
|
AddMock(ent+3, IID_Ownership, { "GetOwner": () => 2 });
|
|
AddMock(ent+4, IID_Ownership, { "GetOwner": () => 2 });
|
|
AddMock(ent+5, IID_Ownership, { "GetOwner": () => 2 });
|
|
AddMock(ent+6, IID_Ownership, { "GetOwner": () => 2 });
|
|
AddMock(ent+7, IID_Ownership, { "GetOwner": () => 2 });
|
|
|
|
unitAI.CanAttack = function(target)
|
|
{
|
|
return target !== ent+2 && target !== ent+7;
|
|
};
|
|
|
|
AddMock(ent, IID_Attack, {
|
|
"GetPreference": (target) => ({
|
|
[ent+4]: 0,
|
|
[ent+5]: 1,
|
|
[ent+6]: 2,
|
|
[ent+7]: 0
|
|
}?.[target])
|
|
});
|
|
|
|
const runTest = function(ents, res)
|
|
{
|
|
result = undefined;
|
|
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
|
"ResetActiveQuery": () => ents
|
|
});
|
|
TS_ASSERT_EQUALS(unitAI.FindWalkAndFightTargets(), !!res);
|
|
TS_ASSERT_EQUALS(result, res);
|
|
};
|
|
|
|
// No entities.
|
|
runTest([]);
|
|
|
|
// Entities that cannot be attacked.
|
|
runTest([ent+1, ent+2, ent+7]);
|
|
|
|
// No preference, one attackable entity.
|
|
runTest([ent+1, ent+2, ent+3], ent+3);
|
|
|
|
// Check preferences.
|
|
runTest([ent+1, ent+2, ent+3, ent+4], ent+4);
|
|
runTest([ent+1, ent+2, ent+3, ent+4, ent+5], ent+4);
|
|
runTest([ent+1, ent+2, ent+6, ent+3, ent+4, ent+5], ent+4);
|
|
runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+4, ent+5], ent+4);
|
|
runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+5], ent+5);
|
|
runTest([ent+1, ent+2, ent+7, ent+6, ent+3], ent+6);
|
|
runTest([ent+1, ent+2, ent+7, ent+3], ent+3);
|
|
}
|
|
|
|
TestWalkAndFightTargets();
|
|
|
|
function TestAttemptObstructionMitigation()
|
|
{
|
|
ResetState();
|
|
|
|
const controllerID = 100;
|
|
const member1ID = 201;
|
|
const member2ID = 202;
|
|
const member3ID = 203;
|
|
const playerID = 1;
|
|
const playerEntity = 5;
|
|
|
|
// Helper function to create timer mocks
|
|
function createTimerMock()
|
|
{
|
|
let timerId = null;
|
|
let canceledTimer = null;
|
|
|
|
return {
|
|
"SetTimeout": function(entity, iid, functionName, time, data)
|
|
{
|
|
timerId = 123;
|
|
return timerId;
|
|
},
|
|
"CancelTimer": function(id)
|
|
{
|
|
canceledTimer = id;
|
|
},
|
|
"SetInterval": function() { return null; }
|
|
};
|
|
}
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_Timer, createTimerMock(true));
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
|
"GetPlayerByID": id => playerEntity,
|
|
"GetNumPlayers": () => 2
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_Pathfinder, {
|
|
"GetClearance": () => 1,
|
|
"GetPassabilityClass": () => 16
|
|
});
|
|
|
|
AddMock(playerEntity, IID_Player, {
|
|
"GetPlayerID": () => playerID
|
|
});
|
|
|
|
// Create controller UnitAI
|
|
const controllerAI = ConstructComponent(controllerID, "UnitAI", {
|
|
"FormationController": "true",
|
|
"DefaultStance": "aggressive"
|
|
});
|
|
|
|
// Mock controller position
|
|
let controllerX = 0;
|
|
let controllerZ = 0;
|
|
AddMock(controllerID, IID_Position, {
|
|
"IsInWorld": () => true,
|
|
"GetPosition": () => new Vector3D(controllerX, 0, controllerZ),
|
|
"GetPosition2D": () => new Vector2D(controllerX, controllerZ),
|
|
"GetRotation": () => ({ "y": 0 }),
|
|
"GetTurretParent": () => INVALID_ENTITY,
|
|
"TurnTo": () => {},
|
|
"JumpTo": function(x, z)
|
|
{
|
|
controllerX = x;
|
|
controllerZ = z;
|
|
},
|
|
"MoveOutOfWorld": () => {}
|
|
});
|
|
|
|
AddMock(controllerID, IID_Ownership, {
|
|
"GetOwner": () => playerID,
|
|
});
|
|
|
|
AddMock(controllerID, IID_UnitMotion, {
|
|
"GetWalkSpeed": () => 1,
|
|
"GetAcceleration": () => 1,
|
|
"StopMoving": () => {},
|
|
"MoveToTargetRange": () => true,
|
|
"MoveToPointRange": () => true,
|
|
"SetSpeedMultiplier": () => {},
|
|
"SetAcceleration": () => {},
|
|
"SetPassabilityClassName": () => {},
|
|
"SetFacePointAfterMove": () => {},
|
|
"GetFacePointAfterMove": () => true,
|
|
"GetPassabilityClassName": () => "default",
|
|
"SetMemberOfFormation": () => {},
|
|
"FaceTowardsPoint": () => {}
|
|
});
|
|
|
|
controllerAI.OnCreate();
|
|
|
|
// Helper function to reset controller position
|
|
function resetControllerPosition(x = 0, z = 0)
|
|
{
|
|
controllerX = x;
|
|
controllerZ = z;
|
|
}
|
|
|
|
// Helper function to test obstruction mitigation
|
|
function testObstructionMitigation(formationMock, orderData, expectedX, expectedZ, shouldJump)
|
|
{
|
|
resetControllerPosition();
|
|
|
|
if (formationMock)
|
|
{
|
|
AddMock(controllerID, IID_Formation, formationMock);
|
|
}
|
|
|
|
controllerAI.order = { "data": orderData };
|
|
|
|
// Clear any previous flags
|
|
delete controllerAI.obstructionMitigationAttempted;
|
|
delete controllerAI.obstructionMitigationTimer;
|
|
|
|
controllerAI.AttemptObstructionMitigation();
|
|
|
|
TS_ASSERT_EQUALS(controllerX, expectedX);
|
|
TS_ASSERT_EQUALS(controllerZ, expectedZ);
|
|
|
|
if (shouldJump)
|
|
{
|
|
TS_ASSERT(controllerAI.obstructionMitigationAttempted);
|
|
TS_ASSERT_EQUALS(controllerAI.obstructionMitigationTimer, 123);
|
|
}
|
|
}
|
|
|
|
// Should not execute if already attempted
|
|
(function()
|
|
{
|
|
controllerAI.obstructionMitigationAttempted = true;
|
|
const originalX = controllerX;
|
|
const originalZ = controllerZ;
|
|
|
|
controllerAI.order = { "data": { "x": 100, "z": 100 } };
|
|
controllerAI.AttemptObstructionMitigation();
|
|
|
|
TS_ASSERT_EQUALS(controllerX, originalX);
|
|
TS_ASSERT_EQUALS(controllerZ, originalZ);
|
|
|
|
delete controllerAI.obstructionMitigationAttempted;
|
|
})();
|
|
|
|
// Should not execute without formation component
|
|
(function()
|
|
{
|
|
const originalX = controllerX;
|
|
const originalZ = controllerZ;
|
|
|
|
controllerAI.order = { "data": { "x": 100, "z": 100 } };
|
|
controllerAI.AttemptObstructionMitigation();
|
|
|
|
TS_ASSERT_EQUALS(controllerX, originalX);
|
|
TS_ASSERT_EQUALS(controllerZ, originalZ);
|
|
})();
|
|
|
|
// Should not execute without valid destination
|
|
(function()
|
|
{
|
|
const formationMock = { "GetClosestMemberToPosition": () => member1ID };
|
|
|
|
// Test with missing destination
|
|
testObstructionMitigation(formationMock, {}, 0, 0, false);
|
|
|
|
// Test with undefined x
|
|
testObstructionMitigation(formationMock, { "z": 100 }, 0, 0, false);
|
|
|
|
// Test with undefined z
|
|
testObstructionMitigation(formationMock, { "x": 100 }, 0, 0, false);
|
|
})();
|
|
|
|
// Should not execute if no closest member found
|
|
(function()
|
|
{
|
|
const formationMock = { "GetClosestMemberToPosition": () => INVALID_ENTITY };
|
|
testObstructionMitigation(formationMock, { "x": 100, "z": 100 }, 0, 0, false);
|
|
})();
|
|
|
|
// Should not execute if member or controller missing position component
|
|
(function()
|
|
{
|
|
const formationMock = { "GetClosestMemberToPosition": () => member1ID };
|
|
testObstructionMitigation(formationMock, { "x": 100, "z": 100 }, 0, 0, false);
|
|
})();
|
|
|
|
// Should jump when member is more than 2 meters closer to destination
|
|
(function()
|
|
{
|
|
AddMock(member1ID, IID_Position, {
|
|
"GetPosition2D": () => new Vector2D(90, 90)
|
|
});
|
|
|
|
const formationMock = {
|
|
"GetClosestMemberToPosition": () => member1ID
|
|
};
|
|
|
|
testObstructionMitigation(formationMock, { "x": 100, "z": 100 }, 90, 90, true);
|
|
})();
|
|
|
|
// Should NOT jump when member is NOT more than 2 meters closer
|
|
(function()
|
|
{
|
|
AddMock(member1ID, IID_Position, {
|
|
"GetPosition2D": () => new Vector2D(95, 96)
|
|
});
|
|
|
|
const formationMock = {
|
|
"GetClosestMemberToPosition": () => member1ID
|
|
};
|
|
|
|
controllerX = 95;
|
|
controllerZ = 95;
|
|
|
|
controllerAI.order = { "data": { "x": 100, "z": 100 } };
|
|
delete controllerAI.obstructionMitigationAttempted;
|
|
|
|
controllerAI.AttemptObstructionMitigation();
|
|
|
|
TS_ASSERT_EQUALS(controllerX, 95);
|
|
TS_ASSERT_EQUALS(controllerZ, 95);
|
|
TS_ASSERT(controllerAI.obstructionMitigationAttempted);
|
|
})();
|
|
|
|
// Should NOT jump when member is actually farther away
|
|
(function()
|
|
{
|
|
AddMock(member1ID, IID_Position, {
|
|
"GetPosition2D": () => new Vector2D(0, 0)
|
|
});
|
|
|
|
const formationMock = {
|
|
"GetClosestMemberToPosition": () => member1ID
|
|
};
|
|
|
|
controllerX = 95;
|
|
controllerZ = 95;
|
|
|
|
controllerAI.order = { "data": { "x": 100, "z": 100 } };
|
|
delete controllerAI.obstructionMitigationAttempted;
|
|
|
|
controllerAI.AttemptObstructionMitigation();
|
|
|
|
TS_ASSERT_EQUALS(controllerX, 95);
|
|
TS_ASSERT_EQUALS(controllerZ, 95);
|
|
TS_ASSERT(controllerAI.obstructionMitigationAttempted);
|
|
})();
|
|
|
|
// Should jump when member is exactly 2.1 meters closer (edge case)
|
|
(function()
|
|
{
|
|
AddMock(member1ID, IID_Position, {
|
|
"GetPosition2D": () => new Vector2D(2, 1)
|
|
});
|
|
|
|
const formationMock = {
|
|
"GetClosestMemberToPosition": () => member1ID
|
|
};
|
|
|
|
testObstructionMitigation(formationMock, { "x": 100, "z": 100 }, 2, 1, true);
|
|
})();
|
|
|
|
// Test SetObstructionMitigationFlag and ResetObstructionMitigationFlag
|
|
(function()
|
|
{
|
|
// Use SetTimeout version for this test
|
|
AddMock(SYSTEM_ENTITY, IID_Timer, createTimerMock(true));
|
|
|
|
controllerAI.SetObstructionMitigationFlag();
|
|
|
|
TS_ASSERT(controllerAI.obstructionMitigationAttempted);
|
|
TS_ASSERT_EQUALS(controllerAI.obstructionMitigationTimer, 123);
|
|
|
|
controllerAI.ResetObstructionMitigationFlag();
|
|
|
|
TS_ASSERT(!controllerAI.obstructionMitigationAttempted);
|
|
})();
|
|
|
|
// Multiple members, should pick closest one
|
|
(function()
|
|
{
|
|
const members = [member1ID, member2ID, member3ID];
|
|
|
|
AddMock(member1ID, IID_Position, {
|
|
"GetPosition2D": () => ({ "x": 80, "y": 80 }),
|
|
"IsInWorld": () => true
|
|
});
|
|
|
|
AddMock(member2ID, IID_Position, {
|
|
"GetPosition2D": () => ({ "x": 90, "y": 90 }),
|
|
"IsInWorld": () => true
|
|
});
|
|
|
|
AddMock(member3ID, IID_Position, {
|
|
"GetPosition2D": () => ({ "x": 50, "y": 50 }),
|
|
"IsInWorld": () => true
|
|
});
|
|
|
|
const formationMock = {
|
|
"GetClosestMemberToPosition": function(targetPosition, filter)
|
|
{
|
|
const memberPositions = {
|
|
[member1ID]: { "x": 80, "y": 80 },
|
|
[member2ID]: { "x": 90, "y": 90 },
|
|
[member3ID]: { "x": 50, "y": 50 }
|
|
};
|
|
|
|
let closestMember = INVALID_ENTITY;
|
|
let closestDistance = Infinity;
|
|
|
|
for (const member of members)
|
|
{
|
|
if (filter && !filter(member))
|
|
continue;
|
|
|
|
const memberPos = memberPositions[member];
|
|
if (!memberPos)
|
|
continue;
|
|
|
|
const dist = (targetPosition.x - memberPos.x) ** 2 + (targetPosition.y - memberPos.y) ** 2;
|
|
if (dist < closestDistance)
|
|
{
|
|
closestMember = member;
|
|
closestDistance = dist;
|
|
}
|
|
}
|
|
return closestMember;
|
|
}
|
|
};
|
|
|
|
testObstructionMitigation(formationMock, { "x": 100, "z": 100 }, 90, 90, true);
|
|
})();
|
|
}
|
|
|
|
TestAttemptObstructionMitigation();
|
|
function TestFindAndSetNextTargetBehavior()
|
|
{
|
|
ResetState();
|
|
|
|
const attacker = 10;
|
|
const targets = [20, 21, 22, 23, 24]; // Multiple targets for comprehensive testing
|
|
|
|
// Basic mocks needed for unitAI creation
|
|
AddMock(SYSTEM_ENTITY, IID_Timer, {
|
|
"SetInterval": () => {},
|
|
"SetTimeout": () => {}
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
|
"CreateActiveQuery": () => 1,
|
|
"EnableActiveQuery": () => {},
|
|
"ResetActiveQuery": () => [],
|
|
"DisableActiveQuery": () => {},
|
|
"GetEntityFlagMask": () => 0
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
|
"IsInTargetRange": () => false
|
|
});
|
|
|
|
// Create the unitAI component
|
|
const unitAI = ConstructComponent(attacker, "UnitAI", {
|
|
"FormationController": "false",
|
|
"DefaultStance": "aggressive"
|
|
});
|
|
|
|
AddMock(attacker, IID_Attack, {
|
|
"GetBestAttackAgainst": () => "Melee"
|
|
});
|
|
|
|
unitAI.OnCreate();
|
|
|
|
// Mock the visibility and attackability checks
|
|
unitAI.CanAttack = (target) =>
|
|
{
|
|
// Target 21 is always unattackable (e.g., garrisoned, dead, etc.)
|
|
return target !== 21;
|
|
};
|
|
|
|
unitAI.CheckTargetVisible = (target) =>
|
|
{
|
|
// Target 22 is always not visible
|
|
return target !== 22;
|
|
};
|
|
|
|
// First target is valid
|
|
(function()
|
|
{
|
|
const data = {
|
|
"targetArray": [20, 21, 22, 23, 24],
|
|
"currentTargetIndex": 0, // Currently targeting 20
|
|
"target": 20,
|
|
"allowCapture": false
|
|
};
|
|
|
|
const found = unitAI.FindAndSetNextTarget(data);
|
|
TS_ASSERT(found);
|
|
TS_ASSERT_EQUALS(data.target, 20);
|
|
TS_ASSERT_EQUALS(data.currentTargetIndex, 0);
|
|
})();
|
|
|
|
// End of array behavior
|
|
(function()
|
|
{
|
|
const data = {
|
|
"targetArray": [18, 19, 20, 21, 22],
|
|
"currentTargetIndex": 2, // Currently targeting 22
|
|
"allowCapture": false
|
|
};
|
|
|
|
// 21 and 22 are invalid, should go backward and find 20
|
|
const found = unitAI.FindAndSetNextTarget(data);
|
|
TS_ASSERT(found);
|
|
TS_ASSERT_EQUALS(data.target, 20); // Should be assigned targetArray[initialTarget - 1]
|
|
})();
|
|
|
|
// No valid targets found
|
|
(function()
|
|
{
|
|
const data = {
|
|
"targetArray": [21, 22], // Both are invalid (21 unattackable, 22 not visible)
|
|
"currentTargetIndex": 0,
|
|
"target": 21,
|
|
"allowCapture": false
|
|
};
|
|
|
|
const found = unitAI.FindAndSetNextTarget(data);
|
|
TS_ASSERT(!found);
|
|
TS_ASSERT_EQUALS(data.target, 21); // Should remain unchanged
|
|
TS_ASSERT_EQUALS(data.currentTargetIndex, 0);
|
|
})();
|
|
|
|
// Empty target array
|
|
(function()
|
|
{
|
|
const data = {
|
|
"targetArray": [],
|
|
"currentTargetIndex": 0,
|
|
"target": 20,
|
|
"allowCapture": false
|
|
};
|
|
|
|
const found = unitAI.FindAndSetNextTarget(data);
|
|
TS_ASSERT(!found);
|
|
TS_ASSERT_EQUALS(data.target, 20); // Should remain unchanged
|
|
TS_ASSERT_EQUALS(data.currentTargetIndex, void 0); // Should nullify currentTargetIndex
|
|
})();
|
|
|
|
// Current target is last in array
|
|
(function()
|
|
{
|
|
const data = {
|
|
"targetArray": [20, 21, 22, 23, 24],
|
|
"currentTargetIndex": 4, // Currently targeting 24 (last element)
|
|
"allowCapture": false
|
|
};
|
|
|
|
const found = unitAI.FindAndSetNextTarget(data);
|
|
TS_ASSERT(found);
|
|
TS_ASSERT_EQUALS(data.target, 24); // Should remain unchanged
|
|
TS_ASSERT_EQUALS(data.currentTargetIndex, 4);
|
|
})();
|
|
|
|
// Single invalid target
|
|
(function()
|
|
{
|
|
const data = {
|
|
"targetArray": [21], // Only one target, which is invalid
|
|
"currentTargetIndex": 0,
|
|
"allowCapture": false
|
|
};
|
|
|
|
const found = unitAI.FindAndSetNextTarget(data);
|
|
TS_ASSERT(!found);
|
|
})();
|
|
|
|
// Verify if we don't do more calls then necessary
|
|
(function()
|
|
{
|
|
const searchOrder = [];
|
|
|
|
unitAI.CanAttack = (target) =>
|
|
{
|
|
searchOrder.push(target);
|
|
return target == 20;
|
|
};
|
|
|
|
unitAI.CheckTargetVisible = (target) => true; // All visible
|
|
|
|
const data = {
|
|
"targetArray": [20, 21, 22, 23],
|
|
"currentTargetIndex": 1, // Currently targeting 21
|
|
"allowCapture": false
|
|
};
|
|
|
|
const found = unitAI.FindAndSetNextTarget(data);
|
|
TS_ASSERT(found);
|
|
TS_ASSERT_EQUALS(searchOrder.length, 4); // Should find at 20 (4 calls)
|
|
|
|
})();
|
|
}
|
|
|
|
TestFindAndSetNextTargetBehavior();
|
|
|
|
function TestAttackGroupBehavior()
|
|
{
|
|
ResetState();
|
|
|
|
const attacker1 = 100;
|
|
const attacker2 = 101;
|
|
const targets = [200, 201, 202, 203, 204];
|
|
|
|
// Basic mocks needed for unitAI creation
|
|
AddMock(SYSTEM_ENTITY, IID_Timer, {
|
|
"SetInterval": () => {},
|
|
"SetTimeout": () => {}
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
|
"CreateActiveQuery": () => 1,
|
|
"EnableActiveQuery": () => {},
|
|
"ResetActiveQuery": () => [],
|
|
"DisableActiveQuery": () => {},
|
|
"GetEntityFlagMask": () => 0
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
|
"IsInTargetRange": () => false
|
|
});
|
|
|
|
// Create two unitAI components
|
|
const unitAI1 = ConstructComponent(attacker1, "UnitAI", {
|
|
"FormationController": "false",
|
|
"DefaultStance": "aggressive"
|
|
});
|
|
|
|
const unitAI2 = ConstructComponent(attacker2, "UnitAI", {
|
|
"FormationController": "false",
|
|
"DefaultStance": "aggressive"
|
|
});
|
|
|
|
AddMock(attacker1, IID_Attack, {
|
|
"GetBestAttackAgainst": () => "Melee"
|
|
});
|
|
|
|
AddMock(attacker2, IID_Attack, {
|
|
"GetBestAttackAgainst": () => "Melee"
|
|
});
|
|
|
|
unitAI1.OnCreate();
|
|
unitAI2.OnCreate();
|
|
|
|
// Basic AttackGroup distribution
|
|
(function()
|
|
{
|
|
let orderAdded1 = null;
|
|
let orderAdded2 = null;
|
|
|
|
// Mock AddOrder to capture what orders are created
|
|
unitAI1.AddOrder = (type, data, queued, pushFront) =>
|
|
{
|
|
orderAdded1 = { type, data, queued, pushFront };
|
|
};
|
|
|
|
unitAI2.AddOrder = (type, data, queued, pushFront) =>
|
|
{
|
|
orderAdded2 = { type, data, queued, pushFront };
|
|
};
|
|
|
|
// Call AttackGroup for both units
|
|
unitAI1.AttackGroup(targets, 0, 2, false, false, false);
|
|
unitAI2.AttackGroup(targets, 1, 2, false, false, false);
|
|
|
|
// Verify unit1's order
|
|
TS_ASSERT(orderAdded1);
|
|
TS_ASSERT_EQUALS(orderAdded1.type, "Attack");
|
|
TS_ASSERT_EQUALS(orderAdded1.data.targetArray.length, 5);
|
|
TS_ASSERT_EQUALS(orderAdded1.data.target, INVALID_ENTITY);
|
|
// Unit 0 should start at position 1 ==> Math.round((0 + 0.5) * 2.5) = Math.round(1.25) = 1
|
|
TS_ASSERT_EQUALS(orderAdded1.data.currentTargetIndex, 1);
|
|
|
|
// Verify unit2's order
|
|
TS_ASSERT(orderAdded2);
|
|
TS_ASSERT_EQUALS(orderAdded2.type, "Attack");
|
|
TS_ASSERT_EQUALS(orderAdded2.data.targetArray.length, 5);
|
|
TS_ASSERT_EQUALS(orderAdded2.data.target, INVALID_ENTITY);
|
|
// Unit 1 should start at position 4 ==> Math.round((1 + 0.5) * 2.5) = Math.round(3.75) = 4
|
|
TS_ASSERT_EQUALS(orderAdded2.data.currentTargetIndex, 4);
|
|
})();
|
|
|
|
// More units than targets
|
|
(function()
|
|
{
|
|
let orderAdded1 = null;
|
|
let orderAdded2 = null;
|
|
let orderAdded3 = null;
|
|
|
|
const unitAI3 = ConstructComponent(102, "UnitAI", {
|
|
"FormationController": "false",
|
|
"DefaultStance": "aggressive"
|
|
});
|
|
AddMock(102, IID_Attack, { "GetBestAttackAgainst": () => "Melee" });
|
|
unitAI3.OnCreate();
|
|
|
|
unitAI1.AddOrder = (type, data, queued, pushFront) =>
|
|
{
|
|
orderAdded1 = { type, data, queued, pushFront };
|
|
};
|
|
unitAI2.AddOrder = (type, data, queued, pushFront) =>
|
|
{
|
|
orderAdded2 = { type, data, queued, pushFront };
|
|
};
|
|
unitAI3.AddOrder = (type, data, queued, pushFront) =>
|
|
{
|
|
orderAdded3 = { type, data, queued, pushFront };
|
|
};
|
|
|
|
const smallTargets = [300, 301];
|
|
|
|
unitAI1.AttackGroup(smallTargets, 0, 3, true, true, false);
|
|
unitAI2.AttackGroup(smallTargets, 1, 3, true, true, false);
|
|
unitAI3.AttackGroup(smallTargets, 2, 3, true, true, false);
|
|
|
|
// Verify distribution when more units than targets
|
|
TS_ASSERT(orderAdded1);
|
|
TS_ASSERT_EQUALS(orderAdded1.data.currentTargetIndex, 0); // 0 * 2 / 3 = 0
|
|
TS_ASSERT_EQUALS(orderAdded1.data.allowCapture, true);
|
|
TS_ASSERT_EQUALS(orderAdded1.queued, true);
|
|
|
|
TS_ASSERT(orderAdded2);
|
|
TS_ASSERT_EQUALS(orderAdded2.data.currentTargetIndex, 1); // 1 * 2 / 3 = 0.66, round = 1
|
|
TS_ASSERT(orderAdded2.data.currentTargetIndex < smallTargets.length);
|
|
|
|
TS_ASSERT(orderAdded3);
|
|
TS_ASSERT_EQUALS(orderAdded3.data.currentTargetIndex, 1); // 2 * 2 / 3 = 1.33, round = 1
|
|
TS_ASSERT(orderAdded3.data.currentTargetIndex < smallTargets.length);
|
|
})();
|
|
|
|
// Edge case - single target
|
|
(function()
|
|
{
|
|
let orderAdded = null;
|
|
unitAI1.AddOrder = (type, data, queued, pushFront) =>
|
|
{
|
|
orderAdded = { type, data, queued, pushFront };
|
|
};
|
|
|
|
const singleTarget = [400];
|
|
|
|
unitAI1.AttackGroup(singleTarget, 0, 1, false, false, true);
|
|
|
|
TS_ASSERT(orderAdded);
|
|
TS_ASSERT_EQUALS(orderAdded.data.targetArray.length, 1);
|
|
TS_ASSERT_EQUALS(orderAdded.data.currentTargetIndex, 0);
|
|
TS_ASSERT_EQUALS(orderAdded.pushFront, true);
|
|
})();
|
|
|
|
// Empty targets array should do nothing
|
|
(function()
|
|
{
|
|
let callCount = 0;
|
|
unitAI1.AddOrder = () => { callCount++; };
|
|
|
|
unitAI1.AttackGroup([], 0, 1);
|
|
TS_ASSERT_EQUALS(callCount, 0);
|
|
|
|
unitAI1.AttackGroup(null, 0, 1);
|
|
TS_ASSERT_EQUALS(callCount, 0);
|
|
|
|
unitAI1.AttackGroup(undefined, 0, 1);
|
|
TS_ASSERT_EQUALS(callCount, 0);
|
|
})();
|
|
|
|
// Verify RememberTargetPosition is called
|
|
(function()
|
|
{
|
|
let rememberedOrder = null;
|
|
unitAI1.RememberTargetPosition = (order) =>
|
|
{
|
|
rememberedOrder = order;
|
|
};
|
|
|
|
unitAI1.AddOrder = () => {};
|
|
|
|
const testTargets = [500, 501];
|
|
unitAI1.AttackGroup(testTargets, 0, 1);
|
|
|
|
TS_ASSERT(rememberedOrder);
|
|
TS_ASSERT_EQUALS(rememberedOrder.targetArray.length, 2);
|
|
})();
|
|
}
|
|
|
|
TestAttackGroupBehavior();
|