mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-07-10 08:55:48 -07:00
"MoveStarted" messages were sent by UnitMotion when the unit started moving (sort of) or failed to do so. This was used by formations and guarding but was not really necessary as this can be done in "enter" or in a timer. Differential Revision: https://code.wildfiregames.com/D1885 This was SVN commit r22350.
327 lines
10 KiB
JavaScript
327 lines
10 KiB
JavaScript
Engine.LoadHelperScript("FSM.js");
|
|
Engine.LoadHelperScript("Entity.js");
|
|
Engine.LoadHelperScript("Player.js");
|
|
Engine.LoadComponentScript("interfaces/Attack.js");
|
|
Engine.LoadComponentScript("interfaces/Auras.js");
|
|
Engine.LoadComponentScript("interfaces/BuildingAI.js");
|
|
Engine.LoadComponentScript("interfaces/Capturable.js");
|
|
Engine.LoadComponentScript("interfaces/DamageReceiver.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/UnitAI.js");
|
|
Engine.LoadComponentScript("Formation.js");
|
|
Engine.LoadComponentScript("UnitAI.js");
|
|
|
|
/* 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) {
|
|
return 1;
|
|
},
|
|
EnableActiveQuery: function(id) { },
|
|
ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; },
|
|
DisableActiveQuery: function(id) { },
|
|
GetEntityFlagMask: function(identifier) { },
|
|
});
|
|
|
|
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_Player, {
|
|
IsAlly: function() { return false; },
|
|
IsEnemy: function() { return true; },
|
|
GetEnemies: function() { return []; },
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
|
|
"IsInTargetRange": (ent, target, min, max, opposite) => true
|
|
});
|
|
|
|
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,
|
|
"MoveToFormationOffset": (target, x, z) => {},
|
|
"MoveToTargetRange": (target, min, max) => true,
|
|
"StopMoving": () => {},
|
|
"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; },
|
|
});
|
|
|
|
unitAI.OnCreate();
|
|
|
|
unitAI.SetupRangeQuery(1);
|
|
|
|
|
|
if (mode == 1)
|
|
{
|
|
AddMock(enemy, IID_Health, {
|
|
GetHitpoints: function() { return 10; },
|
|
});
|
|
AddMock(enemy, IID_UnitAI, {
|
|
IsAnimal: function() { return false; }
|
|
});
|
|
}
|
|
else if (mode == 2)
|
|
AddMock(enemy, IID_Health, {
|
|
GetHitpoints: function() { return 0; },
|
|
});
|
|
|
|
var controllerFormation = ConstructComponent(controller, "Formation", {"FormationName": "Line Closed", "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppyness": 0});
|
|
var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" });
|
|
|
|
AddMock(controller, IID_Position, {
|
|
JumpTo: function(x, z) { this.x = x; this.z = z; },
|
|
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; },
|
|
});
|
|
|
|
AddMock(controller, IID_UnitMotion, {
|
|
"GetWalkSpeed": () => 1,
|
|
"StopMoving": () => {},
|
|
"SetSpeedMultiplier": () => {},
|
|
"MoveToPointRange": () => true,
|
|
"GetPassabilityClassName": () => "default"
|
|
});
|
|
|
|
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();
|
|
|
|
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) {
|
|
return 1;
|
|
},
|
|
EnableActiveQuery: function(id) { },
|
|
ResetActiveQuery: function(id) { return [enemy]; },
|
|
DisableActiveQuery: function(id) { },
|
|
GetEntityFlagMask: function(identifier) { },
|
|
});
|
|
|
|
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_Player, {
|
|
IsAlly: function() { return false; },
|
|
IsEnemy: function() { return true; },
|
|
GetEnemies: function() { return []; },
|
|
});
|
|
|
|
// 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,
|
|
"MoveToFormationOffset": (target, x, z) => {},
|
|
"MoveToTargetRange": (target, min, max) => true,
|
|
"StopMoving": () => {},
|
|
"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; },
|
|
});
|
|
|
|
unitAI.OnCreate();
|
|
|
|
unitAI.SetupRangeQuery(1);
|
|
|
|
unitAIs.push(unitAI);
|
|
}
|
|
|
|
// create enemy
|
|
AddMock(enemy, IID_Health, {
|
|
GetHitpoints: function() { return 40; },
|
|
});
|
|
|
|
var controllerFormation = ConstructComponent(controller, "Formation", {"FormationName": "Line Closed", "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppyness": 0});
|
|
var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" });
|
|
|
|
AddMock(controller, IID_Position, {
|
|
GetTurretParent: function() { return INVALID_ENTITY; },
|
|
JumpTo: function(x, z) { this.x = x; this.z = z; },
|
|
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; },
|
|
});
|
|
|
|
AddMock(controller, IID_UnitMotion, {
|
|
"GetWalkSpeed": () => 1,
|
|
"SetSpeedMultiplier": (speed) => {},
|
|
"MoveToPointRange": (x, z, minRange, maxRange) => {},
|
|
"StopMoving": () => {},
|
|
"GetPassabilityClassName": () => "default"
|
|
});
|
|
|
|
AddMock(controller, IID_Attack, {
|
|
GetRange: function() { return {"max":10, "min": 0}; },
|
|
CanAttackAsFormation: function() { return false; },
|
|
});
|
|
|
|
controllerAI.OnCreate();
|
|
|
|
controllerFormation.SetMembers(units);
|
|
|
|
controllerAI.Attack(enemy, []);
|
|
|
|
for (var ent of unitAIs)
|
|
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
|
|
|
|
controllerAI.MoveIntoFormation({"name": "Circle"});
|
|
|
|
// let all units be in position
|
|
for (var ent of unitAIs)
|
|
controllerFormation.SetInPosition(ent);
|
|
|
|
for (var ent of unitAIs)
|
|
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
|
|
|
|
controllerFormation.Disband();
|
|
}
|
|
|
|
TestFormationExiting(0);
|
|
TestFormationExiting(1);
|
|
TestFormationExiting(2);
|
|
|
|
TestMoveIntoFormationWhileAttacking();
|