Add some tests for UnitAI.
Fix said tests for UnitAI. Hopefully fix #647 too. Document HFSM interface a bit. Add Engine.DumpSimState() console command for debugging. This was SVN commit r8681.
This commit is contained in:
parent
78ae72d87a
commit
f378a63d94
@ -235,9 +235,6 @@ var UnitFsmSpec = {
|
||||
},
|
||||
|
||||
"IDLE": {
|
||||
"enter": function() {
|
||||
this.SelectAnimation("idle");
|
||||
},
|
||||
},
|
||||
|
||||
"WALKING": {
|
||||
@ -261,6 +258,11 @@ var UnitFsmSpec = {
|
||||
"FORMATIONMEMBER": {
|
||||
|
||||
"FormationLeave": function(msg) {
|
||||
// We're leaving the formation, so stop our FormationWalk order
|
||||
if (this.FinishOrder())
|
||||
return;
|
||||
|
||||
// No orders left, we're an individual now
|
||||
this.SetNextState("INDIVIDUAL.IDLE");
|
||||
},
|
||||
|
||||
@ -294,6 +296,10 @@ var UnitFsmSpec = {
|
||||
|
||||
"IDLE": {
|
||||
"enter": function() {
|
||||
// Switch back to idle animation to guarantee we won't
|
||||
// get stuck with an incorrect animation
|
||||
this.SelectAnimation("idle");
|
||||
|
||||
// If we entered the idle state we must have nothing better to do,
|
||||
// so immediately check whether there's anybody nearby to attack.
|
||||
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
|
||||
@ -302,11 +308,10 @@ var UnitFsmSpec = {
|
||||
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
var ents = rangeMan.ResetActiveQuery(this.losRangeQuery);
|
||||
if (this.GetStance().attackOnSight && this.AttackVisibleEntity(ents))
|
||||
return true;
|
||||
return true; // (abort the transition since we may have already switched state)
|
||||
}
|
||||
|
||||
// Nobody to attack - switch to idle
|
||||
this.SelectAnimation("idle");
|
||||
// Nobody to attack - stay in idle
|
||||
return false;
|
||||
},
|
||||
|
||||
@ -728,8 +733,9 @@ UnitAI.prototype.SetupRangeQuery = function(owner)
|
||||
|
||||
var players = [];
|
||||
|
||||
if(owner != -1)
|
||||
{ // If unit not just killed, get enemy players via diplomacy
|
||||
if (owner != -1)
|
||||
{
|
||||
// If unit not just killed, get enemy players via diplomacy
|
||||
var player = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
|
||||
|
||||
// Get our diplomacy array
|
||||
@ -737,7 +743,8 @@ UnitAI.prototype.SetupRangeQuery = function(owner)
|
||||
var numPlayers = playerMan.GetNumPlayers();
|
||||
|
||||
for (var i = 1; i < numPlayers; ++i)
|
||||
{ // Exclude gaia, allies, and self
|
||||
{
|
||||
// Exclude gaia, allies, and self
|
||||
// TODO: How to handle neutral players - Special query to attack military only?
|
||||
if (i != owner && diplomacy[i - 1] < 0)
|
||||
players.push(i);
|
||||
|
@ -28,11 +28,34 @@ Engine.QueryInterface = function(ent, iid)
|
||||
return null;
|
||||
};
|
||||
|
||||
Engine.RegisterGlobal = function(name, value)
|
||||
{
|
||||
global[name] = value;
|
||||
};
|
||||
|
||||
Engine.DestroyEntity = function(ent)
|
||||
{
|
||||
for (var cid in g_Components[ent])
|
||||
{
|
||||
var cmp = g_Components[ent][cid];
|
||||
if (cmp.Deinit)
|
||||
cmp.Deinit();
|
||||
}
|
||||
|
||||
delete g_Components[ent];
|
||||
|
||||
// TODO: should send Destroy message
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// Engine.RegisterGlobal
|
||||
// Engine.PostMessage
|
||||
// Engine.BroadcastMessage
|
||||
|
||||
global.ResetState = function()
|
||||
{
|
||||
g_Components = {};
|
||||
};
|
||||
|
||||
global.AddMock = function(ent, iid, mock)
|
||||
{
|
||||
if (!g_Components[ent])
|
||||
@ -46,5 +69,10 @@ global.ConstructComponent = function(ent, name, template)
|
||||
cmp.entity = ent;
|
||||
cmp.template = template;
|
||||
cmp.Init();
|
||||
|
||||
if (!g_Components[ent])
|
||||
g_Components[ent] = {};
|
||||
g_Components[ent][g_ComponentTypes[name].iid] = cmp;
|
||||
|
||||
return cmp;
|
||||
};
|
||||
|
@ -0,0 +1,130 @@
|
||||
Engine.LoadHelperScript("FSM.js");
|
||||
Engine.LoadComponentScript("interfaces/Attack.js");
|
||||
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
|
||||
Engine.LoadComponentScript("interfaces/Formation.js");
|
||||
Engine.LoadComponentScript("interfaces/Health.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, {
|
||||
SetTimeout: function() { },
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
CreateActiveQuery: function(ent, minRange, maxRange, players, iid) {
|
||||
return 1;
|
||||
},
|
||||
EnableActiveQuery: function(id) { },
|
||||
ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; },
|
||||
DisableActiveQuery: function(id) { },
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
||||
GetPlayerByID: function(id) { return playerEntity; },
|
||||
GetNumPlayers: function() { return 2; },
|
||||
});
|
||||
|
||||
AddMock(playerEntity, IID_Player, {
|
||||
GetDiplomacy: function() { return []; },
|
||||
});
|
||||
|
||||
|
||||
var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false" });
|
||||
|
||||
AddMock(unit, IID_Position, {
|
||||
GetPosition: function() { return { "x": 0, "z": 0 }; },
|
||||
IsInWorld: function() { return true; },
|
||||
});
|
||||
|
||||
AddMock(unit, IID_UnitMotion, {
|
||||
GetWalkSpeed: function() { return 1; },
|
||||
MoveToFormationOffset: function(target, x, z) { },
|
||||
MoveToAttackRange: function(target, min, max) { },
|
||||
});
|
||||
|
||||
AddMock(unit, IID_Vision, {
|
||||
GetRange: function() { return 10; },
|
||||
});
|
||||
|
||||
AddMock(unit, IID_Attack, {
|
||||
GetRange: function() { return 10; },
|
||||
GetBestAttack: function() { return "melee"; },
|
||||
GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; },
|
||||
});
|
||||
|
||||
unitAI.OnCreate();
|
||||
|
||||
unitAI.SetupRangeQuery(1);
|
||||
|
||||
|
||||
if (mode == 1)
|
||||
AddMock(enemy, IID_Health, {
|
||||
GetHitpoints: function() { return 10; },
|
||||
});
|
||||
else if (mode == 2)
|
||||
AddMock(enemy, IID_Health, {
|
||||
GetHitpoints: function() { return 0; },
|
||||
});
|
||||
|
||||
var controllerFormation = ConstructComponent(controller, "Formation");
|
||||
var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true" });
|
||||
|
||||
AddMock(controller, IID_Position, {
|
||||
JumpTo: function(x, z) { this.x = x; this.z = z; },
|
||||
GetPosition: function() { return { "x": this.x, "z": this.z }; },
|
||||
IsInWorld: function() { return true; },
|
||||
});
|
||||
|
||||
AddMock(controller, IID_UnitMotion, {
|
||||
SetUnitRadius: function(r) { },
|
||||
SetSpeed: function(speed) { },
|
||||
MoveToPoint: function(x, z) { },
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
TestFormationExiting(0);
|
||||
TestFormationExiting(1);
|
||||
TestFormationExiting(2);
|
@ -9,6 +9,73 @@
|
||||
// plain old JS objects, with no need to serialise the complex
|
||||
// FSM structure itself or to add custom serialisation code.)
|
||||
|
||||
/**
|
||||
|
||||
FSM API:
|
||||
|
||||
Users define the FSM behaviour like:
|
||||
|
||||
var FsmSpec = {
|
||||
|
||||
// Define some default message handlers:
|
||||
|
||||
"MessageName1": function(msg) {
|
||||
// This function will be called in response to calls to
|
||||
// Fsm.ProcessMessage(this, { "type": "MessageName1", "data": msg });
|
||||
//
|
||||
// In this function, 'this' is the component object passed into
|
||||
// ProcessMessage, so you can access 'this.propertyName'
|
||||
// and 'this.methodName()' etc.
|
||||
},
|
||||
|
||||
"MessageName2": function(msg) {
|
||||
// Another message handler.
|
||||
},
|
||||
|
||||
// Define the behaviour for the 'STATENAME' state:
|
||||
"STATENAME": {
|
||||
|
||||
"MessageName1": function(msg) {
|
||||
// This overrides the previous MessageName1 that was
|
||||
// defined earlier, and will be called instead of it
|
||||
// in response to ProcessMessage.
|
||||
},
|
||||
|
||||
// We don't override MessageName2, so the default one
|
||||
// will be called instead.
|
||||
|
||||
// Define the 'STATENAME.SUBSTATENAME' state:
|
||||
// (we support arbitrarily-nested hierarchies of states)
|
||||
"SUBSTATENAME": {
|
||||
|
||||
"MessageName2": function(msg) {
|
||||
// Override the default MessageName2.
|
||||
// But we don't override MessageName1, so the one from
|
||||
// STATENAME will be used instead.
|
||||
},
|
||||
|
||||
"enter": function() {
|
||||
// This is a special function called when transitioning
|
||||
// into this state, or into a substate of this state.
|
||||
//
|
||||
// If it returns true, the transition will be aborted:
|
||||
// do this if you've called SetNextState inside this enter
|
||||
// handler, because otherwise the new state transition
|
||||
// will get mixed up with the previous ongoing one.
|
||||
// In normal cases, you can return false or nothing.
|
||||
},
|
||||
|
||||
"leave": function() {
|
||||
// Called when transitioning out of this state.
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
function FSM(spec)
|
||||
{
|
||||
// The (relatively) human-readable FSM specification needs to get
|
||||
|
@ -379,6 +379,13 @@ void ForceGC(void* cbdata)
|
||||
g_Console->InsertMessage(L"Garbage collection completed in: %f", time);
|
||||
}
|
||||
|
||||
void DumpSimState(void* UNUSED(cbdata))
|
||||
{
|
||||
fs::wpath path (psLogDir()/L"sim_dump.txt");
|
||||
std::ofstream file (path.external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc);
|
||||
g_Game->GetSimulation2()->DumpDebugState(file);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void GuiScriptingInit(ScriptInterface& scriptInterface)
|
||||
@ -430,4 +437,5 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
|
||||
scriptInterface.RegisterFunction<int, &Crash>("Crash");
|
||||
scriptInterface.RegisterFunction<void, &DebugWarn>("DebugWarn");
|
||||
scriptInterface.RegisterFunction<void, &ForceGC>("ForceGC");
|
||||
scriptInterface.RegisterFunction<void, &DumpSimState>("DumpSimState");
|
||||
}
|
||||
|
@ -51,6 +51,12 @@ public:
|
||||
TS_ASSERT(componentManager->LoadScript(L"simulation/components/"+name));
|
||||
}
|
||||
|
||||
static void Script_LoadHelperScript(void* cbdata, std::wstring name)
|
||||
{
|
||||
CComponentManager* componentManager = static_cast<CComponentManager*> (cbdata);
|
||||
TS_ASSERT(componentManager->LoadScript(L"simulation/helpers/"+name));
|
||||
}
|
||||
|
||||
void test_scripts()
|
||||
{
|
||||
if (!FileExists(L"simulation/components/tests/setup.js"))
|
||||
@ -69,6 +75,7 @@ public:
|
||||
ScriptTestSetup(componentManager.GetScriptInterface());
|
||||
|
||||
componentManager.GetScriptInterface().RegisterFunction<void, std::wstring, Script_LoadComponentScript> ("LoadComponentScript");
|
||||
componentManager.GetScriptInterface().RegisterFunction<void, std::wstring, Script_LoadHelperScript> ("LoadHelperScript");
|
||||
|
||||
componentManager.LoadComponentTypes();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user