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

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 (110) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/client/ai.server.d.ts +45 -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 +2 -1
  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 +193 -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 +69 -0
  25. package/dist/client/index22.js +225 -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 +1707 -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 +45 -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 +2 -1
  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 +66 -11
  56. package/dist/server/index14.js +206 -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 +1707 -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 +198 -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 +233 -0
  79. package/src/ai.server.ts +627 -108
  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 +386 -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 +56 -0
  96. package/src/core/equipment.ts +9 -5
  97. package/src/core/targets.spec.ts +112 -0
  98. package/src/core/targets.ts +147 -0
  99. package/src/enemies/factory.ts +8 -0
  100. package/src/index.ts +111 -2
  101. package/src/locomotion.spec.ts +51 -0
  102. package/src/locomotion.ts +48 -0
  103. package/src/movement.spec.ts +78 -0
  104. package/src/movement.ts +46 -0
  105. package/src/server.ts +242 -66
  106. package/src/types.ts +105 -35
  107. package/src/ui.ts +113 -0
  108. package/src/visual.spec.ts +166 -0
  109. package/src/visual.ts +285 -0
  110. package/README.md +0 -1242
package/README.md DELETED
@@ -1,1242 +0,0 @@
1
- # Action Battle System
2
-
3
- Advanced real-time action combat AI system for RPGJS.
4
-
5
- The AI controller manages **behavior only** - all stats (HP, ATK, skills, items, etc.) are configured using the standard RPGJS API.
6
-
7
- ## Features
8
-
9
- - **State Machine AI**: Enemies with dynamic behaviors (Idle, Alert, Combat, Flee, Stunned)
10
- - **Plugin-first architecture**: Replace damage, hitboxes, knockback, hooks, and AI behaviors independently
11
- - **Multiple Enemy Types**: Aggressive, Defensive, Ranged, Tank, Berserker
12
- - **Attack Patterns**: Melee, Combo, Charged, Zone, Dash Attack
13
- - **Skill Support**: AI can use any RPGJS skill
14
- - **Dodge System**: Enemies can dodge and counter-attack
15
- - **Group Behavior**: Enemies coordinate attacks and formations
16
- - **Patrol System**: Waypoint-based patrolling
17
- - **Knockback System**: Weapon-based knockback force
18
- - **Hook System**: Customize hit behavior with `onBeforeHit` and `onAfterHit` hooks
19
-
20
- ## Installation
21
-
22
- ```bash
23
- npm install @rpgjs/action-battle
24
- ```
25
-
26
- ## Plugin-First Customization
27
-
28
- `provideActionBattle()` ships with Zelda-like defaults, but each combat system
29
- can be replaced without rewriting the module.
30
-
31
- ```ts
32
- import { provideActionBattle } from "@rpgjs/action-battle/server";
33
-
34
- export default provideActionBattle({
35
- attack: {
36
- lockMovement: true,
37
- lockDurationMs: 280,
38
- hitboxes: {
39
- right: { offsetX: 18, offsetY: -18, width: 42, height: 36 }
40
- }
41
- },
42
- systems: {
43
- combat: {
44
- damage({ attacker, target, skill }) {
45
- const raw = target.applyDamage(attacker, skill);
46
- return {
47
- damage: raw.damage,
48
- defeated: target.hp <= 0,
49
- raw
50
- };
51
- },
52
- knockback({ attacker, target }) {
53
- const dx = target.x() - attacker.x();
54
- const dy = target.y() - attacker.y();
55
- const distance = Math.max(1, Math.sqrt(dx * dx + dy * dy));
56
- return {
57
- force: 70,
58
- duration: 220,
59
- direction: { x: dx / distance, y: dy / distance }
60
- };
61
- },
62
- hooks: {
63
- beforeHit(context) {
64
- // Return false to cancel a hit, or return a modified context.
65
- return context;
66
- },
67
- afterHit(result) {
68
- console.log(`Damage: ${result.damage}`);
69
- }
70
- }
71
- },
72
- ai: {
73
- behaviors: {
74
- slime({ hpPercent }) {
75
- return {
76
- mode: hpPercent !== null && hpPercent < 0.25 ? "retreat" : "assault",
77
- attackCooldown: 900
78
- };
79
- }
80
- }
81
- }
82
- }
83
- });
84
- ```
85
-
86
- The main extension contracts are:
87
-
88
- - `ActionBattleCombatSystem`: resolves hitboxes, damage, knockback, and hooks.
89
- - `ActionBattleAiBehavior`: returns lightweight AI decisions from event state.
90
- - `ActionBattleHitHooks`: `beforeHit`, `afterDamage`, and `afterHit`.
91
-
92
- Use `createActionEnemy()` when you want data-driven enemy presets:
93
-
94
- ```ts
95
- import { createActionEnemy, EnemyType } from "@rpgjs/action-battle/server";
96
-
97
- const enemyPresets = {
98
- slime: {
99
- enemyType: EnemyType.Aggressive,
100
- behaviorKey: "slime",
101
- stats(event) {
102
- event.hp = 40;
103
- }
104
- }
105
- };
106
-
107
- createActionEnemy(this, "slime", enemyPresets);
108
- ```
109
-
110
- ## Quick Start
111
-
112
- ```typescript
113
- import { createServer, RpgPlayer, RpgEvent, EventMode, ATK, PDEF, MAXHP } from "@rpgjs/server";
114
- import { provideActionBattle, BattleAi, EnemyType } from "@rpgjs/action-battle/server";
115
-
116
- function GoblinEnemy() {
117
- return {
118
- name: "Goblin",
119
- mode: EventMode.Scenario,
120
- onInit() {
121
- this.setGraphic("goblin");
122
-
123
- // Configure stats using RPGJS API
124
- this.hp = 80;
125
- this.param[MAXHP] = 80;
126
- this.param[ATK] = 15;
127
- this.param[PDEF] = 5;
128
-
129
- // Optional: Give skills
130
- // this.learnSkill(Slash);
131
-
132
- // Optional: Give items
133
- // this.addItem(Potion, 2);
134
-
135
- // Apply AI behavior
136
- new BattleAi(this, {
137
- enemyType: EnemyType.Aggressive
138
- });
139
- }
140
- };
141
- }
142
- ```
143
-
144
- ## Using RPGJS API for Stats
145
-
146
- The AI uses the event's existing stats. Configure them in `onInit`:
147
-
148
- ### Health & Resources
149
-
150
- ```typescript
151
- this.hp = 100; // Current HP
152
- this.param[MAXHP] = 100; // Max HP
153
- this.sp = 50; // SP for skills
154
- this.param[MAXSP] = 50; // Max SP
155
- ```
156
-
157
- ### Parameters
158
-
159
- ```typescript
160
- import { ATK, PDEF, SDEF } from "@rpgjs/server";
161
-
162
- this.param[ATK] = 20; // Attack power
163
- this.param[PDEF] = 10; // Physical defense
164
- this.param[SDEF] = 8; // Special defense
165
- ```
166
-
167
- ### Skills
168
-
169
- ```typescript
170
- import { Fireball, Heal } from './database/skills';
171
-
172
- this.learnSkill(Fireball);
173
- this.learnSkill(Heal);
174
- ```
175
-
176
- ### Items & Equipment
177
-
178
- ```typescript
179
- import { Sword, Shield, Potion } from './database/items';
180
-
181
- this.addItem(Potion, 3);
182
- this.equip(Sword);
183
- this.equip(Shield);
184
- ```
185
-
186
- ### Classes
187
-
188
- ```typescript
189
- import { WarriorClass } from './database/classes';
190
-
191
- this.setClass(WarriorClass);
192
- ```
193
-
194
- ### States
195
-
196
- ```typescript
197
- import { PoisonState } from './database/states';
198
-
199
- this.addState(PoisonState);
200
- ```
201
-
202
- ## AI Configuration
203
-
204
- The AI only controls **behavior**. All options are optional:
205
-
206
- ```typescript
207
- new BattleAi(event, {
208
- // Enemy type (affects behavior, not stats)
209
- enemyType: EnemyType.Aggressive,
210
-
211
- // Skill to use for attacks (optional)
212
- attackSkill: Fireball,
213
-
214
- // Timing
215
- attackCooldown: 1000, // ms between attacks
216
-
217
- // Ranges
218
- visionRange: 150, // Detection radius
219
- attackRange: 60, // Attack distance
220
-
221
- // Dodge behavior
222
- dodgeChance: 0.2, // 0-1 probability
223
- dodgeCooldown: 2000, // ms between dodges
224
-
225
- // Flee behavior
226
- fleeThreshold: 0.2, // Flee when HP < 20%
227
-
228
- // Attack patterns
229
- attackPatterns: [
230
- AttackPattern.Melee,
231
- AttackPattern.Combo,
232
- AttackPattern.DashAttack
233
- ],
234
-
235
- // Per-pattern enemy attack timing and reactions
236
- attackProfiles: {
237
- charged: {
238
- startupMs: 900,
239
- activeMs: 140,
240
- recoveryMs: 300,
241
- reaction: {
242
- hitstunMs: 240,
243
- staggerPower: 2
244
- }
245
- }
246
- },
247
-
248
- // Hit reaction tuning
249
- poise: 1,
250
- hitstunMs: 150,
251
- invincibilityMs: 250,
252
-
253
- // Patrol waypoints (for idle state)
254
- patrolWaypoints: [
255
- { x: 100, y: 100 },
256
- { x: 300, y: 100 }
257
- ],
258
-
259
- // Group coordination
260
- groupBehavior: true,
261
-
262
- // Callback when AI is defeated
263
- onDefeated: ({ event, attacker }) => {
264
- const name = attacker?.name?.() ?? "Unknown";
265
- console.log(`${event.name} was defeated by ${name}!`);
266
- }
267
- });
268
- ```
269
-
270
- `attackProfiles` lets enemies telegraph attacks with `startupMs`, keep hitboxes
271
- active for `activeMs`, and apply hit reactions. `poise` controls interruption:
272
- an incoming hit only stuns the enemy when its `reaction.staggerPower` is greater
273
- than or equal to the enemy's `poise`.
274
-
275
- ## Enemy Types
276
-
277
- Types modify AI **behavior** (cooldowns, ranges, dodge), not stats:
278
-
279
- | Type | Attack Speed | Dodge | Behavior |
280
- |------|-------------|-------|----------|
281
- | **Aggressive** | Fast | Low | Rushes player |
282
- | **Defensive** | Slow | High | Counter-attacks |
283
- | **Ranged** | Medium | Medium | Keeps distance |
284
- | **Tank** | Slow | None | Stands ground |
285
- | **Berserker** | Variable | Low | Faster when hurt |
286
-
287
- ## Using Skills for Attacks
288
-
289
- The AI can use any RPGJS skill:
290
-
291
- ```typescript
292
- // In your database/skills.ts
293
- import { Skill } from '@rpgjs/database';
294
-
295
- @Skill({
296
- name: 'Slash',
297
- spCost: 5,
298
- power: 25,
299
- hitRate: 0.95
300
- })
301
- export class Slash {}
302
-
303
- // In your event
304
- onInit() {
305
- this.hp = 100;
306
- this.sp = 50;
307
- this.learnSkill(Slash);
308
-
309
- new BattleAi(this, {
310
- attackSkill: Slash
311
- });
312
- }
313
- ```
314
-
315
- ## AI States
316
-
317
- ```
318
- ┌─────────┐ detect ┌─────────┐ approach ┌─────────┐
319
- │ Idle │ ──────────────> │ Alert │ ─────────────> │ Combat │
320
- └─────────┘ └─────────┘ └─────────┘
321
- ^ │
322
- │ │
323
- │ ┌─────────┐ │
324
- │ │ Stunned │ <────── take damage ───────┤
325
- │ └─────────┘ │
326
- │ │ │
327
- │ v │
328
- │ ┌─────────┐ │
329
- └───────────── │ Flee │ <────── HP low ────────────┘
330
- └─────────┘
331
- ```
332
-
333
- ## Attack Patterns
334
-
335
- | Pattern | Description |
336
- |---------|-------------|
337
- | **Melee** | Single attack |
338
- | **Combo** | 2-3 rapid attacks |
339
- | **Charged** | Wind-up, stronger attack |
340
- | **Zone** | 360° area attack |
341
- | **DashAttack** | Rush toward target then attack |
342
-
343
- ## Examples
344
-
345
- ### Basic Enemy
346
-
347
- ```typescript
348
- function Goblin() {
349
- return {
350
- name: "Goblin",
351
- onInit() {
352
- this.setGraphic("goblin");
353
- this.hp = 50;
354
- this.param[MAXHP] = 50;
355
- this.param[ATK] = 10;
356
-
357
- new BattleAi(this);
358
- }
359
- };
360
- }
361
- ```
362
-
363
- ### Mage with Skills
364
-
365
- ```typescript
366
- function DarkMage() {
367
- return {
368
- name: "Dark Mage",
369
- onInit() {
370
- this.setGraphic("mage");
371
- this.hp = 60;
372
- this.sp = 100;
373
- this.param[MAXHP] = 60;
374
- this.param[MAXSP] = 100;
375
- this.param[ATK] = 25;
376
-
377
- this.learnSkill(Fireball);
378
- this.learnSkill(IceSpike);
379
-
380
- new BattleAi(this, {
381
- enemyType: EnemyType.Ranged,
382
- attackSkill: Fireball,
383
- visionRange: 200
384
- });
385
- }
386
- };
387
- }
388
- ```
389
-
390
- ### Boss
391
-
392
- ```typescript
393
- function DragonBoss() {
394
- return {
395
- name: "Dragon",
396
- onInit() {
397
- this.setGraphic("dragon");
398
- this.hp = 500;
399
- this.param[MAXHP] = 500;
400
- this.param[ATK] = 50;
401
- this.param[PDEF] = 30;
402
-
403
- this.learnSkill(FireBreath);
404
- this.learnSkill(TailSwipe);
405
-
406
- new BattleAi(this, {
407
- enemyType: EnemyType.Tank,
408
- attackSkill: FireBreath,
409
- attackPatterns: [
410
- AttackPattern.Melee,
411
- AttackPattern.Zone,
412
- AttackPattern.Charged
413
- ],
414
- fleeThreshold: 0.1,
415
- visionRange: 250
416
- });
417
- }
418
- };
419
- }
420
- ```
421
-
422
- ### Patrol Guard
423
-
424
- ```typescript
425
- function PatrolGuard() {
426
- return {
427
- name: "Guard",
428
- onInit() {
429
- this.setGraphic("guard");
430
- this.hp = 80;
431
- this.param[MAXHP] = 80;
432
- this.param[ATK] = 15;
433
-
434
- new BattleAi(this, {
435
- enemyType: EnemyType.Defensive,
436
- patrolWaypoints: [
437
- { x: 100, y: 150 },
438
- { x: 300, y: 150 },
439
- { x: 300, y: 350 },
440
- { x: 100, y: 350 }
441
- ]
442
- });
443
- }
444
- };
445
- }
446
- ```
447
-
448
- ### Wolf Pack (Group)
449
-
450
- ```typescript
451
- function Wolf() {
452
- return {
453
- name: "Wolf",
454
- onInit() {
455
- this.setGraphic("wolf");
456
- this.hp = 40;
457
- this.param[MAXHP] = 40;
458
- this.param[ATK] = 12;
459
-
460
- new BattleAi(this, {
461
- enemyType: EnemyType.Aggressive,
462
- groupBehavior: true,
463
- attackPatterns: [
464
- AttackPattern.Melee,
465
- AttackPattern.Combo
466
- ]
467
- });
468
- }
469
- };
470
- }
471
- ```
472
-
473
- ### Complete Example with Weapons
474
-
475
- ```typescript
476
- import { createServer, RpgPlayer, RpgMap, EventMode, MAXHP, ATK, PDEF } from "@rpgjs/server";
477
- import { provideActionBattle, BattleAi, EnemyType } from "@rpgjs/action-battle/server";
478
-
479
- // Define weapons with knockback
480
- const IronSword = {
481
- id: 'iron-sword',
482
- name: 'Iron Sword',
483
- description: 'A reliable iron sword',
484
- atk: 15,
485
- knockbackForce: 40,
486
- _type: 'weapon' as const,
487
- };
488
-
489
- const GiantMaul = {
490
- id: 'giant-maul',
491
- name: 'Giant Maul',
492
- description: 'Massive hammer with devastating knockback',
493
- atk: 30,
494
- knockbackForce: 100,
495
- _type: 'weapon' as const,
496
- };
497
-
498
- const GoblinDagger = {
499
- id: 'goblin-dagger',
500
- name: 'Goblin Dagger',
501
- description: 'Small rusty dagger',
502
- atk: 8,
503
- knockbackForce: 20,
504
- _type: 'weapon' as const,
505
- };
506
-
507
- // Enemy with weapon
508
- function GoblinWarrior() {
509
- return {
510
- name: "Goblin Warrior",
511
- mode: EventMode.Scenario,
512
- onInit() {
513
- this.setGraphic("goblin");
514
-
515
- // Stats
516
- this.hp = 60;
517
- this.param[MAXHP] = 60;
518
- this.param[ATK] = 12;
519
- this.param[PDEF] = 5;
520
-
521
- // Equip weapon (knockbackForce: 20)
522
- this.addItem(GoblinDagger);
523
- this.equip(GoblinDagger.id);
524
-
525
- // AI
526
- new BattleAi(this, {
527
- enemyType: EnemyType.Aggressive,
528
- attackRange: 45
529
- });
530
- }
531
- };
532
- }
533
-
534
- // Server setup
535
- export default createServer({
536
- providers: [
537
- provideActionBattle(),
538
- {
539
- database: {
540
- 'iron-sword': IronSword,
541
- 'giant-maul': GiantMaul,
542
- 'goblin-dagger': GoblinDagger
543
- },
544
- player: {
545
- onJoinMap(player: RpgPlayer, map: RpgMap) {
546
- // Setup player stats
547
- player.hp = 100;
548
- player.param[MAXHP] = 100;
549
- player.param[ATK] = 15;
550
-
551
- // Give player a weapon with high knockback
552
- player.addItem(GiantMaul);
553
- player.equip(GiantMaul.id);
554
-
555
- // Player attacks will now knock enemies back with force 100
556
- }
557
- },
558
- maps: [
559
- {
560
- id: 'battle-map',
561
- events: [{ event: GoblinWarrior() }]
562
- }
563
- ]
564
- }
565
- ]
566
- });
567
- ```
568
-
569
- ## API Reference
570
-
571
- ### BattleAi Methods
572
-
573
- ```typescript
574
- // Get current health (uses event.hp)
575
- ai.getHealth(): number
576
-
577
- // Get max health (uses event.param[MAXHP])
578
- ai.getMaxHealth(): number
579
-
580
- // Get current target
581
- ai.getTarget(): RpgPlayer | null
582
-
583
- // Get current AI state
584
- ai.getState(): AiState
585
-
586
- // Get enemy type
587
- ai.getEnemyType(): EnemyType
588
-
589
- // Handle damage (called automatically)
590
- ai.takeDamage(attacker: RpgPlayer): boolean
591
-
592
- // Clean up AI instance
593
- ai.destroy(): void
594
- ```
595
-
596
- ## Player Combat
597
-
598
- The module handles player attacks via the `action` input:
599
-
600
- ```typescript
601
- // Player presses action key -> attack animation + hitbox
602
- // Hitbox detects enemy -> applyPlayerHitToEvent(player, event)
603
- // Damage uses RPGJS formula: target.applyDamage(attacker)
604
- // Knockback force is based on equipped weapon's knockbackForce property
605
- ```
606
-
607
- By default, the player is locked in place for `350ms` when attacking, similar
608
- to classic A-RPG combat where the attack resolves before movement resumes.
609
-
610
- ```ts
611
- provideActionBattle({
612
- attack: {
613
- lockMovement: true,
614
- lockDurationMs: 350,
615
- showPreview: true,
616
- previewDurationMs: 180,
617
- previewColor: 0xfff3b0,
618
- previewAccentColor: 0xffffff
619
- }
620
- });
621
- ```
622
-
623
- Set `lockMovement` to `false` if your game should allow moving attacks.
624
- The client also stops predicted movement immediately and shows a short slash
625
- preview so the action feels responsive even before the server hit resolves. Set
626
- `showPreview` to `false` if your project uses only spritesheet combat
627
- animations.
628
-
629
- Player attacks are resolved with `createMovingHitbox()` instead of a passive
630
- contact collision. You can still customize the generated hitboxes with
631
- `attack.hitboxes` or `attack.resolveHitboxes`.
632
-
633
- ### Attack profile model
634
-
635
- Use `attack.profile` to describe the timing model of a player attack in one
636
- typed object. A profile separates the attack into startup, active, and recovery
637
- phases so combat systems can share the same vocabulary.
638
-
639
- ```ts
640
- import { provideActionBattle } from "@rpgjs/action-battle/server";
641
-
642
- export default provideActionBattle({
643
- attack: {
644
- profile: {
645
- id: "iron-sword",
646
- startupMs: 80,
647
- activeMs: 120,
648
- recoveryMs: 180,
649
- cooldownMs: 380,
650
- movementLock: true,
651
- directionLock: true,
652
- animationKey: "attack",
653
- hitPolicy: "oncePerTarget",
654
- reaction: {
655
- invincibilityMs: 250,
656
- hitstunMs: 150,
657
- staggerPower: 1
658
- },
659
- hitboxes: {
660
- right: { offsetX: 18, offsetY: -18, width: 42, height: 36 }
661
- }
662
- },
663
- lockDurationMs: 380
664
- }
665
- });
666
- ```
667
-
668
- The default profile mirrors the legacy attack lock: no startup, a short active
669
- window, and recovery that totals `350ms`. The player attack runtime uses
670
- `startupMs` before creating the hitbox, `activeMs` to keep the hitbox active,
671
- and `totalDurationMs` for movement and direction locks. `hitPolicy:
672
- "oncePerTarget"` prevents the same attack window from damaging the same target
673
- multiple times.
674
-
675
- `reaction` describes what happens after the hit connects:
676
-
677
- - `invincibilityMs`: temporary invincibility after damage.
678
- - `hitstunMs`: stun duration requested by the hit.
679
- - `staggerPower`: value compared against enemy `poise`.
680
-
681
- ```ts
682
- import {
683
- normalizeActionBattleAttackProfile,
684
- type ActionBattleAttackProfile
685
- } from "@rpgjs/action-battle/server";
686
-
687
- const sword: ActionBattleAttackProfile = {
688
- id: "sword",
689
- startupMs: 70,
690
- activeMs: 110,
691
- recoveryMs: 170
692
- };
693
-
694
- const normalized = normalizeActionBattleAttackProfile(sword);
695
- ```
696
-
697
- Equipped weapons can override the player attack profile:
698
-
699
- ```ts
700
- const Dagger = {
701
- id: "dagger",
702
- name: "Dagger",
703
- _type: "weapon" as const,
704
- atk: 8,
705
- knockbackForce: 20,
706
- attackProfile: {
707
- id: "dagger",
708
- startupMs: 40,
709
- activeMs: 70,
710
- recoveryMs: 110
711
- }
712
- };
713
- ```
714
-
715
- Enable lightweight attack logs while tuning profiles:
716
-
717
- ```ts
718
- provideActionBattle({
719
- debug: {
720
- attacks: true
721
- }
722
- });
723
- ```
724
-
725
- When the action targets a normal event with no `BattleAi`, the server lets the
726
- event handle `onAction` and does not create the combat hitbox. Enemy events
727
- with `BattleAi` still trigger the A-RPG attack.
728
-
729
- ## Configurable Combat Animations
730
-
731
- By default, player and AI attacks keep using the existing `attack` animation:
732
-
733
- ```ts
734
- player.setGraphicAnimation("attack", 1);
735
- ```
736
-
737
- Use `animations` when your combat sprites are stored in separate graphics such
738
- as `hero_attack`, `hero_hurt`, or `hero_die`.
739
-
740
- ```ts
741
- import { provideActionBattle } from "@rpgjs/action-battle/server";
742
-
743
- export default provideActionBattle({
744
- animations: {
745
- attack: "attack",
746
- hurt: "hurt",
747
- die: {
748
- animationName: "die",
749
- repeat: 1,
750
- delayMs: 500
751
- },
752
- castSkill: "skill"
753
- }
754
- });
755
- ```
756
-
757
- RPGJS Studio stores combat animations as spritesheet media ids. If
758
- `provideStudioGame()` is installed, `createStudioActionBattleAnimations()` can
759
- read the project animations attached to the player at runtime. By default, the
760
- helper plays Studio attack spritesheets with
761
- `setGraphicAnimation("attack", graphic, 1)`:
762
-
763
- ```ts
764
- import { provideActionBattle } from "@rpgjs/action-battle/server";
765
- import { createStudioActionBattleAnimations } from "@rpgjs/studio/server";
766
-
767
- export default provideActionBattle({
768
- animations: createStudioActionBattleAnimations()
769
- });
770
- ```
771
-
772
- You can also pass a static Studio animation object to override the media ids
773
- manually. Animation values may be media ids or media objects returned by the
774
- Studio game API.
775
-
776
- The Studio field `castSpell` is accepted as an alias for action-battle's
777
- `castSkill` animation key.
778
-
779
- For data-driven spritesheets, use resolver functions:
780
-
781
- ```ts
782
- provideActionBattle({
783
- animations: {
784
- attack: (entity) => ({
785
- animationName: "walk",
786
- graphic: entity.combatAnimations?.attack,
787
- repeat: 1
788
- }),
789
- hurt: (entity) => ({
790
- animationName: "walk",
791
- graphic: entity.combatAnimations?.hurt,
792
- repeat: 1
793
- }),
794
- die: (entity) => ({
795
- animationName: "walk",
796
- graphic: entity.combatAnimations?.die,
797
- repeat: 1,
798
- waitEnd: true
799
- }),
800
- castSkill: (entity, context) => ({
801
- animationName: "walk",
802
- graphic: entity.combatAnimations?.castSkill,
803
- repeat: 1
804
- })
805
- }
806
- });
807
- ```
808
-
809
- When `graphic` is provided, action-battle calls:
810
-
811
- ```ts
812
- entity.setGraphicAnimation(animationName, graphic, repeat);
813
- ```
814
-
815
- Otherwise it calls:
816
-
817
- ```ts
818
- entity.setGraphicAnimation(animationName, repeat);
819
- ```
820
-
821
- Return `null` or `undefined` from a resolver to skip the animation. `BattleAi`
822
- can also override the global configuration per enemy:
823
-
824
- ```ts
825
- new BattleAi(this, {
826
- animations: {
827
- attack: {
828
- animationName: "walk",
829
- graphic: "slime_attack",
830
- repeat: 1
831
- },
832
- die: {
833
- animationName: "walk",
834
- graphic: "slime_die",
835
- repeat: 1,
836
- delayMs: 700
837
- }
838
- }
839
- });
840
- ```
841
-
842
- `waitEnd: true` uses the default defeated transition timeout. Use `delayMs`
843
- when you need an exact duration. The visual transition itself is handled by the
844
- client `sprite.onBeforeRemove` hook.
845
-
846
- ## Knockback System
847
-
848
- Knockback force is determined by the equipped weapon's `knockbackForce` property.
849
-
850
- ### Creating Weapons with Knockback
851
-
852
- ```typescript
853
- // Light weapon - low knockback
854
- const Dagger = {
855
- id: 'dagger',
856
- name: 'Iron Dagger',
857
- atk: 10,
858
- knockbackForce: 20,
859
- _type: 'weapon' as const,
860
- };
861
-
862
- // Heavy weapon - high knockback
863
- const Warhammer = {
864
- id: 'warhammer',
865
- name: 'War Hammer',
866
- atk: 30,
867
- knockbackForce: 100,
868
- _type: 'weapon' as const,
869
- };
870
- ```
871
-
872
- ### Default Knockback
873
-
874
- If no weapon is equipped or the weapon doesn't have `knockbackForce`, the default value is used:
875
-
876
- ```typescript
877
- import { DEFAULT_KNOCKBACK } from "@rpgjs/action-battle/server";
878
-
879
- console.log(DEFAULT_KNOCKBACK.force); // 50
880
- console.log(DEFAULT_KNOCKBACK.duration); // 300ms
881
- ```
882
-
883
- ## Hook System
884
-
885
- Customize hit behavior using hooks. Available on both player-to-enemy and enemy-to-player hits.
886
-
887
- ### HitResult Interface
888
-
889
- ```typescript
890
- interface HitResult {
891
- damage: number; // Damage dealt
892
- knockbackForce: number; // Knockback force (from weapon)
893
- knockbackDuration: number; // Knockback duration in ms
894
- defeated: boolean; // Whether target was defeated
895
- attacker: RpgPlayer | RpgEvent;
896
- target: RpgPlayer | RpgEvent;
897
- }
898
- ```
899
-
900
- ### Using Hooks with applyPlayerHitToEvent
901
-
902
- ```typescript
903
- import { applyPlayerHitToEvent } from "@rpgjs/action-battle/server";
904
-
905
- // In your custom attack handler
906
- const result = applyPlayerHitToEvent(player, event, {
907
- onBeforeHit(hitResult) {
908
- // Modify knockback for armored enemies
909
- if ((hitResult.target as any).hasState?.('armored')) {
910
- hitResult.knockbackForce *= 0.5;
911
- }
912
-
913
- // Critical hit - double knockback
914
- if (Math.random() < 0.1) {
915
- hitResult.knockbackForce *= 2;
916
- console.log('Critical hit!');
917
- }
918
-
919
- return hitResult; // Must return modified result
920
- },
921
-
922
- onAfterHit(hitResult) {
923
- // Award gold on kill
924
- if (hitResult.defeated) {
925
- (hitResult.attacker as any).gold += 10;
926
- }
927
-
928
- // Apply poison on hit (30% chance)
929
- if (Math.random() < 0.3) {
930
- (hitResult.target as any).addState?.('poison');
931
- }
932
-
933
- // Play custom sound
934
- playSound('hit');
935
- }
936
- });
937
- ```
938
-
939
- ### Custom Attack Implementation
940
-
941
- Override the default attack to add custom hooks:
942
-
943
- ```typescript
944
- import {
945
- applyPlayerHitToEvent,
946
- DEFAULT_PLAYER_ATTACK_HITBOXES,
947
- getPlayerWeaponKnockbackForce
948
- } from "@rpgjs/action-battle/server";
949
-
950
- // Custom attack with hooks
951
- function customAttack(player: RpgPlayer) {
952
- player.setGraphicAnimation('attack', 1);
953
-
954
- const direction = player.getDirection();
955
- const hitboxConfig = DEFAULT_PLAYER_ATTACK_HITBOXES[direction] || DEFAULT_PLAYER_ATTACK_HITBOXES.default;
956
-
957
- const hitboxes = [{
958
- x: player.x() + hitboxConfig.offsetX,
959
- y: player.y() + hitboxConfig.offsetY,
960
- width: hitboxConfig.width,
961
- height: hitboxConfig.height
962
- }];
963
-
964
- const map = player.getCurrentMap();
965
- map?.createMovingHitbox(hitboxes, { speed: 3 }).subscribe({
966
- next(hits) {
967
- hits.forEach((hit) => {
968
- if (hit instanceof RpgEvent) {
969
- applyPlayerHitToEvent(player, hit, {
970
- onBeforeHit(result) {
971
- // Custom modifications
972
- return result;
973
- },
974
- onAfterHit(result) {
975
- // Custom effects
976
- }
977
- });
978
- }
979
- });
980
- }
981
- });
982
- }
983
- ```
984
-
985
- ### Getting Weapon Knockback Force
986
-
987
- ```typescript
988
- import { getPlayerWeaponKnockbackForce } from "@rpgjs/action-battle/server";
989
-
990
- const force = getPlayerWeaponKnockbackForce(player);
991
- console.log(`Player knockback force: ${force}`);
992
- ```
993
-
994
- ## onDefeated Hook
995
-
996
- The `onDefeated` callback is triggered when an AI enemy is killed. The simplest
997
- reward flow is configured directly on `BattleAi`; the reward is given to the
998
- player who landed the killing blow.
999
-
1000
- ```typescript
1001
- new BattleAi(this, {
1002
- enemyType: EnemyType.Aggressive,
1003
- animations: {
1004
- die: {
1005
- animationName: "die",
1006
- graphic: "goblin_die",
1007
- repeat: 1,
1008
- delayMs: 700
1009
- }
1010
- },
1011
- rewards: {
1012
- exp: 50,
1013
- gold: 25,
1014
- items: [{ itemId: "health_potion", amount: 1, chance: 30 }],
1015
- showNotification: true
1016
- }
1017
- });
1018
- ```
1019
-
1020
- On defeat, `BattleAi` stops the AI, awards configured rewards once, and calls
1021
- `event.remove({ reason: "defeated", transition })`. The client can use
1022
- `sprite.onBeforeRemove` to play the `die` transition before the sprite
1023
- disappears.
1024
-
1025
- `onDefeated` receives a context object in new code:
1026
- - Award experience, gold, or items to the player
1027
- - Spawn loot drops
1028
- - Trigger events or cutscenes
1029
- - Update quest progress
1030
- - Play death sounds
1031
-
1032
- ### Basic Usage
1033
-
1034
- ```typescript
1035
- new BattleAi(this, {
1036
- enemyType: EnemyType.Aggressive,
1037
- onDefeated: ({ event, attacker }) => {
1038
- const name = attacker?.name?.() ?? "Unknown";
1039
- console.log(`${event.name} was defeated by ${name}!`);
1040
- }
1041
- });
1042
- ```
1043
-
1044
- The legacy `(event, attacker)` callback signature is still supported for
1045
- two-argument callbacks.
1046
-
1047
- ### Award Rewards on Kill
1048
-
1049
- ```typescript
1050
- function Goblin() {
1051
- return {
1052
- name: "Goblin",
1053
- onInit() {
1054
- this.setGraphic("goblin");
1055
- this.hp = 50;
1056
- this.param[MAXHP] = 50;
1057
- this.param[ATK] = 10;
1058
-
1059
- new BattleAi(this, {
1060
- enemyType: EnemyType.Aggressive,
1061
- rewards: {
1062
- gold: 25,
1063
- exp: 50,
1064
- items: [{ item: HealthPotion, amount: 1, chance: 30 }]
1065
- }
1066
- });
1067
- }
1068
- };
1069
- }
1070
- ```
1071
-
1072
- ### Spawn Loot on Death
1073
-
1074
- ```typescript
1075
- new BattleAi(this, {
1076
- onDefeated: ({ event }) => {
1077
- const map = event.getCurrentMap();
1078
- if (!map) return;
1079
-
1080
- // Spawn loot at enemy position
1081
- map.createDynamicEvent({
1082
- x: event.x(),
1083
- y: event.y(),
1084
- event: LootChest({ items: [GoldCoin, HealthPotion] })
1085
- });
1086
- }
1087
- });
1088
- ```
1089
-
1090
- ### Track Kill Count
1091
-
1092
- ```typescript
1093
- let killCount = 0;
1094
-
1095
- new BattleAi(this, {
1096
- onDefeated: () => {
1097
- killCount++;
1098
-
1099
- // Check quest progress
1100
- if (killCount >= 10) {
1101
- triggerQuestComplete('slay_goblins');
1102
- }
1103
- }
1104
- });
1105
- ```
1106
-
1107
- ### Boss Death Event
1108
-
1109
- ```typescript
1110
- function DragonBoss() {
1111
- return {
1112
- name: "Ancient Dragon",
1113
- onInit() {
1114
- this.setGraphic("dragon");
1115
- this.hp = 1000;
1116
- this.param[MAXHP] = 1000;
1117
-
1118
- new BattleAi(this, {
1119
- enemyType: EnemyType.Tank,
1120
- onDefeated: ({ event }) => {
1121
- const map = event.getCurrentMap();
1122
-
1123
- // Announce victory
1124
- map?.getPlayersIn()?.forEach(player => {
1125
- player.showNotification({
1126
- message: "The Ancient Dragon has been slain!",
1127
- time: 5000
1128
- });
1129
-
1130
- // Reward all participants
1131
- player.gold += 1000;
1132
- player.exp += 5000;
1133
- player.addItem(DragonScale);
1134
- });
1135
-
1136
- // Open dungeon exit
1137
- map?.setTileProperty(exitX, exitY, { passable: true });
1138
- }
1139
- });
1140
- }
1141
- };
1142
- }
1143
- ```
1144
-
1145
- ## Visual Feedback
1146
-
1147
- Automatic feedback:
1148
-
1149
- - **Flash Effect**: Red flash when taking damage
1150
- - **Damage Numbers**: Floating damage text
1151
- - **Attack Animation**: Triggers `attack` animation
1152
- - **Knockback**: Entities pushed back based on weapon `knockbackForce`
1153
-
1154
- ## Action Bar + AoE Targeting (client + server)
1155
-
1156
- The action-battle package includes optional GUI components for an A-RPG action bar
1157
- and AoE skill targeting. They are disabled by default and are configured via
1158
- `provideActionBattle()`.
1159
-
1160
- ### Enable the Action Bar
1161
-
1162
- ```ts
1163
- import { provideActionBattle } from "@rpgjs/action-battle";
1164
-
1165
- export default provideActionBattle({
1166
- ui: {
1167
- actionBar: {
1168
- enabled: true,
1169
- autoOpen: true,
1170
- mode: "both" // "items" | "skills" | "both"
1171
- }
1172
- }
1173
- });
1174
- ```
1175
-
1176
- You can open/close it manually on the server:
1177
-
1178
- ```ts
1179
- import { openActionBattleActionBar } from "@rpgjs/action-battle/server";
1180
-
1181
- openActionBattleActionBar(player);
1182
- ```
1183
-
1184
- ### Skill Range + AoE Mask (ASCII)
1185
-
1186
- Define range and AoE mask on the skill data (custom fields). The range uses
1187
- Manhattan distance, and the mask is centered on the target tile.
1188
-
1189
- ```ts
1190
- @Skill({
1191
- name: "Nova",
1192
- spCost: 12,
1193
- // Custom fields used by action-battle
1194
- range: 3,
1195
- aoeMask: [
1196
- ".#.",
1197
- "###",
1198
- ".#."
1199
- ]
1200
- })
1201
- export class Nova {}
1202
- ```
1203
-
1204
- ### Targeting Options
1205
-
1206
- ```ts
1207
- export default provideActionBattle({
1208
- ui: {
1209
- actionBar: {
1210
- enabled: true,
1211
- autoOpen: false
1212
- },
1213
- targeting: {
1214
- enabled: true,
1215
- showGrid: true,
1216
- colors: {
1217
- area: 0x2f9ef7,
1218
- edge: 0x1b6a98,
1219
- cursor: 0xffd166
1220
- }
1221
- }
1222
- },
1223
- targeting: {
1224
- affects: "events", // "events" | "players" | "both"
1225
- allowEmptyTarget: true
1226
- }
1227
- });
1228
- ```
1229
-
1230
- ### Custom Targeting Resolver (optional)
1231
-
1232
- If you prefer to compute targeting from your own skill schema, use `getTargeting`:
1233
-
1234
- ```ts
1235
- export default provideActionBattle({
1236
- skills: {
1237
- getTargeting(skill) {
1238
- return skill?.targeting;
1239
- }
1240
- }
1241
- });
1242
- ```