source: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js@ 25208

Last change on this file since 25208 was 25208, checked in by Freagarach, 3 years ago

Build using Builder instead of UnitAI.

Moves the building logic from UnitAI to Builder.
Makes it easier for modders to change building behaviour, e.g. letting structures build.

Differential revision: D3812
Comment by: @Angen

  • Property svn:eol-style set to native
File size: 14.8 KB
Line 
1Engine.LoadHelperScript("FSM.js");
2Engine.LoadHelperScript("Player.js");
3Engine.LoadHelperScript("Position.js");
4Engine.LoadHelperScript("Sound.js");
5Engine.LoadComponentScript("interfaces/Auras.js");
6Engine.LoadComponentScript("interfaces/Builder.js");
7Engine.LoadComponentScript("interfaces/BuildingAI.js");
8Engine.LoadComponentScript("interfaces/Capturable.js");
9Engine.LoadComponentScript("interfaces/Garrisonable.js");
10Engine.LoadComponentScript("interfaces/Resistance.js");
11Engine.LoadComponentScript("interfaces/Formation.js");
12Engine.LoadComponentScript("interfaces/Heal.js");
13Engine.LoadComponentScript("interfaces/Health.js");
14Engine.LoadComponentScript("interfaces/Pack.js");
15Engine.LoadComponentScript("interfaces/ResourceSupply.js");
16Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
17Engine.LoadComponentScript("interfaces/Timer.js");
18Engine.LoadComponentScript("interfaces/UnitAI.js");
19Engine.LoadComponentScript("Formation.js");
20Engine.LoadComponentScript("UnitAI.js");
21
22/**
23 * Fairly straightforward test that entity renaming is handled
24 * by unitAI states. These ought to be augmented with integration tests, ideally.
25 */
26function TestTargetEntityRenaming(init_state, post_state, setup)
27{
28 ResetState();
29 const player_ent = 5;
30 const target_ent = 6;
31
32 AddMock(SYSTEM_ENTITY, IID_Timer, {
33 "SetInterval": () => {},
34 "SetTimeout": () => {}
35 });
36 AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
37 "IsInTargetRange": () => false
38 });
39
40 let unitAI = ConstructComponent(player_ent, "UnitAI", {
41 "FormationController": "false",
42 "DefaultStance": "aggressive",
43 "FleeDistance": 10
44 });
45 unitAI.OnCreate();
46
47 setup(unitAI, player_ent, target_ent);
48
49 TS_ASSERT_EQUALS(unitAI.GetCurrentState(), init_state);
50
51 unitAI.OnGlobalEntityRenamed({
52 "entity": target_ent,
53 "newentity": target_ent + 1
54 });
55
56 TS_ASSERT_EQUALS(unitAI.GetCurrentState(), post_state);
57}
58
59TestTargetEntityRenaming(
60 "INDIVIDUAL.GARRISON.APPROACHING", "INDIVIDUAL.IDLE",
61 (unitAI, player_ent, target_ent) => {
62 unitAI.CanGarrison = (target) => target == target_ent;
63 unitAI.MoveToTargetRange = (target) => target == target_ent;
64 unitAI.AbleToMove = () => true;
65
66 unitAI.Garrison(target_ent, false);
67 }
68);
69
70TestTargetEntityRenaming(
71 "INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.REPAIR.REPAIRING",
72 (unitAI, player_ent, target_ent) => {
73
74 AddMock(player_ent, IID_Builder, {
75 "StartRepairing": () => true,
76 "StopRepairing": () => {}
77 });
78
79 QueryBuilderListInterface = () => {};
80 unitAI.CheckTargetRange = () => true;
81 unitAI.CanRepair = (target) => target == target_ent;
82
83 unitAI.Repair(target_ent, false, false);
84 }
85);
86
87
88TestTargetEntityRenaming(
89 "INDIVIDUAL.FLEEING", "INDIVIDUAL.FLEEING",
90 (unitAI, player_ent, target_ent) => {
91 PositionHelper.DistanceBetweenEntities = () => 10;
92 unitAI.CheckTargetRangeExplicit = () => false;
93
94 AddMock(player_ent, IID_UnitMotion, {
95 "MoveToTargetRange": () => true,
96 "GetRunMultiplier": () => 1,
97 "SetSpeedMultiplier": () => {},
98 "StopMoving": () => {}
99 });
100
101 unitAI.Flee(target_ent, false);
102 }
103);
104
105/* Regression test.
106 * Tests the FSM behaviour of a unit when walking as part of a formation,
107 * then exiting the formation.
108 * mode == 0: There is no enemy unit nearby.
109 * mode == 1: There is a live enemy unit nearby.
110 * mode == 2: There is a dead enemy unit nearby.
111 */
112function TestFormationExiting(mode)
113{
114 ResetState();
115
116 var playerEntity = 5;
117 var unit = 10;
118 var enemy = 20;
119 var controller = 30;
120
121
122 AddMock(SYSTEM_ENTITY, IID_Timer, {
123 "SetInterval": function() { },
124 "SetTimeout": function() { },
125 });
126
127 AddMock(SYSTEM_ENTITY, IID_RangeManager, {
128 "CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize) {
129 return 1;
130 },
131 "EnableActiveQuery": function(id) { },
132 "ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; },
133 "DisableActiveQuery": function(id) { },
134 "GetEntityFlagMask": function(identifier) { },
135 });
136
137 AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
138 "GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
139 });
140
141 AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
142 "GetPlayerByID": function(id) { return playerEntity; },
143 "GetNumPlayers": function() { return 2; },
144 });
145
146 AddMock(playerEntity, IID_Player, {
147 "IsAlly": function() { return false; },
148 "IsEnemy": function() { return true; },
149 "GetEnemies": function() { return [2]; },
150 });
151
152 AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
153 "IsInTargetRange": () => true,
154 "IsInPointRange": () => true
155 });
156
157 var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
158
159 AddMock(unit, IID_Identity, {
160 "GetClassesList": function() { return []; },
161 });
162
163 AddMock(unit, IID_Ownership, {
164 "GetOwner": function() { return 1; },
165 });
166
167 AddMock(unit, IID_Position, {
168 "GetTurretParent": function() { return INVALID_ENTITY; },
169 "GetPosition": function() { return new Vector3D(); },
170 "GetPosition2D": function() { return new Vector2D(); },
171 "GetRotation": function() { return { "y": 0 }; },
172 "IsInWorld": function() { return true; },
173 });
174
175 AddMock(unit, IID_UnitMotion, {
176 "GetWalkSpeed": () => 1,
177 "MoveToFormationOffset": (target, x, z) => {},
178 "MoveToTargetRange": (target, min, max) => true,
179 "StopMoving": () => {},
180 "SetFacePointAfterMove": () => {},
181 "GetFacePointAfterMove": () => true,
182 "GetPassabilityClassName": () => "default"
183 });
184
185 AddMock(unit, IID_Vision, {
186 "GetRange": function() { return 10; },
187 });
188
189 AddMock(unit, IID_Attack, {
190 "GetRange": function() { return { "max": 10, "min": 0 }; },
191 "GetFullAttackRange": function() { return { "max": 40, "min": 0 }; },
192 "GetBestAttackAgainst": function(t) { return "melee"; },
193 "GetPreference": function(t) { return 0; },
194 "GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; },
195 "CanAttack": function(v) { return true; },
196 "CompareEntitiesByPreference": function(a, b) { return 0; },
197 });
198
199 unitAI.OnCreate();
200
201 unitAI.SetupAttackRangeQuery(1);
202
203
204 if (mode == 1)
205 {
206 AddMock(enemy, IID_Health, {
207 "GetHitpoints": function() { return 10; },
208 });
209 AddMock(enemy, IID_UnitAI, {
210 "IsAnimal": () => "false",
211 "IsDangerousAnimal": () => "false"
212 });
213 }
214 else if (mode == 2)
215 AddMock(enemy, IID_Health, {
216 "GetHitpoints": function() { return 0; },
217 });
218
219 let controllerFormation = ConstructComponent(controller, "Formation", {
220 "FormationName": "Line Closed",
221 "FormationShape": "square",
222 "ShiftRows": "false",
223 "SortingClasses": "",
224 "WidthDepthRatio": 1,
225 "UnitSeparationWidthMultiplier": 1,
226 "UnitSeparationDepthMultiplier": 1,
227 "SpeedMultiplier": 1,
228 "Sloppiness": 0
229 });
230 let controllerAI = ConstructComponent(controller, "UnitAI", {
231 "FormationController": "true",
232 "DefaultStance": "aggressive"
233 });
234
235 AddMock(controller, IID_Position, {
236 "JumpTo": function(x, z) { this.x = x; this.z = z; },
237 "GetTurretParent": function() { return INVALID_ENTITY; },
238 "GetPosition": function() { return new Vector3D(this.x, 0, this.z); },
239 "GetPosition2D": function() { return new Vector2D(this.x, this.z); },
240 "GetRotation": function() { return { "y": 0 }; },
241 "IsInWorld": function() { return true; },
242 "MoveOutOfWorld": () => {}
243 });
244
245 AddMock(controller, IID_UnitMotion, {
246 "GetWalkSpeed": () => 1,
247 "StopMoving": () => {},
248 "SetSpeedMultiplier": () => {},
249 "MoveToPointRange": () => true,
250 "SetFacePointAfterMove": () => {},
251 "GetFacePointAfterMove": () => true,
252 "GetPassabilityClassName": () => "default"
253 });
254
255 controllerAI.OnCreate();
256
257
258 TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.IDLE");
259 TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
260
261 controllerFormation.SetMembers([unit]);
262 controllerAI.Walk(100, 100, false);
263
264 TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.WALKING");
265 TS_ASSERT_EQUALS(unitAI.fsmStateName, "FORMATIONMEMBER.WALKING");
266
267 controllerFormation.Disband();
268
269 unitAI.UnitFsm.ProcessMessage(unitAI, { "type": "Timer" });
270
271 if (mode == 0)
272 TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
273 else if (mode == 1)
274 TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
275 else if (mode == 2)
276 TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
277 else
278 TS_FAIL("invalid mode");
279}
280
281function TestMoveIntoFormationWhileAttacking()
282{
283 ResetState();
284
285 var playerEntity = 5;
286 var controller = 10;
287 var enemy = 20;
288 var unit = 30;
289 var units = [];
290 var unitCount = 8;
291 var unitAIs = [];
292
293 AddMock(SYSTEM_ENTITY, IID_Timer, {
294 "SetInterval": function() { },
295 "SetTimeout": function() { },
296 });
297
298
299 AddMock(SYSTEM_ENTITY, IID_RangeManager, {
300 "CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize) {
301 return 1;
302 },
303 "EnableActiveQuery": function(id) { },
304 "ResetActiveQuery": function(id) { return [enemy]; },
305 "DisableActiveQuery": function(id) { },
306 "GetEntityFlagMask": function(identifier) { },
307 });
308
309 AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
310 "GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
311 });
312
313 AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
314 "GetPlayerByID": function(id) { return playerEntity; },
315 "GetNumPlayers": function() { return 2; },
316 });
317
318 AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
319 "IsInTargetRange": (ent, target, min, max) => true
320 });
321
322 AddMock(playerEntity, IID_Player, {
323 "IsAlly": function() { return false; },
324 "IsEnemy": function() { return true; },
325 "GetEnemies": function() { return [2]; },
326 });
327
328 // create units
329 for (var i = 0; i < unitCount; i++)
330 {
331
332 units.push(unit + i);
333
334 var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
335
336 AddMock(unit + i, IID_Identity, {
337 "GetClassesList": function() { return []; },
338 });
339
340 AddMock(unit + i, IID_Ownership, {
341 "GetOwner": function() { return 1; },
342 });
343
344 AddMock(unit + i, IID_Position, {
345 "GetTurretParent": function() { return INVALID_ENTITY; },
346 "GetPosition": function() { return new Vector3D(); },
347 "GetPosition2D": function() { return new Vector2D(); },
348 "GetRotation": function() { return { "y": 0 }; },
349 "IsInWorld": function() { return true; },
350 });
351
352 AddMock(unit + i, IID_UnitMotion, {
353 "GetWalkSpeed": () => 1,
354 "MoveToFormationOffset": (target, x, z) => {},
355 "MoveToTargetRange": (target, min, max) => true,
356 "StopMoving": () => {},
357 "SetFacePointAfterMove": () => {},
358 "GetFacePointAfterMove": () => true,
359 "GetPassabilityClassName": () => "default"
360 });
361
362 AddMock(unit + i, IID_Vision, {
363 "GetRange": function() { return 10; },
364 });
365
366 AddMock(unit + i, IID_Attack, {
367 "GetRange": function() { return { "max": 10, "min": 0 }; },
368 "GetFullAttackRange": function() { return { "max": 40, "min": 0 }; },
369 "GetBestAttackAgainst": function(t) { return "melee"; },
370 "GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; },
371 "CanAttack": function(v) { return true; },
372 "CompareEntitiesByPreference": function(a, b) { return 0; },
373 });
374
375 unitAI.OnCreate();
376
377 unitAI.SetupAttackRangeQuery(1);
378
379 unitAIs.push(unitAI);
380 }
381
382 // create enemy
383 AddMock(enemy, IID_Health, {
384 "GetHitpoints": function() { return 40; },
385 });
386
387 let controllerFormation = ConstructComponent(controller, "Formation", {
388 "FormationName": "Line Closed",
389 "FormationShape": "square",
390 "ShiftRows": "false",
391 "SortingClasses": "",
392 "WidthDepthRatio": 1,
393 "UnitSeparationWidthMultiplier": 1,
394 "UnitSeparationDepthMultiplier": 1,
395 "SpeedMultiplier": 1,
396 "Sloppiness": 0
397 });
398 let controllerAI = ConstructComponent(controller, "UnitAI", {
399 "FormationController": "true",
400 "DefaultStance": "aggressive"
401 });
402
403 AddMock(controller, IID_Position, {
404 "GetTurretParent": () => INVALID_ENTITY,
405 "JumpTo": function(x, z) { this.x = x; this.z = z; },
406 "GetPosition": function(){ return new Vector3D(this.x, 0, this.z); },
407 "GetPosition2D": function(){ return new Vector2D(this.x, this.z); },
408 "GetRotation": () => ({ "y": 0 }),
409 "IsInWorld": () => true,
410 "MoveOutOfWorld": () => {},
411 });
412
413 AddMock(controller, IID_UnitMotion, {
414 "GetWalkSpeed": () => 1,
415 "SetSpeedMultiplier": (speed) => {},
416 "MoveToPointRange": (x, z, minRange, maxRange) => {},
417 "StopMoving": () => {},
418 "SetFacePointAfterMove": () => {},
419 "GetFacePointAfterMove": () => true,
420 "GetPassabilityClassName": () => "default"
421 });
422
423 AddMock(controller, IID_Attack, {
424 "GetRange": function() { return { "max": 10, "min": 0 }; },
425 "CanAttackAsFormation": function() { return false; },
426 });
427
428 controllerAI.OnCreate();
429
430 controllerFormation.SetMembers(units);
431
432 controllerAI.Attack(enemy, []);
433
434 for (let ent of unitAIs)
435 TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
436
437 controllerAI.MoveIntoFormation({ "name": "Circle" });
438
439 // let all units be in position
440 for (let ent of unitAIs)
441 controllerFormation.SetWaitingOnController(ent);
442
443 for (let ent of unitAIs)
444 TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
445
446 controllerFormation.Disband();
447}
448
449TestFormationExiting(0);
450TestFormationExiting(1);
451TestFormationExiting(2);
452
453TestMoveIntoFormationWhileAttacking();
454
455
456function TestWalkAndFightTargets()
457{
458 const ent = 10;
459 let unitAI = ConstructComponent(ent, "UnitAI", {
460 "FormationController": "false",
461 "DefaultStance": "aggressive",
462 "FleeDistance": 10
463 });
464 unitAI.OnCreate();
465 unitAI.losAttackRangeQuery = true;
466
467 // The result is stored here
468 let result;
469 unitAI.PushOrderFront = function(type, order)
470 {
471 if (type === "Attack" && order?.target)
472 result = order.target;
473 };
474
475 // Create some targets.
476 AddMock(ent+1, IID_UnitAI, { "IsAnimal": () => true, "IsDangerousAnimal": () => false });
477 AddMock(ent+2, IID_Ownership, { "GetOwner": () => 2 });
478 AddMock(ent+3, IID_Ownership, { "GetOwner": () => 2 });
479 AddMock(ent+4, IID_Ownership, { "GetOwner": () => 2 });
480 AddMock(ent+5, IID_Ownership, { "GetOwner": () => 2 });
481 AddMock(ent+6, IID_Ownership, { "GetOwner": () => 2 });
482 AddMock(ent+7, IID_Ownership, { "GetOwner": () => 2 });
483
484 unitAI.CanAttack = function(target)
485 {
486 return target !== ent+2 && target !== ent+7;
487 };
488
489 AddMock(ent, IID_Attack, {
490 "GetPreference": (target) => ({
491 [ent+4]: 0,
492 [ent+5]: 1,
493 [ent+6]: 2,
494 [ent+7]: 0
495 }?.[target])
496 });
497
498 let runTest = function(ents, res)
499 {
500 result = undefined;
501 AddMock(SYSTEM_ENTITY, IID_RangeManager, {
502 "ResetActiveQuery": () => ents
503 });
504 TS_ASSERT_EQUALS(unitAI.FindWalkAndFightTargets(), !!res);
505 TS_ASSERT_EQUALS(result, res);
506 };
507
508 // No entities.
509 runTest([]);
510
511 // Entities that cannot be attacked.
512 runTest([ent+1, ent+2, ent+7]);
513
514 // No preference, one attackable entity.
515 runTest([ent+1, ent+2, ent+3], ent+3);
516
517 // Check preferences.
518 runTest([ent+1, ent+2, ent+3, ent+4], ent+4);
519 runTest([ent+1, ent+2, ent+3, ent+4, ent+5], ent+4);
520 runTest([ent+1, ent+2, ent+6, ent+3, ent+4, ent+5], ent+4);
521 runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+4, ent+5], ent+4);
522 runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+5], ent+5);
523 runTest([ent+1, ent+2, ent+7, ent+6, ent+3], ent+6);
524 runTest([ent+1, ent+2, ent+7, ent+3], ent+3);
525}
526
527TestWalkAndFightTargets();
Note: See TracBrowser for help on using the repository browser.