@rpgjs/action-battle 5.0.0-beta.3 → 5.0.0-beta.5
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.
- package/README.md +137 -0
- package/dist/ai.server.d.ts +8 -1
- package/dist/client/index.js +8 -4
- package/dist/client/index10.js +97 -330
- package/dist/client/index11.js +25 -0
- package/dist/client/index12.js +1222 -0
- package/dist/client/index13.js +46 -0
- package/dist/client/index14.js +10 -0
- package/dist/client/index15.js +448 -0
- package/dist/client/index2.js +30 -0
- package/dist/client/index3.js +33 -1
- package/dist/client/index4.js +7 -3
- package/dist/client/index7.js +76 -32
- package/dist/client/index8.js +24 -4
- package/dist/client/index9.js +94 -1165
- package/dist/core/context.d.ts +5 -0
- package/dist/core/defaults.d.ts +81 -0
- package/dist/core/hit.d.ts +2 -0
- package/dist/enemies/factory.d.ts +7 -0
- package/dist/index.d.ts +6 -1
- package/dist/server/index.js +7 -3
- package/dist/server/index10.js +10 -0
- package/dist/server/index2.js +23 -3
- package/dist/server/index3.js +30 -0
- package/dist/server/index4.js +137 -1163
- package/dist/server/index5.js +22 -34
- package/dist/server/index6.js +1190 -345
- package/dist/server/index7.js +37 -0
- package/dist/server/index8.js +46 -0
- package/dist/server/index9.js +447 -0
- package/dist/server.d.ts +2 -0
- package/dist/ui/state.d.ts +17 -0
- package/package.json +5 -5
- package/src/ai.server.ts +91 -24
- package/src/animations.ts +43 -4
- package/src/canvas-engine-shim.ts +4 -0
- package/src/client.ts +122 -2
- package/src/components/action-bar.ce +5 -3
- package/src/components/attack-preview.ce +90 -0
- package/src/config.ts +30 -0
- package/src/core/context.ts +35 -0
- package/src/core/contracts.ts +123 -0
- package/src/core/defaults.ts +162 -0
- package/src/core/hit.spec.ts +58 -0
- package/src/core/hit.ts +66 -0
- package/src/enemies/factory.ts +25 -0
- package/src/index.ts +40 -0
- package/src/server.ts +235 -71
- package/src/targeting.spec.ts +24 -0
- package/src/types/canvas-engine.d.ts +4 -0
- package/src/types.ts +46 -1
- package/src/ui/state.ts +57 -0
package/src/server.ts
CHANGED
|
@@ -9,8 +9,13 @@ import {
|
|
|
9
9
|
import { normalizeActionBattleOptions, setActionBattleOptions } from "./config";
|
|
10
10
|
import { manhattanDistance, parseAoeMask } from "./targeting";
|
|
11
11
|
import { playActionBattleAnimation } from "./animations";
|
|
12
|
+
import { getActionBattleSystems, setActionBattleSystems } from "./core/context";
|
|
13
|
+
import { applyActionBattleHit } from "./core/hit";
|
|
14
|
+
import { DEFAULT_ZELDA_PLAYER_HITBOXES } from "./core/defaults";
|
|
15
|
+
import type { ActionBattleHitbox } from "./core/contracts";
|
|
12
16
|
|
|
13
17
|
export const ACTION_BATTLE_ACTION_BAR_GUI_ID = "action-battle-action-bar";
|
|
18
|
+
const DEFAULT_ATTACK_LOCK_DURATION_MS = 350;
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* Default player attack hitboxes offsets for each direction
|
|
@@ -20,11 +25,113 @@ export const ACTION_BATTLE_ACTION_BAR_GUI_ID = "action-battle-action-bar";
|
|
|
20
25
|
* when creating the moving hitbox.
|
|
21
26
|
*/
|
|
22
27
|
export const DEFAULT_PLAYER_ATTACK_HITBOXES = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
...DEFAULT_ZELDA_PLAYER_HITBOXES,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const beginPlayerAttackLock = (
|
|
32
|
+
player: RpgPlayer,
|
|
33
|
+
map: ReturnType<RpgPlayer["getCurrentMap"]> | undefined,
|
|
34
|
+
durationMs: number
|
|
35
|
+
): boolean => {
|
|
36
|
+
if (durationMs <= 0) return true;
|
|
37
|
+
|
|
38
|
+
const runtimePlayer = player as any;
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
if (
|
|
41
|
+
typeof runtimePlayer.__actionBattleAttackLockedUntil === "number" &&
|
|
42
|
+
runtimePlayer.__actionBattleAttackLockedUntil > now
|
|
43
|
+
) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lockId = (runtimePlayer.__actionBattleAttackLockId ?? 0) + 1;
|
|
48
|
+
runtimePlayer.__actionBattleAttackLockId = lockId;
|
|
49
|
+
runtimePlayer.__actionBattleAttackLockedUntil = now + durationMs;
|
|
50
|
+
|
|
51
|
+
const previousCanMove =
|
|
52
|
+
typeof player.canMove === "function" ? player.canMove() : true;
|
|
53
|
+
const previousDirectionFixed = player.directionFixed;
|
|
54
|
+
const previousAnimationFixed = player.animationFixed;
|
|
55
|
+
|
|
56
|
+
player.pendingInputs = [];
|
|
57
|
+
player.lastProcessedInputTs = 0;
|
|
58
|
+
(map as any)?.stopMovement?.(player);
|
|
59
|
+
player.canMove.set(false);
|
|
60
|
+
player.directionFixed = true;
|
|
61
|
+
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
if (runtimePlayer.__actionBattleAttackLockId !== lockId) return;
|
|
64
|
+
runtimePlayer.__actionBattleAttackLockedUntil = 0;
|
|
65
|
+
player.canMove.set(previousCanMove);
|
|
66
|
+
player.directionFixed = previousDirectionFixed;
|
|
67
|
+
player.animationFixed = previousAnimationFixed;
|
|
68
|
+
}, durationMs);
|
|
69
|
+
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const isBattleEvent = (event: RpgEvent) => !!(event as any).battleAi;
|
|
74
|
+
|
|
75
|
+
const rectsOverlap = (
|
|
76
|
+
a: { x: number; y: number; width: number; height: number },
|
|
77
|
+
b: { x: number; y: number; width: number; height: number }
|
|
78
|
+
) =>
|
|
79
|
+
a.x < b.x + b.width &&
|
|
80
|
+
a.x + a.width > b.x &&
|
|
81
|
+
a.y < b.y + b.height &&
|
|
82
|
+
a.y + a.height > b.y;
|
|
83
|
+
|
|
84
|
+
const eventRect = (event: RpgEvent) => {
|
|
85
|
+
const hitbox =
|
|
86
|
+
typeof event.hitbox === "function" ? event.hitbox() : (event as any).hitbox;
|
|
87
|
+
return {
|
|
88
|
+
x: event.x(),
|
|
89
|
+
y: event.y(),
|
|
90
|
+
width: hitbox?.w ?? 32,
|
|
91
|
+
height: hitbox?.h ?? 32,
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const getVisibleActionEvents = (
|
|
96
|
+
player: RpgPlayer,
|
|
97
|
+
map: ReturnType<RpgPlayer["getCurrentMap"]> | undefined,
|
|
98
|
+
hitboxes: Array<{ x: number; y: number; width: number; height: number }>
|
|
99
|
+
) => {
|
|
100
|
+
if (!map) return [];
|
|
101
|
+
|
|
102
|
+
const eventsById = new Map<string, RpgEvent>();
|
|
103
|
+
const addEvent = (event: RpgEvent | undefined) => {
|
|
104
|
+
if (!event) return;
|
|
105
|
+
const isVisible =
|
|
106
|
+
typeof (map as any).isEventVisibleForPlayer === "function"
|
|
107
|
+
? (map as any).isEventVisibleForPlayer(event, player)
|
|
108
|
+
: true;
|
|
109
|
+
if (!isVisible) return;
|
|
110
|
+
eventsById.set(event.id, event);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const collisions = (map as any).getCollisions?.(player.id);
|
|
114
|
+
if (Array.isArray(collisions)) {
|
|
115
|
+
collisions.forEach((id: string) => addEvent(map.getEvent(id)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const event of map.getEvents()) {
|
|
119
|
+
const rect = eventRect(event);
|
|
120
|
+
if (hitboxes.some((hitbox) => rectsOverlap(hitbox, rect))) {
|
|
121
|
+
addEvent(event);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Array.from(eventsById.values());
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const isActionReservedForNormalEvent = (
|
|
129
|
+
player: RpgPlayer,
|
|
130
|
+
map: ReturnType<RpgPlayer["getCurrentMap"]> | undefined,
|
|
131
|
+
hitboxes: Array<{ x: number; y: number; width: number; height: number }>
|
|
132
|
+
) => {
|
|
133
|
+
const events = getVisibleActionEvents(player, map, hitboxes);
|
|
134
|
+
return events.length > 0 && !events.some(isBattleEvent);
|
|
28
135
|
};
|
|
29
136
|
|
|
30
137
|
/**
|
|
@@ -98,52 +205,97 @@ export function applyPlayerHitToEvent(
|
|
|
98
205
|
const ai = (target as any).battleAi as BattleAi;
|
|
99
206
|
if (!ai) return undefined;
|
|
100
207
|
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
208
|
+
const systems = getActionBattleSystems();
|
|
209
|
+
const result = applyActionBattleHit(
|
|
210
|
+
{
|
|
211
|
+
...systems.combat,
|
|
212
|
+
hooks: hooks
|
|
213
|
+
? {
|
|
214
|
+
...systems.combat.hooks,
|
|
215
|
+
beforeHit(context) {
|
|
216
|
+
const before = systems.combat.hooks?.beforeHit?.(context);
|
|
217
|
+
if (before === false) return false;
|
|
218
|
+
const nextContext = before || context;
|
|
219
|
+
const legacyResult = toLegacyHitResult(nextContext);
|
|
220
|
+
const modified = hooks.onBeforeHit?.(legacyResult);
|
|
221
|
+
if (!modified) return nextContext;
|
|
222
|
+
return {
|
|
223
|
+
...nextContext,
|
|
224
|
+
damage: {
|
|
225
|
+
damage: modified.damage,
|
|
226
|
+
defeated: modified.defeated,
|
|
227
|
+
raw: nextContext.damage?.raw,
|
|
228
|
+
},
|
|
229
|
+
knockback: {
|
|
230
|
+
force: modified.knockbackForce,
|
|
231
|
+
duration: modified.knockbackDuration,
|
|
232
|
+
direction: nextContext.knockback?.direction,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
afterHit(result) {
|
|
237
|
+
systems.combat.hooks?.afterHit?.(result);
|
|
238
|
+
hooks.onAfterHit?.(result as HitResult);
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
: systems.combat.hooks,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
attacker: player,
|
|
245
|
+
target,
|
|
127
246
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Apply knockback only if not defeated (entity still exists)
|
|
131
|
-
if (!hitResult.defeated && hitResult.knockbackForce > 0 && distance > 0) {
|
|
132
|
-
const knockbackDirection = {
|
|
133
|
-
x: dx / distance,
|
|
134
|
-
y: dy / distance
|
|
135
|
-
};
|
|
136
|
-
target.knockback(knockbackDirection, hitResult.knockbackForce, hitResult.knockbackDuration);
|
|
137
|
-
}
|
|
247
|
+
);
|
|
138
248
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
249
|
+
if (!result.cancelled) {
|
|
250
|
+
ai.handleDamage(player, {
|
|
251
|
+
damage: result.damage,
|
|
252
|
+
defeated: result.defeated,
|
|
253
|
+
raw: result.rawDamage,
|
|
254
|
+
});
|
|
142
255
|
}
|
|
143
256
|
|
|
144
|
-
return
|
|
257
|
+
return result as HitResult;
|
|
145
258
|
}
|
|
146
259
|
|
|
260
|
+
const toLegacyHitResult = (context: any): HitResult => ({
|
|
261
|
+
damage: context.damage?.damage ?? 0,
|
|
262
|
+
knockbackForce: context.knockback?.force ?? getPlayerWeaponKnockbackForce(context.attacker),
|
|
263
|
+
knockbackDuration: context.knockback?.duration ?? DEFAULT_KNOCKBACK.duration,
|
|
264
|
+
defeated: context.damage?.defeated ?? false,
|
|
265
|
+
attacker: context.attacker,
|
|
266
|
+
target: context.target,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const resolvePlayerAttackHitboxes = (
|
|
270
|
+
player: RpgPlayer,
|
|
271
|
+
directionKey: string,
|
|
272
|
+
options: ActionBattleOptions
|
|
273
|
+
): ActionBattleHitbox[] => {
|
|
274
|
+
const configuredHitboxes = {
|
|
275
|
+
...DEFAULT_PLAYER_ATTACK_HITBOXES,
|
|
276
|
+
...options.attack?.hitboxes,
|
|
277
|
+
};
|
|
278
|
+
const hitboxConfig =
|
|
279
|
+
configuredHitboxes[
|
|
280
|
+
directionKey as keyof typeof DEFAULT_PLAYER_ATTACK_HITBOXES
|
|
281
|
+
] || configuredHitboxes.default;
|
|
282
|
+
const defaultHitboxes = [
|
|
283
|
+
{
|
|
284
|
+
x: player.x() + hitboxConfig.offsetX,
|
|
285
|
+
y: player.y() + hitboxConfig.offsetY,
|
|
286
|
+
width: hitboxConfig.width,
|
|
287
|
+
height: hitboxConfig.height,
|
|
288
|
+
},
|
|
289
|
+
];
|
|
290
|
+
return (
|
|
291
|
+
options.attack?.resolveHitboxes?.({
|
|
292
|
+
player,
|
|
293
|
+
direction: directionKey,
|
|
294
|
+
defaultHitboxes,
|
|
295
|
+
}) ?? defaultHitboxes
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
|
|
147
299
|
const resolveSignal = (value: any) =>
|
|
148
300
|
typeof value === "function" ? value() : value;
|
|
149
301
|
|
|
@@ -404,6 +556,7 @@ export const createActionBattleServer = (
|
|
|
404
556
|
) => {
|
|
405
557
|
const options = normalizeActionBattleOptions(rawOptions);
|
|
406
558
|
setActionBattleOptions(options);
|
|
559
|
+
setActionBattleSystems(options);
|
|
407
560
|
return defineModule<RpgServer>({
|
|
408
561
|
player: {
|
|
409
562
|
/**
|
|
@@ -419,38 +572,39 @@ export const createActionBattleServer = (
|
|
|
419
572
|
*/
|
|
420
573
|
onInput(player: RpgPlayer, input: any) {
|
|
421
574
|
if (input.action == Control.Action) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
// Get player position
|
|
425
|
-
const playerX = player.x();
|
|
426
|
-
const playerY = player.y();
|
|
575
|
+
const map = player.getCurrentMap();
|
|
427
576
|
const direction = player.getDirection();
|
|
428
577
|
|
|
429
578
|
// Convert Direction enum to string key
|
|
430
579
|
const directionKey = direction as string;
|
|
431
580
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
581
|
+
const hitboxes = resolvePlayerAttackHitboxes(
|
|
582
|
+
player,
|
|
583
|
+
directionKey,
|
|
584
|
+
options
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
if (isActionReservedForNormalEvent(player, map, hitboxes)) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const lockMovement = options.attack?.lockMovement !== false;
|
|
592
|
+
const lockDurationMs =
|
|
593
|
+
options.attack?.lockDurationMs ?? DEFAULT_ATTACK_LOCK_DURATION_MS;
|
|
594
|
+
let movementLocked = false;
|
|
595
|
+
|
|
596
|
+
if (
|
|
597
|
+
lockMovement &&
|
|
598
|
+
!beginPlayerAttackLock(player, map, Math.max(0, lockDurationMs))
|
|
599
|
+
) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
movementLocked = lockMovement && lockDurationMs > 0;
|
|
452
603
|
|
|
453
|
-
|
|
604
|
+
playActionBattleAnimation("attack", player, options.animations);
|
|
605
|
+
if (movementLocked) {
|
|
606
|
+
player.animationFixed = true;
|
|
607
|
+
}
|
|
454
608
|
|
|
455
609
|
map?.createMovingHitbox(hitboxes, { speed: 3 }).subscribe({
|
|
456
610
|
next(hits) {
|
|
@@ -507,3 +661,13 @@ export const createActionBattleServer = (
|
|
|
507
661
|
};
|
|
508
662
|
|
|
509
663
|
export default createActionBattleServer();
|
|
664
|
+
|
|
665
|
+
export {
|
|
666
|
+
AiDebug,
|
|
667
|
+
AiState,
|
|
668
|
+
AttackPattern,
|
|
669
|
+
BattleAi,
|
|
670
|
+
DEFAULT_KNOCKBACK,
|
|
671
|
+
EnemyType,
|
|
672
|
+
} from "./ai.server";
|
|
673
|
+
export type { ApplyHitHooks, BattleAiOptions, HitResult } from "./ai.server";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { manhattanDistance, parseAoeMask } from "./targeting";
|
|
3
|
+
|
|
4
|
+
describe("targeting helpers", () => {
|
|
5
|
+
test("parses an ASCII AoE mask around its center", () => {
|
|
6
|
+
const mask = parseAoeMask([".#.", "###", ".#."]);
|
|
7
|
+
|
|
8
|
+
expect(mask.width).toBe(3);
|
|
9
|
+
expect(mask.height).toBe(3);
|
|
10
|
+
expect(mask.cells).toEqual(
|
|
11
|
+
expect.arrayContaining([
|
|
12
|
+
{ dx: 0, dy: -1 },
|
|
13
|
+
{ dx: -1, dy: 0 },
|
|
14
|
+
{ dx: 0, dy: 0 },
|
|
15
|
+
{ dx: 1, dy: 0 },
|
|
16
|
+
{ dx: 0, dy: 1 },
|
|
17
|
+
])
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("uses Manhattan distance for tile targeting", () => {
|
|
22
|
+
expect(manhattanDistance({ x: 2, y: 3 }, { x: 5, y: 1 })).toBe(5);
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionBattleAiBehavior,
|
|
3
|
+
ActionBattleCombatSystem,
|
|
4
|
+
ActionBattleHitHooks,
|
|
5
|
+
ActionBattleHitbox,
|
|
6
|
+
} from "./core/contracts";
|
|
7
|
+
|
|
1
8
|
export type ActionBattleAoeMask = string[] | string;
|
|
2
9
|
|
|
3
10
|
export type ActionBattleActionBarMode = "items" | "skills" | "both";
|
|
@@ -8,7 +15,8 @@ export type ActionBattleAnimationKey =
|
|
|
8
15
|
| "attack"
|
|
9
16
|
| "hurt"
|
|
10
17
|
| "die"
|
|
11
|
-
| "castSkill"
|
|
18
|
+
| "castSkill"
|
|
19
|
+
| "castSpell";
|
|
12
20
|
|
|
13
21
|
export type ActionBattleAnimationResult =
|
|
14
22
|
| string
|
|
@@ -91,11 +99,48 @@ export interface ActionBattleTargetingOptions {
|
|
|
91
99
|
allowEmptyTarget?: boolean;
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
export interface ActionBattleAttackOptions {
|
|
103
|
+
lockMovement?: boolean;
|
|
104
|
+
lockDurationMs?: number;
|
|
105
|
+
showPreview?: boolean;
|
|
106
|
+
previewDurationMs?: number;
|
|
107
|
+
previewColor?: number;
|
|
108
|
+
previewAccentColor?: number;
|
|
109
|
+
hitboxes?: Partial<
|
|
110
|
+
Record<
|
|
111
|
+
"up" | "down" | "left" | "right" | "default",
|
|
112
|
+
{ offsetX: number; offsetY: number; width: number; height: number }
|
|
113
|
+
>
|
|
114
|
+
>;
|
|
115
|
+
resolveHitboxes?: (context: {
|
|
116
|
+
player: any;
|
|
117
|
+
direction: string;
|
|
118
|
+
defaultHitboxes: ActionBattleHitbox[];
|
|
119
|
+
}) => ActionBattleHitbox[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ActionBattleCombatOptions {
|
|
123
|
+
damage?: ActionBattleCombatSystem["resolveDamage"];
|
|
124
|
+
knockback?: ActionBattleCombatSystem["resolveKnockback"];
|
|
125
|
+
hooks?: ActionBattleHitHooks;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ActionBattleAiSystemOptions {
|
|
129
|
+
behaviors?: Record<string, ActionBattleAiBehavior>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface ActionBattleSystemOptions {
|
|
133
|
+
combat?: ActionBattleCombatOptions;
|
|
134
|
+
ai?: ActionBattleAiSystemOptions;
|
|
135
|
+
}
|
|
136
|
+
|
|
94
137
|
export interface ActionBattleOptions {
|
|
95
138
|
ui?: ActionBattleUiOptions;
|
|
96
139
|
skills?: ActionBattleSkillOptions;
|
|
97
140
|
targeting?: ActionBattleTargetingOptions;
|
|
141
|
+
attack?: ActionBattleAttackOptions;
|
|
98
142
|
animations?: ActionBattleAnimationOptions;
|
|
143
|
+
systems?: ActionBattleSystemOptions;
|
|
99
144
|
}
|
|
100
145
|
|
|
101
146
|
export interface ActionBattleActionBarItem {
|
package/src/ui/state.ts
CHANGED
|
@@ -2,6 +2,16 @@ import { signal } from "canvasengine";
|
|
|
2
2
|
import { ActionBattleActionBarSkill, ActionBattleOptions } from "../types";
|
|
3
3
|
import { DEFAULT_ACTION_BATTLE_OPTIONS, normalizeActionBattleOptions } from "../config";
|
|
4
4
|
|
|
5
|
+
export interface ActionBattleAttackPreviewState {
|
|
6
|
+
active: boolean;
|
|
7
|
+
id: number;
|
|
8
|
+
direction: string;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
durationMs: number;
|
|
11
|
+
color: number;
|
|
12
|
+
accentColor: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
export interface ActionBattleTargetingState {
|
|
6
16
|
active: boolean;
|
|
7
17
|
skill: ActionBattleActionBarSkill | null;
|
|
@@ -18,6 +28,16 @@ const defaultTargetingState: ActionBattleTargetingState = {
|
|
|
18
28
|
aoeMask: DEFAULT_ACTION_BATTLE_OPTIONS.skills?.defaultAoeMask || ["#"],
|
|
19
29
|
};
|
|
20
30
|
|
|
31
|
+
const defaultAttackPreviewState: ActionBattleAttackPreviewState = {
|
|
32
|
+
active: false,
|
|
33
|
+
id: 0,
|
|
34
|
+
direction: "down",
|
|
35
|
+
startedAt: 0,
|
|
36
|
+
durationMs: 180,
|
|
37
|
+
color: 0xfff3b0,
|
|
38
|
+
accentColor: 0xffffff,
|
|
39
|
+
};
|
|
40
|
+
|
|
21
41
|
export const actionBattleUiOptions = signal(
|
|
22
42
|
normalizeActionBattleOptions({}).ui || {}
|
|
23
43
|
);
|
|
@@ -28,6 +48,10 @@ export const actionBattleSkillOptions = signal(
|
|
|
28
48
|
export const actionBattleTargetingState = signal<ActionBattleTargetingState>({
|
|
29
49
|
...defaultTargetingState,
|
|
30
50
|
});
|
|
51
|
+
export const actionBattleAttackPreviewState =
|
|
52
|
+
signal<ActionBattleAttackPreviewState>({
|
|
53
|
+
...defaultAttackPreviewState,
|
|
54
|
+
});
|
|
31
55
|
|
|
32
56
|
export const setActionBattleOptions = (options: ActionBattleOptions = {}) => {
|
|
33
57
|
const normalized = normalizeActionBattleOptions(options);
|
|
@@ -66,3 +90,36 @@ export const moveTargetingOffset = (dx: number, dy: number) => {
|
|
|
66
90
|
offset: next,
|
|
67
91
|
});
|
|
68
92
|
};
|
|
93
|
+
|
|
94
|
+
export const startAttackPreview = (options: {
|
|
95
|
+
direction: string;
|
|
96
|
+
durationMs?: number;
|
|
97
|
+
color?: number;
|
|
98
|
+
accentColor?: number;
|
|
99
|
+
}) => {
|
|
100
|
+
const current = actionBattleAttackPreviewState();
|
|
101
|
+
const id = current.id + 1;
|
|
102
|
+
const durationMs = Math.max(
|
|
103
|
+
1,
|
|
104
|
+
options.durationMs ?? defaultAttackPreviewState.durationMs
|
|
105
|
+
);
|
|
106
|
+
actionBattleAttackPreviewState.set({
|
|
107
|
+
active: true,
|
|
108
|
+
id,
|
|
109
|
+
direction: options.direction,
|
|
110
|
+
startedAt: Date.now(),
|
|
111
|
+
durationMs,
|
|
112
|
+
color: options.color ?? defaultAttackPreviewState.color,
|
|
113
|
+
accentColor: options.accentColor ?? defaultAttackPreviewState.accentColor,
|
|
114
|
+
});
|
|
115
|
+
return id;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const stopAttackPreview = (id?: number) => {
|
|
119
|
+
const current = actionBattleAttackPreviewState();
|
|
120
|
+
if (id !== undefined && current.id !== id) return;
|
|
121
|
+
actionBattleAttackPreviewState.set({
|
|
122
|
+
...current,
|
|
123
|
+
active: false,
|
|
124
|
+
});
|
|
125
|
+
};
|