@rpgjs/action-battle 5.0.0-beta.4 → 5.0.0-beta.6

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 (63) hide show
  1. package/README.md +115 -0
  2. package/dist/ai.server.d.ts +17 -2
  3. package/dist/client/index.js +13 -8
  4. package/dist/client/index10.js +54 -136
  5. package/dist/client/index11.js +52 -23
  6. package/dist/client/index12.js +101 -1217
  7. package/dist/client/index13.js +139 -42
  8. package/dist/client/index14.js +23 -8
  9. package/dist/client/index15.js +68 -444
  10. package/dist/client/index16.js +1281 -0
  11. package/dist/client/index17.js +13 -0
  12. package/dist/client/index18.js +60 -0
  13. package/dist/client/index19.js +10 -0
  14. package/dist/client/index2.js +25 -87
  15. package/dist/client/index20.js +504 -0
  16. package/dist/client/index3.js +45 -83
  17. package/dist/client/index4.js +98 -297
  18. package/dist/client/index5.js +81 -33
  19. package/dist/client/index6.js +284 -78
  20. package/dist/client/index7.js +33 -74
  21. package/dist/client/index8.js +95 -55
  22. package/dist/client/index9.js +75 -96
  23. package/dist/core/attack-profile.d.ts +9 -0
  24. package/dist/core/attack-runtime.d.ts +20 -0
  25. package/dist/core/enemy-attack-profiles.d.ts +6 -0
  26. package/dist/core/equipment.d.ts +2 -0
  27. package/dist/core/hit-reaction.d.ts +5 -0
  28. package/dist/index.d.ts +6 -1
  29. package/dist/server/index.js +12 -7
  30. package/dist/server/index10.js +1278 -8
  31. package/dist/server/index11.js +37 -0
  32. package/dist/server/index12.js +60 -0
  33. package/dist/server/index13.js +13 -0
  34. package/dist/server/index14.js +503 -0
  35. package/dist/server/index15.js +10 -0
  36. package/dist/server/index3.js +25 -87
  37. package/dist/server/index4.js +45 -141
  38. package/dist/server/index5.js +104 -21
  39. package/dist/server/index6.js +137 -1215
  40. package/dist/server/index7.js +22 -34
  41. package/dist/server/index8.js +70 -44
  42. package/dist/server/index9.js +44 -437
  43. package/dist/server.d.ts +7 -1
  44. package/package.json +5 -5
  45. package/src/ai.server.ts +172 -43
  46. package/src/client.ts +21 -12
  47. package/src/config.ts +17 -2
  48. package/src/core/attack-profile.spec.ts +118 -0
  49. package/src/core/attack-profile.ts +100 -0
  50. package/src/core/attack-runtime.spec.ts +103 -0
  51. package/src/core/attack-runtime.ts +83 -0
  52. package/src/core/contracts.ts +3 -0
  53. package/src/core/enemy-attack-profiles.spec.ts +35 -0
  54. package/src/core/enemy-attack-profiles.ts +103 -0
  55. package/src/core/equipment.spec.ts +37 -0
  56. package/src/core/equipment.ts +17 -0
  57. package/src/core/hit-reaction.spec.ts +43 -0
  58. package/src/core/hit-reaction.ts +70 -0
  59. package/src/core/hit.spec.ts +54 -1
  60. package/src/core/hit.ts +26 -0
  61. package/src/index.ts +36 -0
  62. package/src/server.ts +180 -33
  63. package/src/types.ts +62 -6
package/src/core/hit.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import type { ActionBattleCombatSystem, ActionBattleHitContext, ActionBattleHitResult } from "./contracts";
2
+ import {
3
+ isActionBattleEntityInvincible,
4
+ setActionBattleInvincibility,
5
+ } from "./hit-reaction";
2
6
 
3
7
  export const applyActionBattleHit = (
4
8
  system: ActionBattleCombatSystem,
@@ -20,6 +24,20 @@ export const applyActionBattleHit = (
20
24
  }
21
25
  if (before) hitContext = before;
22
26
 
27
+ if (isActionBattleEntityInvincible(hitContext.target)) {
28
+ return {
29
+ damage: 0,
30
+ knockbackForce: 0,
31
+ knockbackDuration: 0,
32
+ defeated: false,
33
+ attacker: hitContext.attacker,
34
+ target: hitContext.target,
35
+ cancelled: true,
36
+ metadata: hitContext.metadata,
37
+ reaction: hitContext.reaction,
38
+ };
39
+ }
40
+
23
41
  const damage =
24
42
  hitContext.damage ??
25
43
  system.resolveDamage({
@@ -50,6 +68,13 @@ export const applyActionBattleHit = (
50
68
  );
51
69
  }
52
70
 
71
+ if (!damage.defeated && hitContext.reaction?.invincibilityMs) {
72
+ setActionBattleInvincibility(
73
+ hitContext.target,
74
+ hitContext.reaction.invincibilityMs
75
+ );
76
+ }
77
+
53
78
  const result: ActionBattleHitResult = {
54
79
  damage: damage.damage,
55
80
  knockbackForce: knockback.force,
@@ -58,6 +83,7 @@ export const applyActionBattleHit = (
58
83
  attacker: hitContext.attacker,
59
84
  target: hitContext.target,
60
85
  rawDamage: damage.raw,
86
+ reaction: hitContext.reaction,
61
87
  metadata: hitContext.metadata,
62
88
  };
63
89
 
package/src/index.ts CHANGED
@@ -25,6 +25,15 @@ export type {
25
25
  ActionBattleUiOptions,
26
26
  ActionBattleUiActionBarOptions,
27
27
  ActionBattleUiTargetingOptions,
28
+ ActionBattleAttackDirection,
29
+ ActionBattleAttackHitboxConfig,
30
+ ActionBattleAttackHitboxMap,
31
+ ActionBattleAttackHitPolicy,
32
+ ActionBattleAttackProfile,
33
+ ActionBattleDebugOptions,
34
+ ActionBattleHitReactionProfile,
35
+ NormalizedActionBattleHitReactionProfile,
36
+ NormalizedActionBattleAttackProfile,
28
37
  ActionBattleCombatOptions,
29
38
  ActionBattleSystemOptions,
30
39
  ActionBattleAiSystemOptions,
@@ -47,6 +56,33 @@ export type {
47
56
  ActionBattleKnockbackResult,
48
57
  ActionBattleSystems,
49
58
  } from "./core/contracts";
59
+ export {
60
+ DEFAULT_ACTION_BATTLE_ATTACK_PROFILE,
61
+ normalizeActionBattleAttackProfile,
62
+ type ActionBattleAttackProfileFallbacks,
63
+ } from "./core/attack-profile";
64
+ export {
65
+ ACTION_BATTLE_HITBOX_FRAME_MS,
66
+ ActionBattleHitTracker,
67
+ createActionBattleAttackId,
68
+ getNormalizedActionBattleAttackProfile,
69
+ resolveActionBattleHitboxSpeed,
70
+ scheduleActionBattleStartup,
71
+ } from "./core/attack-runtime";
72
+ export {
73
+ DEFAULT_ACTION_BATTLE_HIT_REACTION,
74
+ isActionBattleEntityInvincible,
75
+ normalizeActionBattleHitReaction,
76
+ setActionBattleInvincibility,
77
+ } from "./core/hit-reaction";
78
+ export {
79
+ DEFAULT_ACTION_BATTLE_ENEMY_ATTACK_PROFILES,
80
+ normalizeActionBattleEnemyAttackProfiles,
81
+ type ActionBattleEnemyAttackProfileKey,
82
+ type ActionBattleEnemyAttackProfileMap,
83
+ type NormalizedActionBattleEnemyAttackProfileMap,
84
+ } from "./core/enemy-attack-profiles";
85
+ export { resolveActionBattleWeaponAttackProfile } from "./core/equipment";
50
86
  export {
51
87
  DEFAULT_ZELDA_PLAYER_HITBOXES,
52
88
  createDefaultPlayerHitboxResolver,
package/src/server.ts CHANGED
@@ -12,7 +12,20 @@ import { playActionBattleAnimation } from "./animations";
12
12
  import { getActionBattleSystems, setActionBattleSystems } from "./core/context";
13
13
  import { applyActionBattleHit } from "./core/hit";
14
14
  import { DEFAULT_ZELDA_PLAYER_HITBOXES } from "./core/defaults";
15
+ import {
16
+ ActionBattleHitTracker,
17
+ createActionBattleAttackId,
18
+ getNormalizedActionBattleAttackProfile,
19
+ resolveActionBattleHitboxSpeed,
20
+ scheduleActionBattleStartup,
21
+ } from "./core/attack-runtime";
22
+ import { normalizeActionBattleAttackProfile } from "./core/attack-profile";
23
+ import { resolveActionBattleWeaponAttackProfile } from "./core/equipment";
15
24
  import type { ActionBattleHitbox } from "./core/contracts";
25
+ import type {
26
+ ActionBattleAttackProfile,
27
+ NormalizedActionBattleAttackProfile,
28
+ } from "./types";
16
29
 
17
30
  export const ACTION_BATTLE_ACTION_BAR_GUI_ID = "action-battle-action-bar";
18
31
  const DEFAULT_ATTACK_LOCK_DURATION_MS = 350;
@@ -31,7 +44,8 @@ export const DEFAULT_PLAYER_ATTACK_HITBOXES = {
31
44
  const beginPlayerAttackLock = (
32
45
  player: RpgPlayer,
33
46
  map: ReturnType<RpgPlayer["getCurrentMap"]> | undefined,
34
- durationMs: number
47
+ durationMs: number,
48
+ locks: { movement: boolean; direction: boolean }
35
49
  ): boolean => {
36
50
  if (durationMs <= 0) return true;
37
51
 
@@ -53,11 +67,15 @@ const beginPlayerAttackLock = (
53
67
  const previousDirectionFixed = player.directionFixed;
54
68
  const previousAnimationFixed = player.animationFixed;
55
69
 
56
- player.pendingInputs = [];
57
- player.lastProcessedInputTs = 0;
58
- (map as any)?.stopMovement?.(player);
59
- player.canMove.set(false);
60
- player.directionFixed = true;
70
+ if (locks.movement) {
71
+ player.pendingInputs = [];
72
+ player.lastProcessedInputTs = 0;
73
+ (map as any)?.stopMovement?.(player);
74
+ player.canMove.set(false);
75
+ }
76
+ if (locks.direction) {
77
+ player.directionFixed = true;
78
+ }
61
79
 
62
80
  setTimeout(() => {
63
81
  if (runtimePlayer.__actionBattleAttackLockId !== lockId) return;
@@ -115,6 +133,16 @@ const getVisibleActionEvents = (
115
133
  collisions.forEach((id: string) => addEvent(map.getEvent(id)));
116
134
  }
117
135
 
136
+ const direction =
137
+ typeof player.getDirection === "function" ? player.getDirection() : undefined;
138
+ const interactionCollisions = (map as any).getInteractionCollisions?.(
139
+ player.id,
140
+ direction
141
+ );
142
+ if (Array.isArray(interactionCollisions)) {
143
+ interactionCollisions.forEach((id: string) => addEvent(map.getEvent(id)));
144
+ }
145
+
118
146
  for (const event of map.getEvents()) {
119
147
  const rect = eventRect(event);
120
148
  if (hitboxes.some((hitbox) => rectsOverlap(hitbox, rect))) {
@@ -200,7 +228,8 @@ export function getPlayerWeaponKnockbackForce(player: RpgPlayer): number {
200
228
  export function applyPlayerHitToEvent(
201
229
  player: RpgPlayer,
202
230
  target: RpgEvent,
203
- hooks?: ApplyHitHooks
231
+ hooks?: ApplyHitHooks,
232
+ metadata?: Record<string, any>
204
233
  ): HitResult | undefined {
205
234
  const ai = (target as any).battleAi as BattleAi;
206
235
  if (!ai) return undefined;
@@ -243,6 +272,8 @@ export function applyPlayerHitToEvent(
243
272
  {
244
273
  attacker: player,
245
274
  target,
275
+ metadata,
276
+ reaction: metadata?.reaction,
246
277
  }
247
278
  );
248
279
 
@@ -251,6 +282,7 @@ export function applyPlayerHitToEvent(
251
282
  damage: result.damage,
252
283
  defeated: result.defeated,
253
284
  raw: result.rawDamage,
285
+ reaction: result.reaction,
254
286
  });
255
287
  }
256
288
 
@@ -269,11 +301,13 @@ const toLegacyHitResult = (context: any): HitResult => ({
269
301
  const resolvePlayerAttackHitboxes = (
270
302
  player: RpgPlayer,
271
303
  directionKey: string,
272
- options: ActionBattleOptions
304
+ options: ActionBattleOptions,
305
+ profile: NormalizedActionBattleAttackProfile
273
306
  ): ActionBattleHitbox[] => {
274
307
  const configuredHitboxes = {
275
308
  ...DEFAULT_PLAYER_ATTACK_HITBOXES,
276
309
  ...options.attack?.hitboxes,
310
+ ...profile.hitboxes,
277
311
  };
278
312
  const hitboxConfig =
279
313
  configuredHitboxes[
@@ -296,6 +330,39 @@ const resolvePlayerAttackHitboxes = (
296
330
  );
297
331
  };
298
332
 
333
+ const mergeAttackProfileOverrides = (
334
+ base: NormalizedActionBattleAttackProfile,
335
+ override: ActionBattleAttackProfile
336
+ ): ActionBattleAttackProfile => ({
337
+ ...base,
338
+ ...override,
339
+ reaction: {
340
+ ...base.reaction,
341
+ ...override.reaction,
342
+ },
343
+ hitboxes: {
344
+ ...base.hitboxes,
345
+ ...override.hitboxes,
346
+ },
347
+ });
348
+
349
+ const resolvePlayerAttackProfile = (
350
+ player: RpgPlayer,
351
+ options: ActionBattleOptions
352
+ ): NormalizedActionBattleAttackProfile => {
353
+ const baseProfile = getNormalizedActionBattleAttackProfile(options);
354
+ const weaponProfile = resolveActionBattleWeaponAttackProfile(player);
355
+ if (!weaponProfile) return baseProfile;
356
+ return normalizeActionBattleAttackProfile(
357
+ mergeAttackProfileOverrides(baseProfile, weaponProfile),
358
+ {
359
+ lockMovement: options.attack?.lockMovement,
360
+ lockDurationMs: options.attack?.lockDurationMs,
361
+ hitboxes: options.attack?.hitboxes,
362
+ }
363
+ );
364
+ };
365
+
299
366
  const resolveSignal = (value: any) =>
300
367
  typeof value === "function" ? value() : value;
301
368
 
@@ -423,7 +490,7 @@ const ensureActionBarGui = (
423
490
  const gui = existing || player.gui(ACTION_BATTLE_ACTION_BAR_GUI_ID);
424
491
  if (!(gui as any).__actionBattleReady) {
425
492
  (gui as any).__actionBattleReady = true;
426
- gui.on("useItem", ({ id }) => {
493
+ gui.on("useItem", ({ id }: { id: string }) => {
427
494
  try {
428
495
  player.useItem(id);
429
496
  } catch {
@@ -431,10 +498,13 @@ const ensureActionBarGui = (
431
498
  }
432
499
  gui.update(buildActionBarData(player, options));
433
500
  });
434
- gui.on("useSkill", ({ id, target }) => {
435
- handleActionBattleSkillUse(player, id, target, options);
436
- gui.update(buildActionBarData(player, options));
437
- });
501
+ gui.on(
502
+ "useSkill",
503
+ ({ id, target }: { id: string; target?: { x: number; y: number } }) => {
504
+ handleActionBattleSkillUse(player, id, target, options);
505
+ gui.update(buildActionBarData(player, options));
506
+ }
507
+ );
438
508
  gui.on("refresh", () => {
439
509
  gui.update(buildActionBarData(player, options));
440
510
  });
@@ -523,7 +593,7 @@ const handleActionBattleSkillUse = (
523
593
  const targets: any[] = [];
524
594
  const affects = options.targeting?.affects || "events";
525
595
  if (affects === "events" || affects === "both") {
526
- map.getEvents().forEach((event) => {
596
+ map.getEvents().forEach((event: RpgEvent) => {
527
597
  const tile = getEntityTile(event, tileSize);
528
598
  if (affected.has(`${tile.x},${tile.y}`)) {
529
599
  targets.push(event);
@@ -531,7 +601,7 @@ const handleActionBattleSkillUse = (
531
601
  });
532
602
  }
533
603
  if (affects === "players" || affects === "both") {
534
- map.getPlayers().forEach((other) => {
604
+ map.getPlayers().forEach((other: RpgPlayer) => {
535
605
  if (other.id === player.id) return;
536
606
  const tile = getEntityTile(other, tileSize);
537
607
  if (affected.has(`${tile.x},${tile.y}`)) {
@@ -574,6 +644,7 @@ export const createActionBattleServer = (
574
644
  if (input.action == Control.Action) {
575
645
  const map = player.getCurrentMap();
576
646
  const direction = player.getDirection();
647
+ const attackProfile = resolvePlayerAttackProfile(player, options);
577
648
 
578
649
  // Convert Direction enum to string key
579
650
  const directionKey = direction as string;
@@ -581,42 +652,80 @@ export const createActionBattleServer = (
581
652
  const hitboxes = resolvePlayerAttackHitboxes(
582
653
  player,
583
654
  directionKey,
584
- options
655
+ options,
656
+ attackProfile
585
657
  );
586
658
 
587
659
  if (isActionReservedForNormalEvent(player, map, hitboxes)) {
588
660
  return;
589
661
  }
590
662
 
591
- const lockMovement = options.attack?.lockMovement !== false;
663
+ const lockMovement = attackProfile.movementLock;
664
+ const lockDirection = attackProfile.directionLock;
592
665
  const lockDurationMs =
593
- options.attack?.lockDurationMs ?? DEFAULT_ATTACK_LOCK_DURATION_MS;
594
- let movementLocked = false;
666
+ attackProfile.totalDurationMs ?? DEFAULT_ATTACK_LOCK_DURATION_MS;
667
+ const actionLocked = (lockMovement || lockDirection) && lockDurationMs > 0;
595
668
 
596
669
  if (
597
- lockMovement &&
598
- !beginPlayerAttackLock(player, map, Math.max(0, lockDurationMs))
670
+ actionLocked &&
671
+ !beginPlayerAttackLock(player, map, Math.max(0, lockDurationMs), {
672
+ movement: lockMovement,
673
+ direction: lockDirection,
674
+ })
599
675
  ) {
600
676
  return;
601
677
  }
602
- movementLocked = lockMovement && lockDurationMs > 0;
603
678
 
604
679
  playActionBattleAnimation("attack", player, options.animations);
605
- if (movementLocked) {
680
+ if (actionLocked) {
606
681
  player.animationFixed = true;
607
682
  }
683
+ const attackId = createActionBattleAttackId(
684
+ player.id,
685
+ attackProfile.id
686
+ );
687
+ const hitTracker = new ActionBattleHitTracker(
688
+ attackProfile.hitPolicy
689
+ );
690
+ if (options.debug?.attacks) {
691
+ console.log("[ActionBattle] player attack", {
692
+ attackId,
693
+ playerId: player.id,
694
+ profile: attackProfile.id,
695
+ hitboxes,
696
+ });
697
+ }
608
698
 
609
- map?.createMovingHitbox(hitboxes, { speed: 3 }).subscribe({
610
- next(hits) {
611
- hits.forEach((hit) => {
612
- if (hit instanceof RpgEvent) {
613
- const result = applyPlayerHitToEvent(player, hit);
614
- if (result?.defeated) {
615
- console.log(`Player ${player.id} defeated AI ${hit.id}`);
616
- }
617
- }
699
+ scheduleActionBattleStartup(attackProfile, () => {
700
+ map
701
+ ?.createMovingHitbox(hitboxes, {
702
+ speed: resolveActionBattleHitboxSpeed(
703
+ attackProfile,
704
+ hitboxes.length
705
+ ),
706
+ })
707
+ .subscribe({
708
+ next(hits: any[]) {
709
+ hits.forEach((hit: any) => {
710
+ if (hit instanceof RpgEvent) {
711
+ if (!hitTracker.tryHit(hit)) return;
712
+ const result = applyPlayerHitToEvent(
713
+ player,
714
+ hit,
715
+ undefined,
716
+ {
717
+ attackId,
718
+ attackProfileId: attackProfile.id,
719
+ reaction: attackProfile.reaction,
720
+ }
721
+ );
722
+ if (result?.defeated) {
723
+ console.log(`Player ${player.id} defeated AI ${hit.id}`);
724
+ }
725
+ }
726
+ });
727
+ },
618
728
  });
619
- },
620
729
  });
621
730
  }
622
731
  },
@@ -662,6 +771,44 @@ export const createActionBattleServer = (
662
771
 
663
772
  export default createActionBattleServer();
664
773
 
774
+ export {
775
+ ACTION_BATTLE_HITBOX_FRAME_MS,
776
+ ActionBattleHitTracker,
777
+ createActionBattleAttackId,
778
+ getNormalizedActionBattleAttackProfile,
779
+ resolveActionBattleHitboxSpeed,
780
+ scheduleActionBattleStartup,
781
+ } from "./core/attack-runtime";
782
+ export {
783
+ DEFAULT_ACTION_BATTLE_ATTACK_PROFILE,
784
+ normalizeActionBattleAttackProfile,
785
+ type ActionBattleAttackProfileFallbacks,
786
+ } from "./core/attack-profile";
787
+ export type {
788
+ ActionBattleAttackDirection,
789
+ ActionBattleAttackHitboxConfig,
790
+ ActionBattleAttackHitboxMap,
791
+ ActionBattleAttackHitPolicy,
792
+ ActionBattleAttackProfile,
793
+ ActionBattleDebugOptions,
794
+ ActionBattleHitReactionProfile,
795
+ NormalizedActionBattleHitReactionProfile,
796
+ NormalizedActionBattleAttackProfile,
797
+ } from "./types";
798
+ export {
799
+ DEFAULT_ACTION_BATTLE_HIT_REACTION,
800
+ isActionBattleEntityInvincible,
801
+ normalizeActionBattleHitReaction,
802
+ setActionBattleInvincibility,
803
+ } from "./core/hit-reaction";
804
+ export {
805
+ DEFAULT_ACTION_BATTLE_ENEMY_ATTACK_PROFILES,
806
+ normalizeActionBattleEnemyAttackProfiles,
807
+ type ActionBattleEnemyAttackProfileKey,
808
+ type ActionBattleEnemyAttackProfileMap,
809
+ type NormalizedActionBattleEnemyAttackProfileMap,
810
+ } from "./core/enemy-attack-profiles";
811
+ export { resolveActionBattleWeaponAttackProfile } from "./core/equipment";
665
812
  export {
666
813
  AiDebug,
667
814
  AiState,
package/src/types.ts CHANGED
@@ -89,6 +89,61 @@ export interface ActionBattleUiOptions {
89
89
  targeting?: ActionBattleUiTargetingOptions;
90
90
  }
91
91
 
92
+ export type ActionBattleAttackDirection =
93
+ | "up"
94
+ | "down"
95
+ | "left"
96
+ | "right"
97
+ | "default";
98
+
99
+ export interface ActionBattleAttackHitboxConfig {
100
+ offsetX: number;
101
+ offsetY: number;
102
+ width: number;
103
+ height: number;
104
+ }
105
+
106
+ export type ActionBattleAttackHitboxMap = Partial<
107
+ Record<ActionBattleAttackDirection, ActionBattleAttackHitboxConfig>
108
+ >;
109
+
110
+ export type ActionBattleAttackHitPolicy =
111
+ | "oncePerTarget"
112
+ | "allowRepeatHits";
113
+
114
+ export interface ActionBattleHitReactionProfile {
115
+ invincibilityMs?: number;
116
+ hitstunMs?: number;
117
+ staggerPower?: number;
118
+ }
119
+
120
+ export interface NormalizedActionBattleHitReactionProfile {
121
+ invincibilityMs: number;
122
+ hitstunMs: number;
123
+ staggerPower: number;
124
+ }
125
+
126
+ export interface ActionBattleAttackProfile {
127
+ id?: string;
128
+ startupMs?: number;
129
+ activeMs?: number;
130
+ recoveryMs?: number;
131
+ cooldownMs?: number;
132
+ movementLock?: boolean;
133
+ directionLock?: boolean;
134
+ animationKey?: ActionBattleAnimationKey;
135
+ hitPolicy?: ActionBattleAttackHitPolicy;
136
+ reaction?: ActionBattleHitReactionProfile;
137
+ hitboxes?: ActionBattleAttackHitboxMap;
138
+ }
139
+
140
+ export interface NormalizedActionBattleAttackProfile
141
+ extends Required<Omit<ActionBattleAttackProfile, "hitboxes" | "reaction">> {
142
+ reaction: NormalizedActionBattleHitReactionProfile;
143
+ hitboxes?: ActionBattleAttackHitboxMap;
144
+ totalDurationMs: number;
145
+ }
146
+
92
147
  export interface ActionBattleSkillOptions {
93
148
  getTargeting?: ActionBattleSkillTargetingResolver;
94
149
  defaultAoeMask?: ActionBattleAoeMask;
@@ -99,19 +154,19 @@ export interface ActionBattleTargetingOptions {
99
154
  allowEmptyTarget?: boolean;
100
155
  }
101
156
 
157
+ export interface ActionBattleDebugOptions {
158
+ attacks?: boolean;
159
+ }
160
+
102
161
  export interface ActionBattleAttackOptions {
162
+ profile?: ActionBattleAttackProfile;
103
163
  lockMovement?: boolean;
104
164
  lockDurationMs?: number;
105
165
  showPreview?: boolean;
106
166
  previewDurationMs?: number;
107
167
  previewColor?: number;
108
168
  previewAccentColor?: number;
109
- hitboxes?: Partial<
110
- Record<
111
- "up" | "down" | "left" | "right" | "default",
112
- { offsetX: number; offsetY: number; width: number; height: number }
113
- >
114
- >;
169
+ hitboxes?: ActionBattleAttackHitboxMap;
115
170
  resolveHitboxes?: (context: {
116
171
  player: any;
117
172
  direction: string;
@@ -139,6 +194,7 @@ export interface ActionBattleOptions {
139
194
  skills?: ActionBattleSkillOptions;
140
195
  targeting?: ActionBattleTargetingOptions;
141
196
  attack?: ActionBattleAttackOptions;
197
+ debug?: ActionBattleDebugOptions;
142
198
  animations?: ActionBattleAnimationOptions;
143
199
  systems?: ActionBattleSystemOptions;
144
200
  }