@rpgjs/action-battle 5.0.0-beta.1 → 5.0.0-beta.10
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/CHANGELOG.md +38 -0
- package/LICENSE +19 -0
- package/README.md +392 -22
- package/dist/{ai.server.d.ts → client/ai.server.d.ts} +90 -28
- package/dist/client/animations.d.ts +16 -0
- package/dist/{client.d.ts → client/client.d.ts} +3 -2
- package/dist/{config.d.ts → client/config.d.ts} +2 -0
- package/dist/client/core/attack-profile.d.ts +9 -0
- package/dist/client/core/attack-runtime.d.ts +20 -0
- package/dist/client/core/context.d.ts +5 -0
- package/dist/client/core/defaults.d.ts +81 -0
- package/dist/client/core/enemy-attack-profiles.d.ts +6 -0
- package/dist/client/core/equipment.d.ts +2 -0
- package/dist/client/core/hit-reaction.d.ts +5 -0
- package/dist/client/core/hit.d.ts +2 -0
- package/dist/client/enemies/factory.d.ts +7 -0
- package/dist/client/index.d.ts +21 -0
- package/dist/client/index.js +24 -31
- package/dist/client/index10.js +61 -0
- package/dist/client/index11.js +55 -0
- package/dist/client/index12.js +106 -0
- package/dist/client/index13.js +143 -0
- package/dist/client/index14.js +25 -0
- package/dist/client/index15.js +72 -0
- package/dist/client/index16.js +1343 -0
- package/dist/client/index17.js +13 -0
- package/dist/client/index18.js +60 -0
- package/dist/client/index19.js +10 -0
- package/dist/client/index2.js +30 -45
- package/dist/client/index20.js +504 -0
- package/dist/client/index3.js +45 -1288
- package/dist/client/index4.js +105 -330
- package/dist/client/index5.js +84 -291
- package/dist/client/index6.js +309 -95
- package/dist/client/index7.js +35 -59
- package/dist/client/index8.js +101 -54
- package/dist/client/index9.js +79 -30
- package/dist/{server.d.ts → client/server.d.ts} +12 -4
- package/dist/client/ui/state.d.ts +35 -0
- package/dist/server/ai.server.d.ts +569 -0
- package/dist/server/animations.d.ts +16 -0
- package/dist/server/config.d.ts +5 -0
- package/dist/server/core/attack-profile.d.ts +9 -0
- package/dist/server/core/attack-runtime.d.ts +20 -0
- package/dist/server/core/context.d.ts +5 -0
- package/dist/server/core/defaults.d.ts +81 -0
- package/dist/server/core/enemy-attack-profiles.d.ts +6 -0
- package/dist/server/core/equipment.d.ts +2 -0
- package/dist/server/core/hit-reaction.d.ts +5 -0
- package/dist/server/core/hit.d.ts +2 -0
- package/dist/server/enemies/factory.d.ts +7 -0
- package/dist/server/index.d.ts +21 -0
- package/dist/server/index.js +23 -31
- package/dist/server/index10.js +1342 -0
- package/dist/server/index11.js +37 -0
- package/dist/server/index12.js +60 -0
- package/dist/server/index13.js +13 -0
- package/dist/server/index14.js +503 -0
- package/dist/server/index15.js +10 -0
- package/dist/server/index2.js +59 -332
- package/dist/server/index3.js +29 -1286
- package/dist/server/index4.js +45 -53
- package/dist/server/index5.js +107 -29
- package/dist/server/index6.js +143 -0
- package/dist/server/index7.js +25 -0
- package/dist/server/index8.js +72 -0
- package/dist/server/index9.js +55 -0
- package/dist/server/server.d.ts +106 -0
- package/dist/server/targeting.d.ts +19 -0
- package/package.json +12 -12
- package/src/ai.server.spec.ts +120 -0
- package/src/ai.server.ts +515 -91
- package/src/animations.ts +149 -0
- package/src/canvas-engine-shim.ts +4 -0
- package/src/client.ts +130 -2
- package/src/components/action-bar.ce +5 -3
- package/src/components/attack-preview.ce +90 -0
- package/src/config.ts +61 -0
- package/src/core/attack-profile.spec.ts +118 -0
- package/src/core/attack-profile.ts +100 -0
- package/src/core/attack-runtime.spec.ts +103 -0
- package/src/core/attack-runtime.ts +83 -0
- package/src/core/context.ts +35 -0
- package/src/core/contracts.ts +126 -0
- package/src/core/defaults.ts +162 -0
- package/src/core/enemy-attack-profiles.spec.ts +35 -0
- package/src/core/enemy-attack-profiles.ts +103 -0
- package/src/core/equipment.spec.ts +37 -0
- package/src/core/equipment.ts +17 -0
- package/src/core/hit-reaction.spec.ts +43 -0
- package/src/core/hit-reaction.ts +70 -0
- package/src/core/hit.spec.ts +111 -0
- package/src/core/hit.ts +92 -0
- package/src/enemies/factory.ts +25 -0
- package/src/index.ts +94 -1
- package/src/server.ts +427 -93
- package/src/targeting.spec.ts +24 -0
- package/src/types/canvas-engine.d.ts +4 -0
- package/src/types.ts +148 -0
- package/src/ui/state.ts +57 -0
- package/dist/index.d.ts +0 -11
- package/dist/ui/state.d.ts +0 -18
- /package/dist/{targeting.d.ts → client/targeting.d.ts} +0 -0
package/src/server.ts
CHANGED
|
@@ -6,10 +6,29 @@ import {
|
|
|
6
6
|
ActionBattleActionBarSkill,
|
|
7
7
|
ActionBattleOptions,
|
|
8
8
|
} from "./types";
|
|
9
|
-
import { normalizeActionBattleOptions } from "./config";
|
|
9
|
+
import { normalizeActionBattleOptions, setActionBattleOptions } from "./config";
|
|
10
10
|
import { manhattanDistance, parseAoeMask } from "./targeting";
|
|
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 {
|
|
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";
|
|
24
|
+
import type { ActionBattleHitbox } from "./core/contracts";
|
|
25
|
+
import type {
|
|
26
|
+
ActionBattleAttackProfile,
|
|
27
|
+
NormalizedActionBattleAttackProfile,
|
|
28
|
+
} from "./types";
|
|
11
29
|
|
|
12
30
|
export const ACTION_BATTLE_ACTION_BAR_GUI_ID = "action-battle-action-bar";
|
|
31
|
+
const DEFAULT_ATTACK_LOCK_DURATION_MS = 350;
|
|
13
32
|
|
|
14
33
|
/**
|
|
15
34
|
* Default player attack hitboxes offsets for each direction
|
|
@@ -19,11 +38,127 @@ export const ACTION_BATTLE_ACTION_BAR_GUI_ID = "action-battle-action-bar";
|
|
|
19
38
|
* when creating the moving hitbox.
|
|
20
39
|
*/
|
|
21
40
|
export const DEFAULT_PLAYER_ATTACK_HITBOXES = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
...DEFAULT_ZELDA_PLAYER_HITBOXES,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const beginPlayerAttackLock = (
|
|
45
|
+
player: RpgPlayer,
|
|
46
|
+
map: ReturnType<RpgPlayer["getCurrentMap"]> | undefined,
|
|
47
|
+
durationMs: number,
|
|
48
|
+
locks: { movement: boolean; direction: boolean }
|
|
49
|
+
): boolean => {
|
|
50
|
+
if (durationMs <= 0) return true;
|
|
51
|
+
|
|
52
|
+
const runtimePlayer = player as any;
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
if (
|
|
55
|
+
typeof runtimePlayer.__actionBattleAttackLockedUntil === "number" &&
|
|
56
|
+
runtimePlayer.__actionBattleAttackLockedUntil > now
|
|
57
|
+
) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const lockId = (runtimePlayer.__actionBattleAttackLockId ?? 0) + 1;
|
|
62
|
+
runtimePlayer.__actionBattleAttackLockId = lockId;
|
|
63
|
+
runtimePlayer.__actionBattleAttackLockedUntil = now + durationMs;
|
|
64
|
+
|
|
65
|
+
const previousCanMove = player.canMove;
|
|
66
|
+
const previousDirectionFixed = player.directionFixed;
|
|
67
|
+
const previousAnimationFixed = player.animationFixed;
|
|
68
|
+
|
|
69
|
+
if (locks.movement) {
|
|
70
|
+
player.pendingInputs = [];
|
|
71
|
+
player.lastProcessedInputTs = 0;
|
|
72
|
+
(map as any)?.stopMovement?.(player);
|
|
73
|
+
player.canMove = false;
|
|
74
|
+
}
|
|
75
|
+
if (locks.direction) {
|
|
76
|
+
player.directionFixed = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
if (runtimePlayer.__actionBattleAttackLockId !== lockId) return;
|
|
81
|
+
runtimePlayer.__actionBattleAttackLockedUntil = 0;
|
|
82
|
+
player.canMove = previousCanMove;
|
|
83
|
+
player.directionFixed = previousDirectionFixed;
|
|
84
|
+
player.animationFixed = previousAnimationFixed;
|
|
85
|
+
}, durationMs);
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const isBattleEvent = (event: RpgEvent) => !!(event as any).battleAi;
|
|
91
|
+
|
|
92
|
+
const rectsOverlap = (
|
|
93
|
+
a: { x: number; y: number; width: number; height: number },
|
|
94
|
+
b: { x: number; y: number; width: number; height: number }
|
|
95
|
+
) =>
|
|
96
|
+
a.x < b.x + b.width &&
|
|
97
|
+
a.x + a.width > b.x &&
|
|
98
|
+
a.y < b.y + b.height &&
|
|
99
|
+
a.y + a.height > b.y;
|
|
100
|
+
|
|
101
|
+
const eventRect = (event: RpgEvent) => {
|
|
102
|
+
const hitbox =
|
|
103
|
+
typeof event.hitbox === "function" ? event.hitbox() : (event as any).hitbox;
|
|
104
|
+
return {
|
|
105
|
+
x: event.x(),
|
|
106
|
+
y: event.y(),
|
|
107
|
+
width: hitbox?.w ?? 32,
|
|
108
|
+
height: hitbox?.h ?? 32,
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const getVisibleActionEvents = (
|
|
113
|
+
player: RpgPlayer,
|
|
114
|
+
map: ReturnType<RpgPlayer["getCurrentMap"]> | undefined,
|
|
115
|
+
hitboxes: Array<{ x: number; y: number; width: number; height: number }>
|
|
116
|
+
) => {
|
|
117
|
+
if (!map) return [];
|
|
118
|
+
|
|
119
|
+
const eventsById = new Map<string, RpgEvent>();
|
|
120
|
+
const addEvent = (event: RpgEvent | undefined) => {
|
|
121
|
+
if (!event) return;
|
|
122
|
+
const isVisible =
|
|
123
|
+
typeof (map as any).isEventVisibleForPlayer === "function"
|
|
124
|
+
? (map as any).isEventVisibleForPlayer(event, player)
|
|
125
|
+
: true;
|
|
126
|
+
if (!isVisible) return;
|
|
127
|
+
eventsById.set(event.id, event);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const collisions = (map as any).getCollisions?.(player.id);
|
|
131
|
+
if (Array.isArray(collisions)) {
|
|
132
|
+
collisions.forEach((id: string) => addEvent(map.getEvent(id)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const direction =
|
|
136
|
+
typeof player.getDirection === "function" ? player.getDirection() : undefined;
|
|
137
|
+
const interactionCollisions = (map as any).getInteractionCollisions?.(
|
|
138
|
+
player.id,
|
|
139
|
+
direction
|
|
140
|
+
);
|
|
141
|
+
if (Array.isArray(interactionCollisions)) {
|
|
142
|
+
interactionCollisions.forEach((id: string) => addEvent(map.getEvent(id)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const event of map.getEvents()) {
|
|
146
|
+
const rect = eventRect(event);
|
|
147
|
+
if (hitboxes.some((hitbox) => rectsOverlap(hitbox, rect))) {
|
|
148
|
+
addEvent(event);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return Array.from(eventsById.values());
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const isActionReservedForNormalEvent = (
|
|
156
|
+
player: RpgPlayer,
|
|
157
|
+
map: ReturnType<RpgPlayer["getCurrentMap"]> | undefined,
|
|
158
|
+
hitboxes: Array<{ x: number; y: number; width: number; height: number }>
|
|
159
|
+
) => {
|
|
160
|
+
const events = getVisibleActionEvents(player, map, hitboxes);
|
|
161
|
+
return events.length > 0 && !events.some(isBattleEvent);
|
|
27
162
|
};
|
|
28
163
|
|
|
29
164
|
/**
|
|
@@ -92,57 +227,141 @@ export function getPlayerWeaponKnockbackForce(player: RpgPlayer): number {
|
|
|
92
227
|
export function applyPlayerHitToEvent(
|
|
93
228
|
player: RpgPlayer,
|
|
94
229
|
target: RpgEvent,
|
|
95
|
-
hooks?: ApplyHitHooks
|
|
230
|
+
hooks?: ApplyHitHooks,
|
|
231
|
+
metadata?: Record<string, any>
|
|
96
232
|
): HitResult | undefined {
|
|
97
233
|
const ai = (target as any).battleAi as BattleAi;
|
|
98
234
|
if (!ai) return undefined;
|
|
99
235
|
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
236
|
+
const systems = getActionBattleSystems();
|
|
237
|
+
const result = applyActionBattleHit(
|
|
238
|
+
{
|
|
239
|
+
...systems.combat,
|
|
240
|
+
hooks: hooks
|
|
241
|
+
? {
|
|
242
|
+
...systems.combat.hooks,
|
|
243
|
+
beforeHit(context) {
|
|
244
|
+
const before = systems.combat.hooks?.beforeHit?.(context);
|
|
245
|
+
if (before === false) return false;
|
|
246
|
+
const nextContext = before || context;
|
|
247
|
+
const legacyResult = toLegacyHitResult(nextContext);
|
|
248
|
+
const modified = hooks.onBeforeHit?.(legacyResult);
|
|
249
|
+
if (!modified) return nextContext;
|
|
250
|
+
return {
|
|
251
|
+
...nextContext,
|
|
252
|
+
damage: {
|
|
253
|
+
damage: modified.damage,
|
|
254
|
+
defeated: modified.defeated,
|
|
255
|
+
raw: nextContext.damage?.raw,
|
|
256
|
+
},
|
|
257
|
+
knockback: {
|
|
258
|
+
force: modified.knockbackForce,
|
|
259
|
+
duration: modified.knockbackDuration,
|
|
260
|
+
direction: nextContext.knockback?.direction,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
afterHit(result) {
|
|
265
|
+
systems.combat.hooks?.afterHit?.(result);
|
|
266
|
+
hooks.onAfterHit?.(result as HitResult);
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
: systems.combat.hooks,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
attacker: player,
|
|
273
|
+
target,
|
|
274
|
+
metadata,
|
|
275
|
+
reaction: metadata?.reaction,
|
|
126
276
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Apply knockback only if not defeated (entity still exists)
|
|
130
|
-
if (!hitResult.defeated && hitResult.knockbackForce > 0 && distance > 0) {
|
|
131
|
-
const knockbackDirection = {
|
|
132
|
-
x: dx / distance,
|
|
133
|
-
y: dy / distance
|
|
134
|
-
};
|
|
135
|
-
target.knockback(knockbackDirection, hitResult.knockbackForce, hitResult.knockbackDuration);
|
|
136
|
-
}
|
|
277
|
+
);
|
|
137
278
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
279
|
+
if (!result.cancelled) {
|
|
280
|
+
ai.handleDamage(player, {
|
|
281
|
+
damage: result.damage,
|
|
282
|
+
defeated: result.defeated,
|
|
283
|
+
raw: result.rawDamage,
|
|
284
|
+
reaction: result.reaction,
|
|
285
|
+
});
|
|
141
286
|
}
|
|
142
287
|
|
|
143
|
-
return
|
|
288
|
+
return result as HitResult;
|
|
144
289
|
}
|
|
145
290
|
|
|
291
|
+
const toLegacyHitResult = (context: any): HitResult => ({
|
|
292
|
+
damage: context.damage?.damage ?? 0,
|
|
293
|
+
knockbackForce: context.knockback?.force ?? getPlayerWeaponKnockbackForce(context.attacker),
|
|
294
|
+
knockbackDuration: context.knockback?.duration ?? DEFAULT_KNOCKBACK.duration,
|
|
295
|
+
defeated: context.damage?.defeated ?? false,
|
|
296
|
+
attacker: context.attacker,
|
|
297
|
+
target: context.target,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const resolvePlayerAttackHitboxes = (
|
|
301
|
+
player: RpgPlayer,
|
|
302
|
+
directionKey: string,
|
|
303
|
+
options: ActionBattleOptions,
|
|
304
|
+
profile: NormalizedActionBattleAttackProfile
|
|
305
|
+
): ActionBattleHitbox[] => {
|
|
306
|
+
const configuredHitboxes = {
|
|
307
|
+
...DEFAULT_PLAYER_ATTACK_HITBOXES,
|
|
308
|
+
...options.attack?.hitboxes,
|
|
309
|
+
...profile.hitboxes,
|
|
310
|
+
};
|
|
311
|
+
const hitboxConfig =
|
|
312
|
+
configuredHitboxes[
|
|
313
|
+
directionKey as keyof typeof DEFAULT_PLAYER_ATTACK_HITBOXES
|
|
314
|
+
] || configuredHitboxes.default;
|
|
315
|
+
const defaultHitboxes = [
|
|
316
|
+
{
|
|
317
|
+
x: player.x() + hitboxConfig.offsetX,
|
|
318
|
+
y: player.y() + hitboxConfig.offsetY,
|
|
319
|
+
width: hitboxConfig.width,
|
|
320
|
+
height: hitboxConfig.height,
|
|
321
|
+
},
|
|
322
|
+
];
|
|
323
|
+
return (
|
|
324
|
+
options.attack?.resolveHitboxes?.({
|
|
325
|
+
player,
|
|
326
|
+
direction: directionKey,
|
|
327
|
+
defaultHitboxes,
|
|
328
|
+
}) ?? defaultHitboxes
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const mergeAttackProfileOverrides = (
|
|
333
|
+
base: NormalizedActionBattleAttackProfile,
|
|
334
|
+
override: ActionBattleAttackProfile
|
|
335
|
+
): ActionBattleAttackProfile => ({
|
|
336
|
+
...base,
|
|
337
|
+
...override,
|
|
338
|
+
reaction: {
|
|
339
|
+
...base.reaction,
|
|
340
|
+
...override.reaction,
|
|
341
|
+
},
|
|
342
|
+
hitboxes: {
|
|
343
|
+
...base.hitboxes,
|
|
344
|
+
...override.hitboxes,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const resolvePlayerAttackProfile = (
|
|
349
|
+
player: RpgPlayer,
|
|
350
|
+
options: ActionBattleOptions
|
|
351
|
+
): NormalizedActionBattleAttackProfile => {
|
|
352
|
+
const baseProfile = getNormalizedActionBattleAttackProfile(options);
|
|
353
|
+
const weaponProfile = resolveActionBattleWeaponAttackProfile(player);
|
|
354
|
+
if (!weaponProfile) return baseProfile;
|
|
355
|
+
return normalizeActionBattleAttackProfile(
|
|
356
|
+
mergeAttackProfileOverrides(baseProfile, weaponProfile),
|
|
357
|
+
{
|
|
358
|
+
lockMovement: options.attack?.lockMovement,
|
|
359
|
+
lockDurationMs: options.attack?.lockDurationMs,
|
|
360
|
+
hitboxes: options.attack?.hitboxes,
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
146
365
|
const resolveSignal = (value: any) =>
|
|
147
366
|
typeof value === "function" ? value() : value;
|
|
148
367
|
|
|
@@ -270,7 +489,7 @@ const ensureActionBarGui = (
|
|
|
270
489
|
const gui = existing || player.gui(ACTION_BATTLE_ACTION_BAR_GUI_ID);
|
|
271
490
|
if (!(gui as any).__actionBattleReady) {
|
|
272
491
|
(gui as any).__actionBattleReady = true;
|
|
273
|
-
gui.on("useItem", ({ id }) => {
|
|
492
|
+
gui.on("useItem", ({ id }: { id: string }) => {
|
|
274
493
|
try {
|
|
275
494
|
player.useItem(id);
|
|
276
495
|
} catch {
|
|
@@ -278,10 +497,13 @@ const ensureActionBarGui = (
|
|
|
278
497
|
}
|
|
279
498
|
gui.update(buildActionBarData(player, options));
|
|
280
499
|
});
|
|
281
|
-
gui.on(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
500
|
+
gui.on(
|
|
501
|
+
"useSkill",
|
|
502
|
+
({ id, target }: { id: string; target?: { x: number; y: number } }) => {
|
|
503
|
+
handleActionBattleSkillUse(player, id, target, options);
|
|
504
|
+
gui.update(buildActionBarData(player, options));
|
|
505
|
+
}
|
|
506
|
+
);
|
|
285
507
|
gui.on("refresh", () => {
|
|
286
508
|
gui.update(buildActionBarData(player, options));
|
|
287
509
|
});
|
|
@@ -330,13 +552,21 @@ const handleActionBattleSkillUse = (
|
|
|
330
552
|
target: { x: number; y: number } | undefined,
|
|
331
553
|
options: ActionBattleOptions
|
|
332
554
|
) => {
|
|
555
|
+
const skillData = resolveSkillData(player, skillId);
|
|
556
|
+
|
|
333
557
|
const map = player.getCurrentMap();
|
|
334
558
|
if (!map) {
|
|
559
|
+
playActionBattleAnimation("castSkill", player, options.animations, {
|
|
560
|
+
skill: skillData,
|
|
561
|
+
});
|
|
335
562
|
player.useSkill(skillId);
|
|
336
563
|
return;
|
|
337
564
|
}
|
|
338
565
|
const targeting = resolveSkillTargeting(player, skillId, options);
|
|
339
566
|
if (!targeting || !target) {
|
|
567
|
+
playActionBattleAnimation("castSkill", player, options.animations, {
|
|
568
|
+
skill: skillData,
|
|
569
|
+
});
|
|
340
570
|
player.useSkill(skillId);
|
|
341
571
|
return;
|
|
342
572
|
}
|
|
@@ -362,7 +592,7 @@ const handleActionBattleSkillUse = (
|
|
|
362
592
|
const targets: any[] = [];
|
|
363
593
|
const affects = options.targeting?.affects || "events";
|
|
364
594
|
if (affects === "events" || affects === "both") {
|
|
365
|
-
map.getEvents().forEach((event) => {
|
|
595
|
+
map.getEvents().forEach((event: RpgEvent) => {
|
|
366
596
|
const tile = getEntityTile(event, tileSize);
|
|
367
597
|
if (affected.has(`${tile.x},${tile.y}`)) {
|
|
368
598
|
targets.push(event);
|
|
@@ -370,7 +600,7 @@ const handleActionBattleSkillUse = (
|
|
|
370
600
|
});
|
|
371
601
|
}
|
|
372
602
|
if (affects === "players" || affects === "both") {
|
|
373
|
-
map.getPlayers().forEach((other) => {
|
|
603
|
+
map.getPlayers().forEach((other: RpgPlayer) => {
|
|
374
604
|
if (other.id === player.id) return;
|
|
375
605
|
const tile = getEntityTile(other, tileSize);
|
|
376
606
|
if (affected.has(`${tile.x},${tile.y}`)) {
|
|
@@ -383,6 +613,10 @@ const handleActionBattleSkillUse = (
|
|
|
383
613
|
return;
|
|
384
614
|
}
|
|
385
615
|
|
|
616
|
+
playActionBattleAnimation("castSkill", player, options.animations, {
|
|
617
|
+
skill: skillData,
|
|
618
|
+
target: targets[0],
|
|
619
|
+
});
|
|
386
620
|
player.useSkill(skillId, targets as any);
|
|
387
621
|
};
|
|
388
622
|
|
|
@@ -390,6 +624,8 @@ export const createActionBattleServer = (
|
|
|
390
624
|
rawOptions: ActionBattleOptions = {}
|
|
391
625
|
) => {
|
|
392
626
|
const options = normalizeActionBattleOptions(rawOptions);
|
|
627
|
+
setActionBattleOptions(options);
|
|
628
|
+
setActionBattleSystems(options);
|
|
393
629
|
return defineModule<RpgServer>({
|
|
394
630
|
player: {
|
|
395
631
|
/**
|
|
@@ -405,51 +641,90 @@ export const createActionBattleServer = (
|
|
|
405
641
|
*/
|
|
406
642
|
onInput(player: RpgPlayer, input: any) {
|
|
407
643
|
if (input.action == Control.Action) {
|
|
408
|
-
|
|
409
|
-
player.setGraphicAnimation("attack", 1);
|
|
410
|
-
|
|
411
|
-
// Get player position
|
|
412
|
-
const playerX = player.x();
|
|
413
|
-
const playerY = player.y();
|
|
644
|
+
const map = player.getCurrentMap();
|
|
414
645
|
const direction = player.getDirection();
|
|
646
|
+
const attackProfile = resolvePlayerAttackProfile(player, options);
|
|
415
647
|
|
|
416
648
|
// Convert Direction enum to string key
|
|
417
649
|
const directionKey = direction as string;
|
|
418
650
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
651
|
+
const hitboxes = resolvePlayerAttackHitboxes(
|
|
652
|
+
player,
|
|
653
|
+
directionKey,
|
|
654
|
+
options,
|
|
655
|
+
attackProfile
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
if (isActionReservedForNormalEvent(player, map, hitboxes)) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const lockMovement = attackProfile.movementLock;
|
|
663
|
+
const lockDirection = attackProfile.directionLock;
|
|
664
|
+
const lockDurationMs =
|
|
665
|
+
attackProfile.totalDurationMs ?? DEFAULT_ATTACK_LOCK_DURATION_MS;
|
|
666
|
+
const actionLocked = (lockMovement || lockDirection) && lockDurationMs > 0;
|
|
667
|
+
|
|
668
|
+
if (
|
|
669
|
+
actionLocked &&
|
|
670
|
+
!beginPlayerAttackLock(player, map, Math.max(0, lockDurationMs), {
|
|
671
|
+
movement: lockMovement,
|
|
672
|
+
direction: lockDirection,
|
|
673
|
+
})
|
|
674
|
+
) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
playActionBattleAnimation("attack", player, options.animations);
|
|
679
|
+
if (actionLocked) {
|
|
680
|
+
player.animationFixed = true;
|
|
681
|
+
}
|
|
682
|
+
const attackId = createActionBattleAttackId(
|
|
683
|
+
player.id,
|
|
684
|
+
attackProfile.id
|
|
685
|
+
);
|
|
686
|
+
const hitTracker = new ActionBattleHitTracker(
|
|
687
|
+
attackProfile.hitPolicy
|
|
688
|
+
);
|
|
689
|
+
if (options.debug?.attacks) {
|
|
690
|
+
console.log("[ActionBattle] player attack", {
|
|
691
|
+
attackId,
|
|
692
|
+
playerId: player.id,
|
|
693
|
+
profile: attackProfile.id,
|
|
694
|
+
hitboxes,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
scheduleActionBattleStartup(attackProfile, () => {
|
|
699
|
+
map
|
|
700
|
+
?.createMovingHitbox(hitboxes, {
|
|
701
|
+
speed: resolveActionBattleHitboxSpeed(
|
|
702
|
+
attackProfile,
|
|
703
|
+
hitboxes.length
|
|
704
|
+
),
|
|
705
|
+
})
|
|
706
|
+
.subscribe({
|
|
707
|
+
next(hits: any[]) {
|
|
708
|
+
hits.forEach((hit: any) => {
|
|
709
|
+
if (hit instanceof RpgEvent) {
|
|
710
|
+
if (!hitTracker.tryHit(hit)) return;
|
|
711
|
+
const result = applyPlayerHitToEvent(
|
|
712
|
+
player,
|
|
713
|
+
hit,
|
|
714
|
+
undefined,
|
|
715
|
+
{
|
|
716
|
+
attackId,
|
|
717
|
+
attackProfileId: attackProfile.id,
|
|
718
|
+
reaction: attackProfile.reaction,
|
|
719
|
+
}
|
|
720
|
+
);
|
|
721
|
+
if (result?.defeated) {
|
|
722
|
+
console.log(`Player ${player.id} defeated AI ${hit.id}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
},
|
|
451
727
|
});
|
|
452
|
-
},
|
|
453
728
|
});
|
|
454
729
|
}
|
|
455
730
|
},
|
|
@@ -494,3 +769,62 @@ export const createActionBattleServer = (
|
|
|
494
769
|
};
|
|
495
770
|
|
|
496
771
|
export default createActionBattleServer();
|
|
772
|
+
|
|
773
|
+
export {
|
|
774
|
+
ACTION_BATTLE_HITBOX_FRAME_MS,
|
|
775
|
+
ActionBattleHitTracker,
|
|
776
|
+
createActionBattleAttackId,
|
|
777
|
+
getNormalizedActionBattleAttackProfile,
|
|
778
|
+
resolveActionBattleHitboxSpeed,
|
|
779
|
+
scheduleActionBattleStartup,
|
|
780
|
+
} from "./core/attack-runtime";
|
|
781
|
+
export {
|
|
782
|
+
DEFAULT_ACTION_BATTLE_ATTACK_PROFILE,
|
|
783
|
+
normalizeActionBattleAttackProfile,
|
|
784
|
+
type ActionBattleAttackProfileFallbacks,
|
|
785
|
+
} from "./core/attack-profile";
|
|
786
|
+
export type {
|
|
787
|
+
ActionBattleAttackDirection,
|
|
788
|
+
ActionBattleAttackHitboxConfig,
|
|
789
|
+
ActionBattleAttackHitboxMap,
|
|
790
|
+
ActionBattleAttackHitPolicy,
|
|
791
|
+
ActionBattleAttackProfile,
|
|
792
|
+
ActionBattleDebugOptions,
|
|
793
|
+
ActionBattleHitReactionProfile,
|
|
794
|
+
NormalizedActionBattleHitReactionProfile,
|
|
795
|
+
NormalizedActionBattleAttackProfile,
|
|
796
|
+
} from "./types";
|
|
797
|
+
export {
|
|
798
|
+
DEFAULT_ACTION_BATTLE_HIT_REACTION,
|
|
799
|
+
isActionBattleEntityInvincible,
|
|
800
|
+
normalizeActionBattleHitReaction,
|
|
801
|
+
setActionBattleInvincibility,
|
|
802
|
+
} from "./core/hit-reaction";
|
|
803
|
+
export {
|
|
804
|
+
DEFAULT_ACTION_BATTLE_ENEMY_ATTACK_PROFILES,
|
|
805
|
+
normalizeActionBattleEnemyAttackProfiles,
|
|
806
|
+
type ActionBattleEnemyAttackProfileKey,
|
|
807
|
+
type ActionBattleEnemyAttackProfileMap,
|
|
808
|
+
type NormalizedActionBattleEnemyAttackProfileMap,
|
|
809
|
+
} from "./core/enemy-attack-profiles";
|
|
810
|
+
export { resolveActionBattleWeaponAttackProfile } from "./core/equipment";
|
|
811
|
+
export {
|
|
812
|
+
AiDebug,
|
|
813
|
+
AiState,
|
|
814
|
+
AttackPattern,
|
|
815
|
+
BattleAi,
|
|
816
|
+
DEFAULT_KNOCKBACK,
|
|
817
|
+
EnemyType,
|
|
818
|
+
} from "./ai.server";
|
|
819
|
+
export type {
|
|
820
|
+
ApplyHitHooks,
|
|
821
|
+
BattleAiDefeatedCallback,
|
|
822
|
+
BattleAiDefeatedContext,
|
|
823
|
+
BattleAiDefeatReward,
|
|
824
|
+
BattleAiLegacyDefeatedCallback,
|
|
825
|
+
BattleAiLegacyOptions,
|
|
826
|
+
BattleAiOptions,
|
|
827
|
+
BattleAiRewardItem,
|
|
828
|
+
BattleAiRewards,
|
|
829
|
+
HitResult,
|
|
830
|
+
} 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
|
+
});
|