@rpgjs/action-battle 5.0.0-beta.10 → 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 (111) hide show
  1. package/CHANGELOG.md +22 -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 +10 -10
  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/components/action-bar.ce +2 -2
  86. package/src/config.ts +84 -37
  87. package/src/core/action-use.spec.ts +317 -0
  88. package/src/core/action-use.ts +386 -0
  89. package/src/core/ai-behavior-tree.spec.ts +116 -0
  90. package/src/core/ai-behavior-tree.ts +272 -0
  91. package/src/core/attack-profile.spec.ts +46 -0
  92. package/src/core/attack-runtime.spec.ts +35 -0
  93. package/src/core/attack-runtime.ts +32 -0
  94. package/src/core/context.ts +9 -0
  95. package/src/core/contracts.ts +146 -1
  96. package/src/core/defaults.ts +56 -0
  97. package/src/core/equipment.ts +9 -5
  98. package/src/core/targets.spec.ts +112 -0
  99. package/src/core/targets.ts +147 -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
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { playActionBattleAnimation } from "./animations";
3
+
4
+ describe("action battle animations", () => {
5
+ test("uses setGraphicAnimation when the entity exposes the server animation API", () => {
6
+ const entity = {
7
+ setGraphicAnimation: vi.fn(),
8
+ setAnimation: vi.fn(),
9
+ };
10
+
11
+ playActionBattleAnimation("attack", entity);
12
+
13
+ expect(entity.setGraphicAnimation).toHaveBeenCalledWith("attack", 1);
14
+ expect(entity.setAnimation).not.toHaveBeenCalled();
15
+ });
16
+
17
+ test("falls back to setAnimation for client sprite objects", () => {
18
+ const entity = {
19
+ setAnimation: vi.fn(),
20
+ };
21
+
22
+ playActionBattleAnimation("attack", entity, {
23
+ attack: {
24
+ animationName: "slash",
25
+ graphic: "hero-slash",
26
+ repeat: 2,
27
+ },
28
+ });
29
+
30
+ expect(entity.setAnimation).toHaveBeenCalledWith(
31
+ "slash",
32
+ "hero-slash",
33
+ 2
34
+ );
35
+ });
36
+
37
+ test("does not throw when the entity has no animation API", () => {
38
+ expect(() => playActionBattleAnimation("attack", {})).not.toThrow();
39
+ });
40
+ });
package/src/animations.ts CHANGED
@@ -20,6 +20,36 @@ export interface ActionBattleAnimationDefaults {
20
20
  repeat?: number;
21
21
  }
22
22
 
23
+ const playResolvedAnimation = (
24
+ entity: ActionBattleAnimationEntity,
25
+ animation: ResolvedActionBattleAnimation
26
+ ) => {
27
+ if (typeof entity.setGraphicAnimation === "function") {
28
+ if (animation.graphic !== undefined) {
29
+ entity.setGraphicAnimation(
30
+ animation.animationName,
31
+ animation.graphic,
32
+ animation.repeat
33
+ );
34
+ } else {
35
+ entity.setGraphicAnimation(animation.animationName, animation.repeat);
36
+ }
37
+ return;
38
+ }
39
+
40
+ if (typeof entity.setAnimation === "function") {
41
+ if (animation.graphic !== undefined) {
42
+ entity.setAnimation(
43
+ animation.animationName,
44
+ animation.graphic,
45
+ animation.repeat
46
+ );
47
+ } else {
48
+ entity.setAnimation(animation.animationName, animation.repeat);
49
+ }
50
+ }
51
+ };
52
+
23
53
  const DEFAULT_ANIMATION_BY_KEY: Record<ActionBattleAnimationKey, string> = {
24
54
  attack: "attack",
25
55
  hurt: "hurt",
@@ -127,15 +157,7 @@ export function playActionBattleAnimation(
127
157
  );
128
158
  if (!animation) return null;
129
159
 
130
- if (animation.graphic !== undefined) {
131
- entity.setGraphicAnimation(
132
- animation.animationName,
133
- animation.graphic,
134
- animation.repeat
135
- );
136
- } else {
137
- entity.setGraphicAnimation(animation.animationName, animation.repeat);
138
- }
160
+ playResolvedAnimation(entity, animation);
139
161
 
140
162
  return animation;
141
163
  }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import {
3
+ applyActionBattleAttackDirection,
4
+ resolveActionBattleAttackDirection,
5
+ } from "./attack-input";
6
+
7
+ describe("action battle attack input", () => {
8
+ test("prefers the direction carried by the action payload", () => {
9
+ const entity = {
10
+ getDirection: () => "left",
11
+ };
12
+
13
+ expect(
14
+ resolveActionBattleAttackDirection(entity, {
15
+ data: { direction: "right" },
16
+ })
17
+ ).toBe("right");
18
+ });
19
+
20
+ test("falls back to the entity direction when the payload is missing", () => {
21
+ const entity = {
22
+ getDirection: () => "up",
23
+ };
24
+
25
+ expect(resolveActionBattleAttackDirection(entity)).toBe("up");
26
+ });
27
+
28
+ test("applies the captured attack direction before direction lock", () => {
29
+ const entity = {
30
+ changeDirection: vi.fn(),
31
+ };
32
+
33
+ applyActionBattleAttackDirection(entity, "down");
34
+
35
+ expect(entity.changeDirection).toHaveBeenCalledWith("down");
36
+ });
37
+
38
+ test("temporarily unlocks direction so attack intent can face while knocked back", () => {
39
+ const entity = {
40
+ directionFixed: true,
41
+ changeDirection: vi.fn(function (this: { directionFixed: boolean }) {
42
+ expect(this.directionFixed).toBe(false);
43
+ }),
44
+ };
45
+
46
+ applyActionBattleAttackDirection(entity, "right");
47
+
48
+ expect(entity.changeDirection).toHaveBeenCalledWith("right");
49
+ expect(entity.directionFixed).toBe(true);
50
+ });
51
+ });
@@ -0,0 +1,59 @@
1
+ export type ActionBattleResolvedDirection = "up" | "down" | "left" | "right";
2
+
3
+ const ACTION_BATTLE_DIRECTIONS = new Set([
4
+ "up",
5
+ "down",
6
+ "left",
7
+ "right",
8
+ ]);
9
+
10
+ const normalizeDirection = (
11
+ value: unknown
12
+ ): ActionBattleResolvedDirection | undefined => {
13
+ if (typeof value !== "string") return undefined;
14
+ return ACTION_BATTLE_DIRECTIONS.has(value)
15
+ ? (value as ActionBattleResolvedDirection)
16
+ : undefined;
17
+ };
18
+
19
+ export const resolveActionBattleAttackDirection = (
20
+ entity: any,
21
+ input?: any
22
+ ): ActionBattleResolvedDirection => {
23
+ const payloadDirection =
24
+ normalizeDirection(input?.data?.direction) ??
25
+ normalizeDirection(input?.data?.attackDirection) ??
26
+ normalizeDirection(input?.direction);
27
+ if (payloadDirection) return payloadDirection;
28
+
29
+ if (typeof entity?.getDirection === "function") {
30
+ const direction = normalizeDirection(entity.getDirection());
31
+ if (direction) return direction;
32
+ }
33
+
34
+ if (typeof entity?.direction === "function") {
35
+ const direction = normalizeDirection(entity.direction());
36
+ if (direction) return direction;
37
+ }
38
+
39
+ return normalizeDirection(entity?.direction) ?? "down";
40
+ };
41
+
42
+ export const applyActionBattleAttackDirection = (
43
+ entity: any,
44
+ direction: ActionBattleResolvedDirection
45
+ ) => {
46
+ if (typeof entity?.changeDirection === "function") {
47
+ const previousDirectionFixed = entity.directionFixed;
48
+ if (previousDirectionFixed === true) {
49
+ entity.directionFixed = false;
50
+ }
51
+ try {
52
+ entity.changeDirection(direction);
53
+ } finally {
54
+ if (previousDirectionFixed === true) {
55
+ entity.directionFixed = previousDirectionFixed;
56
+ }
57
+ }
58
+ }
59
+ };
package/src/client.ts CHANGED
@@ -1,11 +1,5 @@
1
- import { inject, PrebuiltComponentAnimations, RpgClient, RpgClientEngine, RpgGui } from "@rpgjs/client";
1
+ import { PrebuiltComponentAnimations, RpgClient, RpgClientEngine, RpgGui, inject } from "@rpgjs/client";
2
2
  import { defineModule } from "@rpgjs/common";
3
- // @ts-ignore CanvasEngine components are compiled by @canvasengine/compiler.
4
- import ActionBarComponent from "./components/action-bar.ce";
5
- // @ts-ignore CanvasEngine components are compiled by @canvasengine/compiler.
6
- import TargetingOverlayComponent from "./components/targeting-overlay.ce";
7
- // @ts-ignore CanvasEngine components are compiled by @canvasengine/compiler.
8
- import AttackPreviewComponent from "./components/attack-preview.ce";
9
3
  import {
10
4
  setActionBattleOptions,
11
5
  startAttackPreview,
@@ -13,8 +7,22 @@ import {
13
7
  } from "./ui/state";
14
8
  import { ActionBattleOptions } from "./types";
15
9
  import { normalizeActionBattleOptions } from "./config";
16
- import { resolveActionBattleAnimation } from "./animations";
17
10
  import { getNormalizedActionBattleAttackProfile } from "./core/attack-runtime";
11
+ import {
12
+ applyActionBattleAttackDirection,
13
+ resolveActionBattleAttackDirection,
14
+ } from "./attack-input";
15
+ import {
16
+ forceActionBattleLocomotionAnimation,
17
+ withActionBattleAnimationUnlocked,
18
+ } from "./locomotion";
19
+ import { resolveActionBattleUi } from "./ui";
20
+ import {
21
+ ACTION_BATTLE_HIT_FX_COMPONENT_ID,
22
+ createActionBattleClientVisuals,
23
+ playActionBattleVisual,
24
+ setActionBattlePreviewStarter,
25
+ } from "./visual";
18
26
 
19
27
  const DEFAULT_ATTACK_LOCK_DURATION_MS = 350;
20
28
 
@@ -64,6 +72,9 @@ const beginLocalPlayerAttackLock = (
64
72
  player.canMove = previousCanMove;
65
73
  player.directionFixed = previousDirectionFixed;
66
74
  player.animationFixed = previousAnimationFixed;
75
+ if (locks.movement && !previousAnimationFixed) {
76
+ forceActionBattleLocomotionAnimation(player, "stand");
77
+ }
67
78
  }, durationMs);
68
79
 
69
80
  return true;
@@ -79,35 +90,21 @@ const playLocalPlayerAttackAnimation = (
79
90
  player: any,
80
91
  options: ActionBattleOptions
81
92
  ) => {
82
- if (!player || typeof player.setAnimation !== "function") return;
83
- const animation = resolveActionBattleAnimation(
84
- "attack",
85
- player,
86
- options.animations
87
- );
88
- if (!animation) return;
89
-
90
- if (animation.graphic !== undefined) {
91
- player.setAnimation(
92
- animation.animationName,
93
- animation.graphic,
94
- animation.repeat
95
- );
96
- return;
97
- }
98
- player.setAnimation(animation.animationName, animation.repeat);
93
+ withActionBattleAnimationUnlocked(player, () => {
94
+ playActionBattleVisual(options.visual, {
95
+ moment: "attack",
96
+ entity: player,
97
+ });
98
+ });
99
99
  };
100
100
 
101
101
  const showLocalAttackPreview = (player: any, options: ActionBattleOptions) => {
102
- if (!player || options.attack?.showPreview === false) return;
103
- const durationMs = Math.max(1, options.attack?.previewDurationMs ?? 180);
104
- const previewId = startAttackPreview({
105
- direction: resolveLocalPlayerDirection(player),
106
- durationMs,
107
- color: options.attack?.previewColor,
108
- accentColor: options.attack?.previewAccentColor,
102
+ const attackPreview = options.ui?.attackPreview as any;
103
+ if (!player || attackPreview?.enabled === false) return;
104
+ playActionBattleVisual(options.visual, {
105
+ moment: "preview",
106
+ entity: player,
109
107
  });
110
- setTimeout(() => stopAttackPreview(previewId), durationMs);
111
108
  };
112
109
 
113
110
  export const createActionBattleClient = (
@@ -115,60 +112,76 @@ export const createActionBattleClient = (
115
112
  ) => {
116
113
  const normalized = normalizeActionBattleOptions(options);
117
114
  setActionBattleOptions(normalized);
118
- const actionBarEnabled = normalized.ui?.actionBar?.enabled;
119
- const targetingEnabled = normalized.ui?.targeting?.enabled;
120
- const componentsInFront = [
121
- ...(targetingEnabled ? [TargetingOverlayComponent] : []),
122
- AttackPreviewComponent,
123
- ];
115
+ setActionBattlePreviewStarter((entity, previewOptions = {}) => {
116
+ const direction = previewOptions.direction ?? resolveLocalPlayerDirection(entity);
117
+ const durationMs = Math.max(
118
+ 1,
119
+ previewOptions.durationMs ?? normalized.attack?.previewDurationMs ?? 180
120
+ );
121
+ const previewId = startAttackPreview({
122
+ direction,
123
+ durationMs,
124
+ color: previewOptions.color ?? normalized.attack?.previewColor,
125
+ accentColor:
126
+ previewOptions.accentColor ?? normalized.attack?.previewAccentColor,
127
+ });
128
+ setTimeout(() => stopAttackPreview(previewId), durationMs);
129
+ });
130
+ const resolvedUi = resolveActionBattleUi(normalized.ui);
131
+ const actionBarEnabled = resolvedUi.actionBar.enabled;
124
132
  const hitComponent = PrebuiltComponentAnimations?.Hit;
133
+ const fxComponent = PrebuiltComponentAnimations?.Fx;
125
134
  return defineModule<RpgClient>({
126
- componentAnimations: hitComponent
127
- ? [
128
- {
129
- id: "hit",
130
- component: hitComponent,
131
- },
132
- ]
133
- : [],
134
- gui: actionBarEnabled
135
- ? [
136
- {
137
- id: "action-battle-action-bar",
138
- component: ActionBarComponent,
139
- dependencies: () => {
140
- const engine = inject(RpgClientEngine)
141
- return [engine.scene.currentPlayer]
135
+ componentAnimations: [
136
+ ...(hitComponent
137
+ ? [
138
+ {
139
+ id: "hit",
140
+ component: hitComponent,
141
+ },
142
+ ]
143
+ : []),
144
+ ...(fxComponent
145
+ ? [
146
+ {
147
+ id: ACTION_BATTLE_HIT_FX_COMPONENT_ID,
148
+ component: fxComponent,
142
149
  },
143
- },
144
- ]
145
- : [],
150
+ ]
151
+ : []),
152
+ ],
153
+ clientVisuals: createActionBattleClientVisuals(normalized),
154
+ gui: resolvedUi.gui,
146
155
  sprite: {
147
- componentsInFront,
156
+ componentsBehind: resolvedUi.sprite.componentsBehind,
157
+ componentsInFront: resolvedUi.sprite.componentsInFront,
148
158
  },
149
159
  sceneMap: {
150
160
  onAfterLoading() {
151
- if (actionBarEnabled && normalized.ui?.actionBar?.autoOpen) {
161
+ if (actionBarEnabled && resolvedUi.actionBar.autoOpen) {
152
162
  const gui = inject(RpgGui)
153
163
  gui.display('action-battle-action-bar')
154
164
  }
155
165
  }
156
166
  },
157
167
  engine: {
158
- onInput(engine: RpgClientEngine, { input }: { input: string }) {
168
+ onInput(engine: RpgClientEngine, { input, data }: any) {
159
169
  if (input !== "action") return;
160
170
  const player = engine.scene?.getCurrentPlayer?.() as any;
161
171
  if (!player) return;
172
+ const direction = resolveActionBattleAttackDirection(player, { data });
173
+ applyActionBattleAttackDirection(player, direction);
162
174
  const attackProfile = getNormalizedActionBattleAttackProfile(normalized);
163
175
  const lockDurationMs = Math.max(
164
176
  0,
165
177
  attackProfile.totalDurationMs ?? DEFAULT_ATTACK_LOCK_DURATION_MS
166
178
  );
167
179
  if (attackProfile.movementLock || attackProfile.directionLock) {
168
- beginLocalPlayerAttackLock(engine, lockDurationMs, {
180
+ const locked = beginLocalPlayerAttackLock(engine, lockDurationMs, {
169
181
  movement: attackProfile.movementLock,
170
182
  direction: attackProfile.directionLock,
171
183
  });
184
+ if (!locked) return;
172
185
  }
173
186
  playLocalPlayerAttackAnimation(player, normalized);
174
187
  showLocalAttackPreview(player, normalized);
@@ -60,7 +60,7 @@
60
60
  <script>
61
61
  import { signal, computed, effect } from "canvasengine";
62
62
  import { inject } from "@rpgjs/client";
63
- import { RpgClientEngine } from "@rpgjs/client";
63
+ import { getKeyboardControlBind, RpgClientEngine } from "@rpgjs/client";
64
64
  import {
65
65
  actionBattleTargetingState,
66
66
  actionBattleUiOptions,
@@ -299,7 +299,7 @@
299
299
  }
300
300
  },
301
301
  action: {
302
- bind: keyboardControls.action,
302
+ bind: getKeyboardControlBind(keyboardControls.action),
303
303
  keyDown() {
304
304
  if (isTargeting()) {
305
305
  confirmTargeting();
package/src/config.ts CHANGED
@@ -42,68 +42,115 @@ let currentActionBattleOptions: ActionBattleOptions =
42
42
  export function normalizeActionBattleOptions(
43
43
  options: ActionBattleOptions = {}
44
44
  ): ActionBattleOptions {
45
+ const combat = {
46
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.combat,
47
+ ...options.systems?.combat,
48
+ ...options.combat,
49
+ hooks: {
50
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.combat?.hooks,
51
+ ...options.systems?.combat?.hooks,
52
+ ...options.combat?.hooks,
53
+ },
54
+ };
45
55
  const attack = {
46
56
  ...DEFAULT_ACTION_BATTLE_OPTIONS.attack,
47
57
  ...options.attack,
58
+ ...combat.attack,
48
59
  };
49
60
  const attackProfile = normalizeActionBattleAttackProfile(attack.profile, {
50
61
  lockMovement: attack.lockMovement,
51
62
  lockDurationMs: attack.lockDurationMs,
52
63
  hitboxes: attack.hitboxes,
53
64
  });
65
+ const normalizedAttack = {
66
+ ...attack,
67
+ profile: attackProfile,
68
+ };
69
+ const skills = {
70
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.skills,
71
+ ...options.skills,
72
+ };
73
+ skills.targeting = skills.targeting ?? skills.getTargeting;
74
+ skills.getTargeting = skills.getTargeting ?? skills.targeting;
75
+
76
+ const defaultActionBar = DEFAULT_ACTION_BATTLE_OPTIONS.ui?.actionBar as any;
77
+ const defaultTargeting = DEFAULT_ACTION_BATTLE_OPTIONS.ui?.targeting as any;
78
+ const optionActionBar = options.ui?.actionBar as any;
79
+ const optionTargeting = options.ui?.targeting as any;
80
+ const optionAttackPreview = options.ui?.attackPreview as any;
81
+ const actionBar =
82
+ options.ui?.actionBar === false
83
+ ? { ...defaultActionBar, enabled: false }
84
+ : {
85
+ ...defaultActionBar,
86
+ ...(options.ui?.actionBar === true ? { enabled: true } : optionActionBar),
87
+ };
88
+ const legacyPreviewEnabled = normalizedAttack.showPreview !== false;
89
+ const attackPreview =
90
+ options.ui?.attackPreview === false
91
+ ? { enabled: false }
92
+ : {
93
+ enabled: options.ui?.attackPreview === true ? true : legacyPreviewEnabled,
94
+ ...(options.ui?.attackPreview === true ? {} : optionAttackPreview),
95
+ };
96
+ const targeting =
97
+ options.ui?.targeting === false
98
+ ? { ...defaultTargeting, enabled: false }
99
+ : {
100
+ ...defaultTargeting,
101
+ ...(options.ui?.targeting === true ? { enabled: true } : optionTargeting),
102
+ colors: {
103
+ ...defaultTargeting?.colors,
104
+ ...(typeof options.ui?.targeting === "object"
105
+ ? optionTargeting?.colors
106
+ : undefined),
107
+ },
108
+ };
109
+ const ai = {
110
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.ai,
111
+ ...options.systems?.ai,
112
+ ...options.ai,
113
+ behaviors: {
114
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.ai?.behaviors,
115
+ ...options.systems?.ai?.behaviors,
116
+ ...options.ai?.behaviors,
117
+ },
118
+ presets: {
119
+ ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.ai?.presets,
120
+ ...options.systems?.ai?.presets,
121
+ ...options.ai?.presets,
122
+ },
123
+ };
54
124
 
55
125
  return {
56
126
  ui: {
57
- actionBar: {
58
- ...DEFAULT_ACTION_BATTLE_OPTIONS.ui?.actionBar,
59
- ...options.ui?.actionBar,
60
- },
61
- targeting: {
62
- ...DEFAULT_ACTION_BATTLE_OPTIONS.ui?.targeting,
63
- ...options.ui?.targeting,
64
- colors: {
65
- ...DEFAULT_ACTION_BATTLE_OPTIONS.ui?.targeting?.colors,
66
- ...options.ui?.targeting?.colors,
67
- },
68
- },
69
- },
70
- skills: {
71
- ...DEFAULT_ACTION_BATTLE_OPTIONS.skills,
72
- ...options.skills,
127
+ ...options.ui,
128
+ actionBar,
129
+ targeting,
130
+ attackPreview,
73
131
  },
132
+ skills,
74
133
  targeting: {
75
134
  ...DEFAULT_ACTION_BATTLE_OPTIONS.targeting,
76
135
  ...options.targeting,
77
136
  },
78
- debug: {
79
- ...DEFAULT_ACTION_BATTLE_OPTIONS.debug,
80
- ...options.debug,
81
- },
82
- attack: {
83
- ...attack,
84
- profile: attackProfile,
137
+ attack: normalizedAttack,
138
+ combat: {
139
+ ...combat,
140
+ attack: normalizedAttack,
85
141
  },
142
+ ai,
143
+ visual: options.visual,
86
144
  animations: {
87
145
  ...DEFAULT_ACTION_BATTLE_OPTIONS.animations,
88
146
  ...options.animations,
89
147
  },
90
148
  systems: {
91
149
  combat: {
92
- ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.combat,
93
- ...options.systems?.combat,
94
- hooks: {
95
- ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.combat?.hooks,
96
- ...options.systems?.combat?.hooks,
97
- },
98
- },
99
- ai: {
100
- ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.ai,
101
- ...options.systems?.ai,
102
- behaviors: {
103
- ...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.ai?.behaviors,
104
- ...options.systems?.ai?.behaviors,
105
- },
150
+ ...combat,
151
+ attack: normalizedAttack,
106
152
  },
153
+ ai,
107
154
  },
108
155
  };
109
156
  }