@rpgjs/action-battle 5.0.0-beta.11 → 5.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/client/ai.server.d.ts +57 -8
  3. package/dist/client/attack-input.d.ts +3 -0
  4. package/dist/client/core/action-use.d.ts +18 -0
  5. package/dist/client/core/ai-behavior-tree.d.ts +99 -0
  6. package/dist/client/core/attack-runtime.d.ts +2 -0
  7. package/dist/client/core/defaults.d.ts +3 -2
  8. package/dist/client/core/equipment.d.ts +1 -0
  9. package/dist/client/core/targets.d.ts +15 -0
  10. package/dist/client/enemies/factory.d.ts +2 -0
  11. package/dist/client/index.d.ts +12 -7
  12. package/dist/client/index.js +16 -11
  13. package/dist/client/index10.js +32 -56
  14. package/dist/client/index11.js +99 -52
  15. package/dist/client/index12.js +76 -103
  16. package/dist/client/index13.js +72 -135
  17. package/dist/client/index14.js +67 -23
  18. package/dist/client/index15.js +197 -63
  19. package/dist/client/index16.js +112 -1337
  20. package/dist/client/index17.js +203 -7
  21. package/dist/client/index18.js +32 -58
  22. package/dist/client/index19.js +70 -8
  23. package/dist/client/index20.js +57 -501
  24. package/dist/client/index21.js +70 -0
  25. package/dist/client/index22.js +226 -0
  26. package/dist/client/index23.js +16 -0
  27. package/dist/client/index24.js +25 -0
  28. package/dist/client/index25.js +107 -0
  29. package/dist/client/index26.js +1949 -0
  30. package/dist/client/index27.js +12 -0
  31. package/dist/client/index28.js +589 -0
  32. package/dist/client/index4.js +79 -38
  33. package/dist/client/index6.js +65 -306
  34. package/dist/client/index7.js +33 -33
  35. package/dist/client/index8.js +24 -100
  36. package/dist/client/index9.js +293 -61
  37. package/dist/client/locomotion.d.ts +16 -0
  38. package/dist/client/movement.d.ts +14 -0
  39. package/dist/client/server.d.ts +7 -3
  40. package/dist/client/ui.d.ts +22 -0
  41. package/dist/client/visual.d.ts +15 -0
  42. package/dist/server/ai.server.d.ts +57 -8
  43. package/dist/server/attack-input.d.ts +3 -0
  44. package/dist/server/core/action-use.d.ts +18 -0
  45. package/dist/server/core/ai-behavior-tree.d.ts +99 -0
  46. package/dist/server/core/attack-runtime.d.ts +2 -0
  47. package/dist/server/core/defaults.d.ts +3 -2
  48. package/dist/server/core/equipment.d.ts +1 -0
  49. package/dist/server/core/targets.d.ts +15 -0
  50. package/dist/server/enemies/factory.d.ts +2 -0
  51. package/dist/server/index.d.ts +12 -7
  52. package/dist/server/index.js +14 -9
  53. package/dist/server/index10.js +64 -1336
  54. package/dist/server/index11.js +33 -33
  55. package/dist/server/index13.js +67 -11
  56. package/dist/server/index14.js +207 -484
  57. package/dist/server/index15.js +15 -9
  58. package/dist/server/index16.js +26 -0
  59. package/dist/server/index17.js +25 -0
  60. package/dist/server/index18.js +107 -0
  61. package/dist/server/index19.js +1949 -0
  62. package/dist/server/index2.js +10 -2
  63. package/dist/server/index20.js +37 -0
  64. package/dist/server/index21.js +588 -0
  65. package/dist/server/index22.js +78 -0
  66. package/dist/server/index23.js +12 -0
  67. package/dist/server/index5.js +79 -38
  68. package/dist/server/index6.js +192 -129
  69. package/dist/server/index7.js +208 -24
  70. package/dist/server/index8.js +28 -66
  71. package/dist/server/index9.js +68 -51
  72. package/dist/server/locomotion.d.ts +16 -0
  73. package/dist/server/movement.d.ts +14 -0
  74. package/dist/server/server.d.ts +7 -3
  75. package/dist/server/ui.d.ts +22 -0
  76. package/dist/server/visual.d.ts +15 -0
  77. package/package.json +5 -5
  78. package/src/ai.server.spec.ts +380 -1
  79. package/src/ai.server.ts +963 -137
  80. package/src/animations.spec.ts +40 -0
  81. package/src/animations.ts +31 -9
  82. package/src/attack-input.spec.ts +51 -0
  83. package/src/attack-input.ts +59 -0
  84. package/src/client.ts +75 -62
  85. package/src/config.ts +84 -37
  86. package/src/core/action-use.spec.ts +317 -0
  87. package/src/core/action-use.ts +387 -0
  88. package/src/core/ai-behavior-tree.spec.ts +116 -0
  89. package/src/core/ai-behavior-tree.ts +272 -0
  90. package/src/core/attack-profile.spec.ts +46 -0
  91. package/src/core/attack-runtime.spec.ts +35 -0
  92. package/src/core/attack-runtime.ts +32 -0
  93. package/src/core/context.ts +9 -0
  94. package/src/core/contracts.ts +146 -1
  95. package/src/core/defaults.ts +72 -1
  96. package/src/core/equipment.ts +9 -5
  97. package/src/core/hit.spec.ts +21 -0
  98. package/src/core/targets.spec.ts +124 -0
  99. package/src/core/targets.ts +150 -0
  100. package/src/enemies/factory.ts +8 -0
  101. package/src/index.ts +111 -2
  102. package/src/locomotion.spec.ts +51 -0
  103. package/src/locomotion.ts +48 -0
  104. package/src/movement.spec.ts +78 -0
  105. package/src/movement.ts +46 -0
  106. package/src/server.ts +242 -66
  107. package/src/types.ts +105 -35
  108. package/src/ui.ts +113 -0
  109. package/src/visual.spec.ts +166 -0
  110. package/src/visual.ts +285 -0
  111. package/README.md +0 -1242
@@ -1,6 +1,9 @@
1
1
  import { MAXHP } from "@rpgjs/server";
2
2
  import { afterEach, describe, expect, test, vi } from "vitest";
3
- import { BattleAi } from "./ai.server";
3
+ import { AttackPattern, BattleAi } from "./ai.server";
4
+ import { chase, idle, ifTargetVisible } from "./core/ai-behavior-tree";
5
+ import { setActionBattleSystems } from "./core/context";
6
+ import { ACTION_BATTLE_CLIENT_VISUAL_ID } from "./visual";
4
7
 
5
8
  const createEvent = () => ({
6
9
  id: "monster-1",
@@ -13,11 +16,13 @@ const createEvent = () => ({
13
16
  showHit: vi.fn(),
14
17
  setGraphicAnimation: vi.fn(),
15
18
  stopMoveTo: vi.fn(),
19
+ moveTo: vi.fn(),
16
20
  getCurrentMap: vi.fn(() => ({})),
17
21
  remove: vi.fn(),
18
22
  x: vi.fn(() => 0),
19
23
  y: vi.fn(() => 0),
20
24
  direction: vi.fn(() => "down"),
25
+ changeDirection: vi.fn(),
21
26
  });
22
27
 
23
28
  const createPlayer = () => ({
@@ -37,6 +42,7 @@ describe("BattleAi defeat flow", () => {
37
42
  afterEach(() => {
38
43
  vi.useRealTimers();
39
44
  vi.restoreAllMocks();
45
+ setActionBattleSystems({});
40
46
  });
41
47
 
42
48
  test("awards the attacker and requests a defeated remove transition", () => {
@@ -118,3 +124,376 @@ describe("BattleAi defeat flow", () => {
118
124
  });
119
125
  });
120
126
  });
127
+
128
+ describe("BattleAi vision setup", () => {
129
+ afterEach(() => {
130
+ vi.useRealTimers();
131
+ vi.restoreAllMocks();
132
+ setActionBattleSystems({});
133
+ });
134
+
135
+ test("retries vision attachment when the physics body is not ready yet", () => {
136
+ vi.useFakeTimers();
137
+ const event = createEvent();
138
+ const visionShape = { id: "vision_monster-1" };
139
+ event.attachShape.mockReturnValueOnce(undefined).mockReturnValueOnce(visionShape);
140
+
141
+ const ai = new BattleAi(event as any);
142
+
143
+ expect(event.attachShape).toHaveBeenCalledTimes(1);
144
+
145
+ vi.advanceTimersByTime(60);
146
+
147
+ expect(event.attachShape).toHaveBeenCalledTimes(2);
148
+ expect(event.attachShape).toHaveBeenLastCalledWith("vision_monster-1", {
149
+ radius: 150,
150
+ width: 300,
151
+ height: 300,
152
+ angle: 360,
153
+ });
154
+
155
+ ai.destroy();
156
+ });
157
+ });
158
+
159
+ describe("BattleAi behavior tree", () => {
160
+ afterEach(() => {
161
+ vi.useRealTimers();
162
+ vi.restoreAllMocks();
163
+ setActionBattleSystems({});
164
+ });
165
+
166
+ test("executes simplified behavior intents", () => {
167
+ vi.useFakeTimers();
168
+ const event = createEvent();
169
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
170
+ const player = {
171
+ ...createPlayer(),
172
+ x: vi.fn(() => 20),
173
+ y: vi.fn(() => 0),
174
+ };
175
+ const ai = new BattleAi(event as any, {
176
+ simpleBehavior: {
177
+ when: [ifTargetVisible(chase())],
178
+ },
179
+ });
180
+
181
+ ai.onDetectInShape(player as any, {});
182
+ vi.advanceTimersByTime(100);
183
+
184
+ expect(event.moveTo).toHaveBeenCalledWith(player);
185
+ ai.destroy();
186
+ });
187
+
188
+ test("composes named AI presets with local overrides", () => {
189
+ vi.useFakeTimers();
190
+ const event = createEvent();
191
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
192
+ setActionBattleSystems({
193
+ ai: {
194
+ presets: {
195
+ slime: {
196
+ preset: "aggressive",
197
+ visionRange: 220,
198
+ simpleBehavior: {
199
+ otherwise: chase(),
200
+ },
201
+ },
202
+ },
203
+ },
204
+ });
205
+
206
+ const ai = new BattleAi(event as any, {
207
+ preset: "slime",
208
+ attackRange: 70,
209
+ });
210
+
211
+ expect(event.attachShape).toHaveBeenCalledWith("vision_monster-1", {
212
+ radius: 220,
213
+ width: 440,
214
+ height: 440,
215
+ angle: 360,
216
+ });
217
+ ai.destroy();
218
+ });
219
+
220
+ test("local behavior tree overrides preset simple behavior", () => {
221
+ vi.useFakeTimers();
222
+ const event = createEvent();
223
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
224
+ const player = {
225
+ ...createPlayer(),
226
+ hp: 10,
227
+ x: vi.fn(() => 20),
228
+ y: vi.fn(() => 0),
229
+ };
230
+ setActionBattleSystems({
231
+ ai: {
232
+ presets: {
233
+ ranged: {
234
+ simpleBehavior: {
235
+ otherwise: chase(),
236
+ },
237
+ },
238
+ },
239
+ },
240
+ });
241
+
242
+ const ai = new BattleAi(event as any, {
243
+ preset: "ranged",
244
+ behaviorTree: () => ({ status: "success", intent: idle() }),
245
+ });
246
+
247
+ ai.onDetectInShape(player as any, {});
248
+ vi.advanceTimersByTime(100);
249
+
250
+ expect(event.moveTo).not.toHaveBeenCalled();
251
+ expect(event.stopMoveTo).toHaveBeenCalled();
252
+ ai.destroy();
253
+ });
254
+
255
+ test("does not target an already defeated player", () => {
256
+ const event = createEvent();
257
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
258
+ const ai = new BattleAi(event as any);
259
+ const player = {
260
+ ...createPlayer(),
261
+ hp: 0,
262
+ x: vi.fn(() => 20),
263
+ y: vi.fn(() => 0),
264
+ };
265
+
266
+ ai.onDetectInShape(player as any, {});
267
+
268
+ expect(ai.getTarget()).toBeNull();
269
+ ai.destroy();
270
+ });
271
+
272
+ test("approaches a visible target while alert but not yet in combat range", () => {
273
+ vi.useFakeTimers();
274
+ const event = createEvent();
275
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
276
+ const player = {
277
+ ...createPlayer(),
278
+ hp: 10,
279
+ x: vi.fn(() => 120),
280
+ y: vi.fn(() => 0),
281
+ };
282
+ const ai = new BattleAi(event as any, {
283
+ attackRange: 50,
284
+ visionRange: 150,
285
+ });
286
+
287
+ ai.onDetectInShape(player as any, {});
288
+ vi.advanceTimersByTime(100);
289
+
290
+ expect(event.moveTo).toHaveBeenCalledWith(player);
291
+ ai.destroy();
292
+ });
293
+
294
+ test("normalizes position move targets before calling RPGJS moveTo", () => {
295
+ vi.useFakeTimers();
296
+ const event = createEvent();
297
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
298
+
299
+ const ai = new BattleAi(event as any, {
300
+ patrolWaypoints: [{ x: 32, y: 48 }],
301
+ moveToCooldown: 0,
302
+ });
303
+
304
+ expect(event.moveTo).toHaveBeenCalledWith({ x: 32, y: 48 });
305
+ ai.destroy();
306
+ });
307
+
308
+ test("targets its attacker after taking non-lethal damage", () => {
309
+ vi.useFakeTimers();
310
+ const event = createEvent();
311
+ event.hp = 9;
312
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
313
+ const player = {
314
+ ...createPlayer(),
315
+ hp: 10,
316
+ x: vi.fn(() => 120),
317
+ y: vi.fn(() => 0),
318
+ };
319
+ const ai = new BattleAi(event as any, {
320
+ attackRange: 50,
321
+ visionRange: 150,
322
+ });
323
+
324
+ ai.handleDamage(player as any, { damage: 1, defeated: false });
325
+
326
+ expect(ai.getTarget()).toBe(player);
327
+ ai.destroy();
328
+ });
329
+
330
+ test("waits for damage recovery before chasing its attacker", () => {
331
+ vi.useFakeTimers();
332
+ vi.setSystemTime(1000);
333
+ const event = createEvent();
334
+ event.hp = 9;
335
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
336
+ const player = {
337
+ ...createPlayer(),
338
+ hp: 10,
339
+ x: vi.fn(() => 120),
340
+ y: vi.fn(() => 0),
341
+ };
342
+ const ai = new BattleAi(event as any, {
343
+ attackRange: 50,
344
+ visionRange: 150,
345
+ hitstunMs: 100,
346
+ moveToCooldown: 0,
347
+ });
348
+
349
+ ai.handleDamage(player as any, { damage: 1, defeated: false });
350
+ vi.advanceTimersByTime(200);
351
+ expect(event.moveTo).not.toHaveBeenCalledWith(player);
352
+
353
+ vi.advanceTimersByTime(100);
354
+ expect(event.moveTo).toHaveBeenCalledWith(player);
355
+ ai.destroy();
356
+ });
357
+
358
+ test("chases its attacker after hitstun ends", () => {
359
+ vi.useFakeTimers();
360
+ vi.setSystemTime(1000);
361
+ const event = createEvent();
362
+ event.hp = 9;
363
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
364
+ const player = {
365
+ ...createPlayer(),
366
+ hp: 10,
367
+ x: vi.fn(() => 120),
368
+ y: vi.fn(() => 0),
369
+ };
370
+ const ai = new BattleAi(event as any, {
371
+ attackRange: 50,
372
+ visionRange: 150,
373
+ hitstunMs: 100,
374
+ moveToCooldown: 0,
375
+ });
376
+
377
+ ai.handleDamage(player as any, { damage: 1, defeated: false });
378
+ vi.advanceTimersByTime(300);
379
+
380
+ expect(event.moveTo).toHaveBeenCalledWith(player);
381
+ ai.destroy();
382
+ });
383
+
384
+ test("behavior tree idle fallback does not block target acquisition", () => {
385
+ vi.useFakeTimers();
386
+ const event = createEvent();
387
+ const player = {
388
+ ...createPlayer(),
389
+ hp: 10,
390
+ x: vi.fn(() => 30),
391
+ y: vi.fn(() => 0),
392
+ };
393
+ const map = {
394
+ getPlayers: vi.fn(() => [player]),
395
+ getEvents: vi.fn(() => [event]),
396
+ };
397
+ event.getCurrentMap.mockReturnValue(map);
398
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
399
+
400
+ const ai = new BattleAi(event as any, {
401
+ simpleBehavior: {
402
+ otherwise: idle(),
403
+ },
404
+ });
405
+
406
+ vi.advanceTimersByTime(100);
407
+
408
+ expect(ai.getTarget()).toBe(player);
409
+ ai.destroy();
410
+ });
411
+
412
+ test("clears its target when the player is defeated", () => {
413
+ vi.useFakeTimers();
414
+ const event = createEvent();
415
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
416
+ const player = {
417
+ ...createPlayer(),
418
+ hp: 10,
419
+ x: vi.fn(() => 20),
420
+ y: vi.fn(() => 0),
421
+ };
422
+ const ai = new BattleAi(event as any);
423
+
424
+ ai.onDetectInShape(player as any, {});
425
+ expect(ai.getTarget()).toBe(player);
426
+
427
+ player.hp = 0;
428
+ vi.advanceTimersByTime(100);
429
+
430
+ expect(ai.getTarget()).toBeNull();
431
+ expect(event.stopMoveTo).toHaveBeenCalled();
432
+ ai.destroy();
433
+ });
434
+
435
+ test("can target hostile BattleAi events by faction", () => {
436
+ vi.useFakeTimers();
437
+ const event = createEvent();
438
+ const hostile = {
439
+ ...createEvent(),
440
+ id: "bandit-1",
441
+ hp: 10,
442
+ x: vi.fn(() => 30),
443
+ y: vi.fn(() => 0),
444
+ battleAi: {
445
+ getFaction: () => "bandits",
446
+ getTargets: () => "players",
447
+ },
448
+ };
449
+ const map = {
450
+ getPlayers: vi.fn(() => []),
451
+ getEvents: vi.fn(() => [event, hostile]),
452
+ };
453
+ event.getCurrentMap.mockReturnValue(map);
454
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
455
+
456
+ const ai = new BattleAi(event as any, {
457
+ faction: "guards",
458
+ targets: ["bandits"],
459
+ });
460
+
461
+ vi.advanceTimersByTime(100);
462
+
463
+ expect(ai.getTarget()).toBe(hostile);
464
+ ai.destroy();
465
+ });
466
+
467
+ test("dash attacks emit an attack visual before the dash hit", () => {
468
+ vi.useFakeTimers();
469
+ const clientVisual = vi.fn();
470
+ const event = createEvent();
471
+ event.getCurrentMap.mockReturnValue({ clientVisual });
472
+ event.attachShape.mockReturnValue({ id: "vision_monster-1" });
473
+ const player = {
474
+ ...createPlayer(),
475
+ hp: 10,
476
+ x: vi.fn(() => 20),
477
+ y: vi.fn(() => 0),
478
+ };
479
+ const ai = new BattleAi(event as any, {
480
+ attackPatterns: [AttackPattern.DashAttack],
481
+ attackRange: 50,
482
+ moveToCooldown: 0,
483
+ });
484
+
485
+ ai.onDetectInShape(player as any, {});
486
+ (ai as any).performDashAttack();
487
+
488
+ expect(clientVisual).toHaveBeenCalledWith(
489
+ ACTION_BATTLE_CLIENT_VISUAL_ID,
490
+ expect.objectContaining({
491
+ moment: "attack",
492
+ objectId: "monster-1",
493
+ targetId: "player-1",
494
+ pattern: AttackPattern.DashAttack,
495
+ })
496
+ );
497
+ ai.destroy();
498
+ });
499
+ });