Changeset View
Changeset View
Standalone View
Standalone View
binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
Engine.LoadHelperScript("FSM.js"); | 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/Timer.js"); | |||||
Engine.LoadComponentScript("interfaces/UnitAI.js"); | Engine.LoadComponentScript("interfaces/UnitAI.js"); | ||||
Engine.LoadComponentScript("Formation.js"); | |||||
Engine.LoadComponentScript("UnitAI.js"); | Engine.LoadComponentScript("UnitAI.js"); | ||||
/* Regression test. | Engine.LoadComponentScript("interfaces/Timer.js"); | ||||
* Tests the FSM behaviour of a unit when walking as part of a formation, | Engine.LoadComponentScript("interfaces/Heal.js"); | ||||
* then exiting the formation. | Engine.LoadComponentScript("interfaces/Sound.js"); | ||||
* mode == 0: There is no enemy unit nearby. | Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); | ||||
* mode == 1: There is a live enemy unit nearby. | Engine.LoadComponentScript("interfaces/DamageReceiver.js"); | ||||
* mode == 2: There is a dead enemy unit nearby. | Engine.LoadComponentScript("interfaces/Pack.js"); | ||||
*/ | |||||
function TestFormationExiting(mode) | |||||
{ | |||||
ResetState(); | |||||
var playerEntity = 5; | Engine.LoadHelperScript("Sound.js"); | ||||
var unit = 10; | |||||
var enemy = 20; | |||||
var controller = 30; | |||||
const PLAYER_ENTITY = 2; | |||||
const UNIT_ID = 3; | |||||
const TARGET_ENTITY = 4; | |||||
var lastAnimationSet = ""; | |||||
function SetupMocks() | |||||
{ | |||||
AddMock(SYSTEM_ENTITY, IID_Timer, { | AddMock(SYSTEM_ENTITY, IID_Timer, { | ||||
SetInterval: function() { }, | SetInterval: function() { }, | ||||
SetTimeout: 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, { | AddMock(SYSTEM_ENTITY, IID_TemplateManager, { | ||||
GetCurrentTemplateName: function(ent) { return "formations/line_closed"; }, | GetCurrentTemplateName: function(ent) { return "units/gaul_infantry_spearman_b"; }, | ||||
}); | |||||
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 []; }, | |||||
}); | |||||
var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); | |||||
AddMock(unit, IID_Identity, { | |||||
GetClassesList: function() { return []; }, | |||||
}); | }); | ||||
AddMock(unit, IID_Ownership, { | AddMock(UNIT_ID, IID_Sound, { | ||||
GetOwner: function() { return 1; }, | PlaySoundGroup: function() {}, | ||||
}); | }); | ||||
AddMock(unit, IID_Position, { | AddMock(UNIT_ID, IID_Position, { | ||||
GetTurretParent: function() { return INVALID_ENTITY; }, | "IsInWorld" : function() { return true; }, | ||||
GetPosition: function() { return new Vector3D(); }, | "GetPosition" : function() { return new Vector2D(0,0); } | ||||
GetPosition2D: function() { return new Vector2D(); }, | |||||
GetRotation: function() { return { "y": 0 }; }, | |||||
IsInWorld: function() { return true; }, | |||||
}); | }); | ||||
AddMock(unit, IID_UnitMotion, { | AddMock(UNIT_ID, IID_UnitMotion, { | ||||
GetWalkSpeed: function() { return 1; }, | GetTopSpeedRatio : function() { return 0; }, | ||||
MoveToFormationOffset: function(target, x, z) { }, | SetSpeed: function() {}, | ||||
IsInTargetRange: function(target, min, max) { return true; }, | |||||
MoveToTargetRange: function(target, min, max) { }, | |||||
StopMoving: function() { }, | |||||
GetPassabilityClassName: function() { return "default"; }, | |||||
}); | }); | ||||
AddMock(unit, IID_Vision, { | AddMock(UNIT_ID, IID_DamageReceiver, { | ||||
GetRange: function() { return 10; }, | SetInvulnerability : function() {}, | ||||
}); | }); | ||||
AddMock(unit, IID_Attack, { | AddMock(UNIT_ID, IID_Pack, { | ||||
GetRange: function() { return { "max": 10, "min": 0}; }, | Pack : function() {}, | ||||
GetFullAttackRange: function() { return { "max": 40, "min": 0}; }, | Unpack : function() { }, | ||||
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(); | AddMock(UNIT_ID, IID_Visual, { | ||||
SelectAnimation : function(name) { lastAnimationSet = name; }, | |||||
unitAI.SetupRangeQuery(1); | SetVariant : function(key, name) { }, | ||||
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, { | |||||
SetSpeed: function(speed) { }, | |||||
MoveToPointRange: function(x, z, minRange, maxRange) { }, | |||||
GetPassabilityClassName: function() { return "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); | |||||
controllerAI.OnMotionChanged({ "starting": true }); | |||||
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, { | AddMock(SYSTEM_ENTITY, IID_RangeManager, { | ||||
CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) { | CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) { | ||||
return 1; | return 1; | ||||
}, | }, | ||||
EnableActiveQuery: function(id) { }, | EnableActiveQuery: function(id) { }, | ||||
ResetActiveQuery: function(id) { return [enemy]; }, | ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; }, | ||||
DisableActiveQuery: function(id) { }, | DisableActiveQuery: function(id) { }, | ||||
GetEntityFlagMask: function(identifier) { }, | GetEntityFlagMask: function(identifier) { }, | ||||
}); | }); | ||||
AddMock(SYSTEM_ENTITY, IID_TemplateManager, { | AddMock(SYSTEM_ENTITY, IID_TemplateManager, { | ||||
GetCurrentTemplateName: function(ent) { return "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 []; }, | |||||
}); | |||||
// 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; }, | GetCurrentTemplateName: function(ent) { return "units/gaul_infantry_spearman_b"; }, | ||||
}); | }); | ||||
AddMock(unit + i, IID_Position, { | AddMock(SYSTEM_ENTITY, IID_PlayerManager, { | ||||
GetTurretParent: function() { return INVALID_ENTITY; }, | GetPlayerByID: function(id) { return PLAYER_ENTITY; }, | ||||
GetPosition: function() { return new Vector3D(); }, | GetNumPlayers: function() { return 1; }, | ||||
GetPosition2D: function() { return new Vector2D(); }, | });*/ | ||||
GetRotation: function() { return { "y": 0 }; }, | |||||
IsInWorld: function() { return true; }, | |||||
}); | |||||
AddMock(unit + i, IID_UnitMotion, { | |||||
GetWalkSpeed: function() { return 1; }, | |||||
MoveToFormationOffset: function(target, x, z) { }, | |||||
IsInTargetRange: function(target, min, max) { return true; }, | |||||
MoveToTargetRange: function(target, min, max) { }, | |||||
StopMoving: function() { }, | |||||
GetPassabilityClassName: function() { return "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 | // The intention of this test is to validate that unitAI states that select an animation correctly reset it when leaving | ||||
AddMock(enemy, IID_Health, { | // This tests on "unevaled" FSM state instead of trying to get every state because it's basically a nightmare to get 100% coverage in UnitAI | ||||
GetHitpoints: function() { return 40; }, | // And this seems to be good enough to actually detect the bugs. | ||||
}); | function testAnimationsAreReset() | ||||
{ | |||||
var controllerFormation = ConstructComponent(controller, "Formation", {"FormationName": "Line Closed", "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppyness": 0}); | ResetState(); | ||||
var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" }); | SetupMocks(); | ||||
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, { | |||||
SetSpeed: function(speed) { }, | |||||
MoveToPointRange: function(x, z, minRange, maxRange) { }, | |||||
IsInTargetRange: function(target, min, max) { return true; }, | |||||
GetPassabilityClassName: function() { return "default"; }, | |||||
}); | |||||
AddMock(controller, IID_Attack, { | |||||
GetRange: function() { return {"max":10, "min": 0}; }, | |||||
CanAttackAsFormation: function() { return false; }, | |||||
}); | |||||
controllerAI.OnCreate(); | |||||
controllerFormation.SetMembers(units); | let cmpUnitAI = ConstructComponent(UNIT_ID, "UnitAI", { "DefaultStance": "aggressive" }); | ||||
controllerAI.Attack(enemy, []); | cmpUnitAI.OnCreate(); | ||||
TS_ASSERT_EQUALS(cmpUnitAI.UnitFsm.GetCurrentState(cmpUnitAI), "INDIVIDUAL.IDLE"); | |||||
for (var ent of unitAIs) | cmpUnitAI.order = {"data" : { "targetClasses" : [], "target" : TARGET_ENTITY }}; | ||||
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); | |||||
controllerAI.MoveIntoFormation({"name": "Circle"}); | let TestForReset = function(cmpUnitAI, totest) | ||||
{ | |||||
let shouldReset = false; | |||||
for (let fc in totest) | |||||
{ | |||||
if (fc === "leave") | |||||
continue; | |||||
// let all units be in position | let stringified = uneval(totest[fc]); | ||||
for (var ent of unitAIs) | let pos = stringified.search("SelectAnimation"); | ||||
controllerFormation.SetInPosition(ent); | if (pos !== -1) | ||||
{ | |||||
let animation = stringified.substr(pos, stringified.indexOf(")", pos) - pos) + ")"; | |||||
if (animation.search("idle") === -1 && animation.search(", true") === -1) | |||||
shouldReset = true; | |||||
} | |||||
} | |||||
if (shouldReset) | |||||
{ | |||||
if (!totest.leave) | |||||
{ | |||||
TS_FAIL("No leave"); | |||||
return false; | |||||
} | |||||
for (var ent of unitAIs) | let doesReset = false; | ||||
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); | let stringified = uneval(totest.leave); | ||||
let pos = stringified.search("SelectAnimation"); | |||||
if (pos !== -1) | |||||
{ | |||||
let animation = stringified.substr(pos, stringified.indexOf(")", pos) - pos) + ")"; | |||||
if (animation.search("idle") !== -1) | |||||
doesReset = true; | |||||
} | |||||
if (!doesReset) | |||||
{ | |||||
TS_FAIL("No reset in the leave"); | |||||
return false; | |||||
} | |||||
} | |||||
return true; | |||||
} | |||||
controllerFormation.Disband(); | for (let i in cmpUnitAI.UnitFsmSpec.INDIVIDUAL) | ||||
{ | |||||
// skip the default "Enter" states and such. | |||||
if (typeof cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i] === "function") | |||||
continue; | |||||
// skip IDLE because the following dumb test doesn't detect it properly. | |||||
if (i === "IDLE") | |||||
continue; | |||||
// check if this state has 2 levels or 3 levels | |||||
// eg INDIVIDUAL.FLEEING or INDIVIDUAL.COMBAT.SOMETHING | |||||
let hasChildren = false; | |||||
for (let child in cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i]) | |||||
if (typeof cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i][child] !== "function") | |||||
{ | |||||
hasChildren = true; | |||||
break; | |||||
} | |||||
if (hasChildren) | |||||
{ | |||||
for (let child in cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i]) | |||||
{ | |||||
if (!TestForReset(cmpUnitAI, cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i][child])) | |||||
warn("Failed in " + i + " substate " + child); | |||||
} | |||||
} | |||||
else | |||||
if (!TestForReset(cmpUnitAI, cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i])) | |||||
warn("Failed in " + i); | |||||
} | } | ||||
TestFormationExiting(0); | // TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); | ||||
TestFormationExiting(1); | } | ||||
TestFormationExiting(2); | |||||
TestMoveIntoFormationWhileAttacking(); | testAnimationsAreReset(); |
Wildfire Games · Phabricator