@rpgjs/action-battle 5.0.0-beta.5 → 5.0.0-beta.7
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 +161 -22
- package/dist/ai.server.d.ts +55 -4
- package/dist/client/index.js +13 -8
- package/dist/client/index10.js +54 -136
- package/dist/client/index11.js +52 -23
- package/dist/client/index12.js +101 -1217
- package/dist/client/index13.js +139 -42
- package/dist/client/index14.js +23 -8
- package/dist/client/index15.js +68 -444
- 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 +25 -87
- package/dist/client/index20.js +504 -0
- package/dist/client/index3.js +45 -83
- package/dist/client/index4.js +98 -297
- package/dist/client/index5.js +81 -33
- package/dist/client/index6.js +284 -78
- package/dist/client/index7.js +33 -74
- package/dist/client/index8.js +95 -55
- package/dist/client/index9.js +75 -96
- package/dist/core/attack-profile.d.ts +9 -0
- package/dist/core/attack-runtime.d.ts +20 -0
- package/dist/core/enemy-attack-profiles.d.ts +6 -0
- package/dist/core/equipment.d.ts +2 -0
- package/dist/core/hit-reaction.d.ts +5 -0
- package/dist/index.d.ts +7 -2
- package/dist/server/index.js +12 -7
- package/dist/server/index10.js +1340 -8
- 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 +1 -1
- package/dist/server/index3.js +25 -87
- package/dist/server/index4.js +45 -141
- package/dist/server/index5.js +104 -21
- package/dist/server/index6.js +137 -1215
- package/dist/server/index7.js +22 -34
- package/dist/server/index8.js +70 -44
- package/dist/server/index9.js +44 -437
- package/dist/server.d.ts +8 -2
- package/dist/ui/state.d.ts +5 -5
- package/package.json +5 -5
- package/src/ai.server.spec.ts +120 -0
- package/src/ai.server.ts +362 -56
- package/src/client.ts +21 -12
- package/src/config.ts +17 -2
- 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/contracts.ts +3 -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 +54 -1
- package/src/core/hit.ts +26 -0
- package/src/index.ts +48 -1
- package/src/server.ts +192 -34
- package/src/types.ts +62 -6
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ActionBattleAttackProfile } from "../types";
|
|
2
|
+
|
|
3
|
+
const resolveItemId = (item: any) => item?.id?.() ?? item?.id;
|
|
4
|
+
|
|
5
|
+
export function resolveActionBattleWeaponAttackProfile(
|
|
6
|
+
entity: any
|
|
7
|
+
): ActionBattleAttackProfile | null {
|
|
8
|
+
const equipments = entity?.equipments?.() || [];
|
|
9
|
+
for (const item of equipments) {
|
|
10
|
+
const itemId = resolveItemId(item);
|
|
11
|
+
const itemData = entity?.databaseById?.(itemId);
|
|
12
|
+
if (itemData?._type === "weapon" && itemData.attackProfile) {
|
|
13
|
+
return itemData.attackProfile;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ACTION_BATTLE_HIT_REACTION,
|
|
4
|
+
isActionBattleEntityInvincible,
|
|
5
|
+
normalizeActionBattleHitReaction,
|
|
6
|
+
setActionBattleInvincibility,
|
|
7
|
+
} from "./hit-reaction";
|
|
8
|
+
|
|
9
|
+
describe("hit reaction helpers", () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("normalizes hit reaction defaults", () => {
|
|
15
|
+
expect(normalizeActionBattleHitReaction(undefined)).toEqual(
|
|
16
|
+
DEFAULT_ACTION_BATTLE_HIT_REACTION
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("normalizes unsafe reaction values", () => {
|
|
21
|
+
expect(
|
|
22
|
+
normalizeActionBattleHitReaction({
|
|
23
|
+
invincibilityMs: -10,
|
|
24
|
+
hitstunMs: -20,
|
|
25
|
+
staggerPower: -1,
|
|
26
|
+
})
|
|
27
|
+
).toEqual({
|
|
28
|
+
invincibilityMs: 0,
|
|
29
|
+
hitstunMs: 0,
|
|
30
|
+
staggerPower: 0,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("tracks invincibility windows on entities", () => {
|
|
35
|
+
const entity = {};
|
|
36
|
+
vi.spyOn(Date, "now").mockReturnValue(1000);
|
|
37
|
+
|
|
38
|
+
setActionBattleInvincibility(entity, 250);
|
|
39
|
+
|
|
40
|
+
expect(isActionBattleEntityInvincible(entity, 1100)).toBe(true);
|
|
41
|
+
expect(isActionBattleEntityInvincible(entity, 1300)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionBattleHitReactionProfile,
|
|
3
|
+
NormalizedActionBattleHitReactionProfile,
|
|
4
|
+
} from "../types";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_ACTION_BATTLE_HIT_REACTION: NormalizedActionBattleHitReactionProfile = {
|
|
7
|
+
invincibilityMs: 250,
|
|
8
|
+
hitstunMs: 150,
|
|
9
|
+
staggerPower: 1,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const STATE_KEY = "__actionBattleHitReaction";
|
|
13
|
+
|
|
14
|
+
interface ActionBattleHitReactionRuntimeState {
|
|
15
|
+
invincibleUntil: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const isFiniteNumber = (value: unknown): value is number =>
|
|
19
|
+
typeof value === "number" && Number.isFinite(value);
|
|
20
|
+
|
|
21
|
+
const nonNegativeMs = (value: unknown, fallback: number) =>
|
|
22
|
+
isFiniteNumber(value) ? Math.max(0, value) : fallback;
|
|
23
|
+
|
|
24
|
+
const nonNegativeValue = (value: unknown, fallback: number) =>
|
|
25
|
+
isFiniteNumber(value) ? Math.max(0, value) : fallback;
|
|
26
|
+
|
|
27
|
+
const getRuntimeState = (entity: any): ActionBattleHitReactionRuntimeState => {
|
|
28
|
+
if (!entity[STATE_KEY]) {
|
|
29
|
+
entity[STATE_KEY] = {
|
|
30
|
+
invincibleUntil: 0,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return entity[STATE_KEY];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function normalizeActionBattleHitReaction(
|
|
37
|
+
reaction: ActionBattleHitReactionProfile | undefined,
|
|
38
|
+
defaults: NormalizedActionBattleHitReactionProfile =
|
|
39
|
+
DEFAULT_ACTION_BATTLE_HIT_REACTION
|
|
40
|
+
): NormalizedActionBattleHitReactionProfile {
|
|
41
|
+
return {
|
|
42
|
+
invincibilityMs: nonNegativeMs(
|
|
43
|
+
reaction?.invincibilityMs,
|
|
44
|
+
defaults.invincibilityMs
|
|
45
|
+
),
|
|
46
|
+
hitstunMs: nonNegativeMs(reaction?.hitstunMs, defaults.hitstunMs),
|
|
47
|
+
staggerPower: nonNegativeValue(
|
|
48
|
+
reaction?.staggerPower,
|
|
49
|
+
defaults.staggerPower
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isActionBattleEntityInvincible(
|
|
55
|
+
entity: any,
|
|
56
|
+
now = Date.now()
|
|
57
|
+
): boolean {
|
|
58
|
+
if (!entity) return false;
|
|
59
|
+
return getRuntimeState(entity).invincibleUntil > now;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function setActionBattleInvincibility(
|
|
63
|
+
entity: any,
|
|
64
|
+
durationMs: number,
|
|
65
|
+
now = Date.now()
|
|
66
|
+
): void {
|
|
67
|
+
if (!entity || durationMs <= 0) return;
|
|
68
|
+
const state = getRuntimeState(entity);
|
|
69
|
+
state.invincibleUntil = Math.max(state.invincibleUntil, now + durationMs);
|
|
70
|
+
}
|
package/src/core/hit.spec.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { describe, expect, test, vi } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import { applyActionBattleHit } from "./hit";
|
|
3
3
|
import type { ActionBattleCombatSystem } from "./contracts";
|
|
4
|
+
import { setActionBattleInvincibility } from "./hit-reaction";
|
|
4
5
|
|
|
5
6
|
const entity = (hp = 100) => ({
|
|
6
7
|
hp,
|
|
@@ -10,6 +11,10 @@ const entity = (hp = 100) => ({
|
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
describe("applyActionBattleHit", () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
13
18
|
test("runs beforeHit before resolving damage", () => {
|
|
14
19
|
const calls: string[] = [];
|
|
15
20
|
const attacker = entity();
|
|
@@ -55,4 +60,52 @@ describe("applyActionBattleHit", () => {
|
|
|
55
60
|
expect(result.cancelled).toBe(true);
|
|
56
61
|
expect(resolveDamage).not.toHaveBeenCalled();
|
|
57
62
|
});
|
|
63
|
+
|
|
64
|
+
test("cancels a hit when the target is invincible", () => {
|
|
65
|
+
const resolveDamage = vi.fn();
|
|
66
|
+
const attacker = entity();
|
|
67
|
+
const target = entity();
|
|
68
|
+
setActionBattleInvincibility(target, 500, 1000);
|
|
69
|
+
vi.spyOn(Date, "now").mockReturnValue(1100);
|
|
70
|
+
const system: ActionBattleCombatSystem = {
|
|
71
|
+
resolveHitboxes: () => [],
|
|
72
|
+
resolveDamage,
|
|
73
|
+
resolveKnockback: () => ({ force: 0, duration: 0 }),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const result = applyActionBattleHit(system, {
|
|
77
|
+
attacker: attacker as any,
|
|
78
|
+
target: target as any,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(result.cancelled).toBe(true);
|
|
82
|
+
expect(resolveDamage).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("applies invincibility from hit reaction after damage", () => {
|
|
86
|
+
vi.spyOn(Date, "now").mockReturnValue(2000);
|
|
87
|
+
const attacker = entity();
|
|
88
|
+
const target = entity();
|
|
89
|
+
const system: ActionBattleCombatSystem = {
|
|
90
|
+
resolveHitboxes: () => [],
|
|
91
|
+
resolveDamage: () => ({ damage: 12, defeated: false }),
|
|
92
|
+
resolveKnockback: () => ({ force: 0, duration: 0 }),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const result = applyActionBattleHit(system, {
|
|
96
|
+
attacker: attacker as any,
|
|
97
|
+
target: target as any,
|
|
98
|
+
reaction: {
|
|
99
|
+
invincibilityMs: 300,
|
|
100
|
+
hitstunMs: 120,
|
|
101
|
+
staggerPower: 1,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result.cancelled).toBeUndefined();
|
|
106
|
+
expect(applyActionBattleHit(system, {
|
|
107
|
+
attacker: attacker as any,
|
|
108
|
+
target: target as any,
|
|
109
|
+
}).cancelled).toBe(true);
|
|
110
|
+
});
|
|
58
111
|
});
|
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
|
@@ -7,7 +7,18 @@ import type { ActionBattleOptions } from "./types";
|
|
|
7
7
|
export { BattleAi, AiState, EnemyType, AttackPattern, AiDebug, DEFAULT_KNOCKBACK } from "./ai.server";
|
|
8
8
|
|
|
9
9
|
// Types exports
|
|
10
|
-
export type {
|
|
10
|
+
export type {
|
|
11
|
+
HitResult,
|
|
12
|
+
ApplyHitHooks,
|
|
13
|
+
BattleAiOptions,
|
|
14
|
+
BattleAiDefeatedCallback,
|
|
15
|
+
BattleAiDefeatedContext,
|
|
16
|
+
BattleAiDefeatReward,
|
|
17
|
+
BattleAiLegacyDefeatedCallback,
|
|
18
|
+
BattleAiLegacyOptions,
|
|
19
|
+
BattleAiRewardItem,
|
|
20
|
+
BattleAiRewards,
|
|
21
|
+
} from "./ai.server";
|
|
11
22
|
export type {
|
|
12
23
|
ActionBattleAnimationContext,
|
|
13
24
|
ActionBattleAnimationEntity,
|
|
@@ -25,6 +36,15 @@ export type {
|
|
|
25
36
|
ActionBattleUiOptions,
|
|
26
37
|
ActionBattleUiActionBarOptions,
|
|
27
38
|
ActionBattleUiTargetingOptions,
|
|
39
|
+
ActionBattleAttackDirection,
|
|
40
|
+
ActionBattleAttackHitboxConfig,
|
|
41
|
+
ActionBattleAttackHitboxMap,
|
|
42
|
+
ActionBattleAttackHitPolicy,
|
|
43
|
+
ActionBattleAttackProfile,
|
|
44
|
+
ActionBattleDebugOptions,
|
|
45
|
+
ActionBattleHitReactionProfile,
|
|
46
|
+
NormalizedActionBattleHitReactionProfile,
|
|
47
|
+
NormalizedActionBattleAttackProfile,
|
|
28
48
|
ActionBattleCombatOptions,
|
|
29
49
|
ActionBattleSystemOptions,
|
|
30
50
|
ActionBattleAiSystemOptions,
|
|
@@ -47,6 +67,33 @@ export type {
|
|
|
47
67
|
ActionBattleKnockbackResult,
|
|
48
68
|
ActionBattleSystems,
|
|
49
69
|
} from "./core/contracts";
|
|
70
|
+
export {
|
|
71
|
+
DEFAULT_ACTION_BATTLE_ATTACK_PROFILE,
|
|
72
|
+
normalizeActionBattleAttackProfile,
|
|
73
|
+
type ActionBattleAttackProfileFallbacks,
|
|
74
|
+
} from "./core/attack-profile";
|
|
75
|
+
export {
|
|
76
|
+
ACTION_BATTLE_HITBOX_FRAME_MS,
|
|
77
|
+
ActionBattleHitTracker,
|
|
78
|
+
createActionBattleAttackId,
|
|
79
|
+
getNormalizedActionBattleAttackProfile,
|
|
80
|
+
resolveActionBattleHitboxSpeed,
|
|
81
|
+
scheduleActionBattleStartup,
|
|
82
|
+
} from "./core/attack-runtime";
|
|
83
|
+
export {
|
|
84
|
+
DEFAULT_ACTION_BATTLE_HIT_REACTION,
|
|
85
|
+
isActionBattleEntityInvincible,
|
|
86
|
+
normalizeActionBattleHitReaction,
|
|
87
|
+
setActionBattleInvincibility,
|
|
88
|
+
} from "./core/hit-reaction";
|
|
89
|
+
export {
|
|
90
|
+
DEFAULT_ACTION_BATTLE_ENEMY_ATTACK_PROFILES,
|
|
91
|
+
normalizeActionBattleEnemyAttackProfiles,
|
|
92
|
+
type ActionBattleEnemyAttackProfileKey,
|
|
93
|
+
type ActionBattleEnemyAttackProfileMap,
|
|
94
|
+
type NormalizedActionBattleEnemyAttackProfileMap,
|
|
95
|
+
} from "./core/enemy-attack-profiles";
|
|
96
|
+
export { resolveActionBattleWeaponAttackProfile } from "./core/equipment";
|
|
50
97
|
export {
|
|
51
98
|
DEFAULT_ZELDA_PLAYER_HITBOXES,
|
|
52
99
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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(
|
|
435
|
-
|
|
436
|
-
|
|
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 =
|
|
663
|
+
const lockMovement = attackProfile.movementLock;
|
|
664
|
+
const lockDirection = attackProfile.directionLock;
|
|
592
665
|
const lockDurationMs =
|
|
593
|
-
|
|
594
|
-
|
|
666
|
+
attackProfile.totalDurationMs ?? DEFAULT_ATTACK_LOCK_DURATION_MS;
|
|
667
|
+
const actionLocked = (lockMovement || lockDirection) && lockDurationMs > 0;
|
|
595
668
|
|
|
596
669
|
if (
|
|
597
|
-
|
|
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 (
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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,
|
|
@@ -670,4 +817,15 @@ export {
|
|
|
670
817
|
DEFAULT_KNOCKBACK,
|
|
671
818
|
EnemyType,
|
|
672
819
|
} from "./ai.server";
|
|
673
|
-
export type {
|
|
820
|
+
export type {
|
|
821
|
+
ApplyHitHooks,
|
|
822
|
+
BattleAiDefeatedCallback,
|
|
823
|
+
BattleAiDefeatedContext,
|
|
824
|
+
BattleAiDefeatReward,
|
|
825
|
+
BattleAiLegacyDefeatedCallback,
|
|
826
|
+
BattleAiLegacyOptions,
|
|
827
|
+
BattleAiOptions,
|
|
828
|
+
BattleAiRewardItem,
|
|
829
|
+
BattleAiRewards,
|
|
830
|
+
HitResult,
|
|
831
|
+
} from "./ai.server";
|