@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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionBattleAttackProfile,
|
|
3
|
+
NormalizedActionBattleAttackProfile,
|
|
4
|
+
} from "../types";
|
|
5
|
+
import { normalizeActionBattleAttackProfile } from "./attack-profile";
|
|
6
|
+
|
|
7
|
+
export type ActionBattleEnemyAttackProfileKey =
|
|
8
|
+
| "melee"
|
|
9
|
+
| "combo"
|
|
10
|
+
| "charged"
|
|
11
|
+
| "zone"
|
|
12
|
+
| "dashAttack";
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_ACTION_BATTLE_ENEMY_ATTACK_PROFILES: Record<
|
|
15
|
+
ActionBattleEnemyAttackProfileKey,
|
|
16
|
+
ActionBattleAttackProfile
|
|
17
|
+
> = {
|
|
18
|
+
melee: {
|
|
19
|
+
id: "enemy-melee",
|
|
20
|
+
startupMs: 120,
|
|
21
|
+
activeMs: 100,
|
|
22
|
+
recoveryMs: 220,
|
|
23
|
+
cooldownMs: 440,
|
|
24
|
+
reaction: {
|
|
25
|
+
invincibilityMs: 250,
|
|
26
|
+
hitstunMs: 120,
|
|
27
|
+
staggerPower: 1,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
combo: {
|
|
31
|
+
id: "enemy-combo",
|
|
32
|
+
startupMs: 80,
|
|
33
|
+
activeMs: 80,
|
|
34
|
+
recoveryMs: 140,
|
|
35
|
+
cooldownMs: 300,
|
|
36
|
+
reaction: {
|
|
37
|
+
invincibilityMs: 180,
|
|
38
|
+
hitstunMs: 90,
|
|
39
|
+
staggerPower: 0.75,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
charged: {
|
|
43
|
+
id: "enemy-charged",
|
|
44
|
+
startupMs: 800,
|
|
45
|
+
activeMs: 140,
|
|
46
|
+
recoveryMs: 320,
|
|
47
|
+
cooldownMs: 1260,
|
|
48
|
+
reaction: {
|
|
49
|
+
invincibilityMs: 350,
|
|
50
|
+
hitstunMs: 220,
|
|
51
|
+
staggerPower: 2,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
zone: {
|
|
55
|
+
id: "enemy-zone",
|
|
56
|
+
startupMs: 450,
|
|
57
|
+
activeMs: 180,
|
|
58
|
+
recoveryMs: 320,
|
|
59
|
+
cooldownMs: 950,
|
|
60
|
+
reaction: {
|
|
61
|
+
invincibilityMs: 300,
|
|
62
|
+
hitstunMs: 160,
|
|
63
|
+
staggerPower: 1.25,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
dashAttack: {
|
|
67
|
+
id: "enemy-dash",
|
|
68
|
+
startupMs: 180,
|
|
69
|
+
activeMs: 120,
|
|
70
|
+
recoveryMs: 260,
|
|
71
|
+
cooldownMs: 560,
|
|
72
|
+
reaction: {
|
|
73
|
+
invincibilityMs: 280,
|
|
74
|
+
hitstunMs: 150,
|
|
75
|
+
staggerPower: 1.2,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type ActionBattleEnemyAttackProfileMap = Partial<
|
|
81
|
+
Record<ActionBattleEnemyAttackProfileKey, ActionBattleAttackProfile>
|
|
82
|
+
>;
|
|
83
|
+
|
|
84
|
+
export type NormalizedActionBattleEnemyAttackProfileMap = Record<
|
|
85
|
+
ActionBattleEnemyAttackProfileKey,
|
|
86
|
+
NormalizedActionBattleAttackProfile
|
|
87
|
+
>;
|
|
88
|
+
|
|
89
|
+
export function normalizeActionBattleEnemyAttackProfiles(
|
|
90
|
+
overrides: ActionBattleEnemyAttackProfileMap = {}
|
|
91
|
+
): NormalizedActionBattleEnemyAttackProfileMap {
|
|
92
|
+
return Object.fromEntries(
|
|
93
|
+
Object.entries(DEFAULT_ACTION_BATTLE_ENEMY_ATTACK_PROFILES).map(
|
|
94
|
+
([key, defaultProfile]) => [
|
|
95
|
+
key,
|
|
96
|
+
normalizeActionBattleAttackProfile({
|
|
97
|
+
...defaultProfile,
|
|
98
|
+
...overrides[key as ActionBattleEnemyAttackProfileKey],
|
|
99
|
+
}),
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
) as NormalizedActionBattleEnemyAttackProfileMap;
|
|
103
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { resolveActionBattleWeaponAttackProfile } from "./equipment";
|
|
3
|
+
|
|
4
|
+
describe("equipment helpers", () => {
|
|
5
|
+
test("resolves attack profile from equipped weapon data", () => {
|
|
6
|
+
const attackProfile = {
|
|
7
|
+
id: "dagger",
|
|
8
|
+
startupMs: 40,
|
|
9
|
+
activeMs: 70,
|
|
10
|
+
recoveryMs: 120,
|
|
11
|
+
};
|
|
12
|
+
const entity = {
|
|
13
|
+
equipments: () => [{ id: () => "dagger" }],
|
|
14
|
+
databaseById: (id: string) =>
|
|
15
|
+
id === "dagger"
|
|
16
|
+
? {
|
|
17
|
+
_type: "weapon",
|
|
18
|
+
attackProfile,
|
|
19
|
+
}
|
|
20
|
+
: null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
expect(resolveActionBattleWeaponAttackProfile(entity)).toBe(attackProfile);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("ignores non-weapon equipment", () => {
|
|
27
|
+
const entity = {
|
|
28
|
+
equipments: () => [{ id: () => "ring" }],
|
|
29
|
+
databaseById: () => ({
|
|
30
|
+
_type: "armor",
|
|
31
|
+
attackProfile: { id: "invalid" },
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
expect(resolveActionBattleWeaponAttackProfile(entity)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { applyActionBattleHit } from "./hit";
|
|
3
|
+
import type { ActionBattleCombatSystem } from "./contracts";
|
|
4
|
+
import { setActionBattleInvincibility } from "./hit-reaction";
|
|
5
|
+
|
|
6
|
+
const entity = (hp = 100) => ({
|
|
7
|
+
hp,
|
|
8
|
+
x: () => 0,
|
|
9
|
+
y: () => 0,
|
|
10
|
+
knockback: vi.fn(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("applyActionBattleHit", () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("runs beforeHit before resolving damage", () => {
|
|
19
|
+
const calls: string[] = [];
|
|
20
|
+
const attacker = entity();
|
|
21
|
+
const target = entity();
|
|
22
|
+
const system: ActionBattleCombatSystem = {
|
|
23
|
+
resolveHitboxes: () => [],
|
|
24
|
+
resolveDamage: () => {
|
|
25
|
+
calls.push("damage");
|
|
26
|
+
return { damage: 12, defeated: false };
|
|
27
|
+
},
|
|
28
|
+
resolveKnockback: () => ({ force: 0, duration: 0 }),
|
|
29
|
+
hooks: {
|
|
30
|
+
beforeHit() {
|
|
31
|
+
calls.push("before");
|
|
32
|
+
},
|
|
33
|
+
afterHit() {
|
|
34
|
+
calls.push("after");
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = applyActionBattleHit(system, { attacker: attacker as any, target: target as any });
|
|
40
|
+
|
|
41
|
+
expect(result.damage).toBe(12);
|
|
42
|
+
expect(calls).toEqual(["before", "damage", "after"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("can cancel a hit before damage is resolved", () => {
|
|
46
|
+
const resolveDamage = vi.fn();
|
|
47
|
+
const attacker = entity();
|
|
48
|
+
const target = entity();
|
|
49
|
+
const system: ActionBattleCombatSystem = {
|
|
50
|
+
resolveHitboxes: () => [],
|
|
51
|
+
resolveDamage,
|
|
52
|
+
resolveKnockback: () => ({ force: 0, duration: 0 }),
|
|
53
|
+
hooks: {
|
|
54
|
+
beforeHit: () => false,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = applyActionBattleHit(system, { attacker: attacker as any, target: target as any });
|
|
59
|
+
|
|
60
|
+
expect(result.cancelled).toBe(true);
|
|
61
|
+
expect(resolveDamage).not.toHaveBeenCalled();
|
|
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
|
+
});
|
|
111
|
+
});
|
package/src/core/hit.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ActionBattleCombatSystem, ActionBattleHitContext, ActionBattleHitResult } from "./contracts";
|
|
2
|
+
import {
|
|
3
|
+
isActionBattleEntityInvincible,
|
|
4
|
+
setActionBattleInvincibility,
|
|
5
|
+
} from "./hit-reaction";
|
|
6
|
+
|
|
7
|
+
export const applyActionBattleHit = (
|
|
8
|
+
system: ActionBattleCombatSystem,
|
|
9
|
+
context: ActionBattleHitContext
|
|
10
|
+
): ActionBattleHitResult => {
|
|
11
|
+
let hitContext = { ...context };
|
|
12
|
+
const before = system.hooks?.beforeHit?.(hitContext);
|
|
13
|
+
if (before === false) {
|
|
14
|
+
return {
|
|
15
|
+
damage: 0,
|
|
16
|
+
knockbackForce: 0,
|
|
17
|
+
knockbackDuration: 0,
|
|
18
|
+
defeated: false,
|
|
19
|
+
attacker: hitContext.attacker,
|
|
20
|
+
target: hitContext.target,
|
|
21
|
+
cancelled: true,
|
|
22
|
+
metadata: hitContext.metadata,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (before) hitContext = before;
|
|
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
|
+
|
|
41
|
+
const damage =
|
|
42
|
+
hitContext.damage ??
|
|
43
|
+
system.resolveDamage({
|
|
44
|
+
attacker: hitContext.attacker,
|
|
45
|
+
target: hitContext.target,
|
|
46
|
+
skill: hitContext.skill,
|
|
47
|
+
pattern: hitContext.pattern,
|
|
48
|
+
});
|
|
49
|
+
hitContext.damage = damage;
|
|
50
|
+
|
|
51
|
+
const afterDamage = system.hooks?.afterDamage?.(hitContext);
|
|
52
|
+
if (afterDamage) hitContext = afterDamage;
|
|
53
|
+
|
|
54
|
+
const knockback =
|
|
55
|
+
hitContext.knockback ??
|
|
56
|
+
system.resolveKnockback({
|
|
57
|
+
attacker: hitContext.attacker,
|
|
58
|
+
target: hitContext.target,
|
|
59
|
+
damage,
|
|
60
|
+
});
|
|
61
|
+
hitContext.knockback = knockback;
|
|
62
|
+
|
|
63
|
+
if (!damage.defeated && knockback.force > 0 && knockback.direction) {
|
|
64
|
+
(hitContext.target as any).knockback?.(
|
|
65
|
+
knockback.direction,
|
|
66
|
+
knockback.force,
|
|
67
|
+
knockback.duration
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!damage.defeated && hitContext.reaction?.invincibilityMs) {
|
|
72
|
+
setActionBattleInvincibility(
|
|
73
|
+
hitContext.target,
|
|
74
|
+
hitContext.reaction.invincibilityMs
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result: ActionBattleHitResult = {
|
|
79
|
+
damage: damage.damage,
|
|
80
|
+
knockbackForce: knockback.force,
|
|
81
|
+
knockbackDuration: knockback.duration,
|
|
82
|
+
defeated: damage.defeated,
|
|
83
|
+
attacker: hitContext.attacker,
|
|
84
|
+
target: hitContext.target,
|
|
85
|
+
rawDamage: damage.raw,
|
|
86
|
+
reaction: hitContext.reaction,
|
|
87
|
+
metadata: hitContext.metadata,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
system.hooks?.afterHit?.(result);
|
|
91
|
+
return result;
|
|
92
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RpgEvent } from "@rpgjs/server";
|
|
2
|
+
import { BattleAi, type BattleAiOptions } from "../ai.server";
|
|
3
|
+
|
|
4
|
+
export interface ActionBattleEnemyPreset extends BattleAiOptions {
|
|
5
|
+
stats?: (event: RpgEvent) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ActionBattleEnemyPresetMap = Record<string, ActionBattleEnemyPreset>;
|
|
9
|
+
|
|
10
|
+
export const createActionEnemy = (
|
|
11
|
+
event: RpgEvent,
|
|
12
|
+
presetOrOptions: string | BattleAiOptions,
|
|
13
|
+
presets: ActionBattleEnemyPresetMap = {}
|
|
14
|
+
) => {
|
|
15
|
+
const options =
|
|
16
|
+
typeof presetOrOptions === "string"
|
|
17
|
+
? presets[presetOrOptions]
|
|
18
|
+
: presetOrOptions;
|
|
19
|
+
if (!options) {
|
|
20
|
+
throw new Error(`Action battle enemy preset not found: ${presetOrOptions}`);
|
|
21
|
+
}
|
|
22
|
+
const preset = options as ActionBattleEnemyPreset;
|
|
23
|
+
preset.stats?.(event);
|
|
24
|
+
return new BattleAi(event, options);
|
|
25
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -7,18 +7,111 @@ 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 { HitResult, ApplyHitHooks } from "./ai.server";
|
|
11
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";
|
|
22
|
+
export type {
|
|
23
|
+
ActionBattleAnimationContext,
|
|
24
|
+
ActionBattleAnimationEntity,
|
|
25
|
+
ActionBattleAnimationKey,
|
|
26
|
+
ActionBattleAnimationOptions,
|
|
27
|
+
ActionBattleAnimationResolver,
|
|
28
|
+
ActionBattleAnimationResult,
|
|
12
29
|
ActionBattleOptions,
|
|
13
30
|
ActionBattleActionBarData,
|
|
14
31
|
ActionBattleActionBarItem,
|
|
15
32
|
ActionBattleActionBarSkill,
|
|
16
33
|
ActionBattleSkillTargeting,
|
|
17
34
|
ActionBattleSkillTargetingResolver,
|
|
35
|
+
ActionBattleAttackOptions,
|
|
18
36
|
ActionBattleUiOptions,
|
|
19
37
|
ActionBattleUiActionBarOptions,
|
|
20
38
|
ActionBattleUiTargetingOptions,
|
|
39
|
+
ActionBattleAttackDirection,
|
|
40
|
+
ActionBattleAttackHitboxConfig,
|
|
41
|
+
ActionBattleAttackHitboxMap,
|
|
42
|
+
ActionBattleAttackHitPolicy,
|
|
43
|
+
ActionBattleAttackProfile,
|
|
44
|
+
ActionBattleDebugOptions,
|
|
45
|
+
ActionBattleHitReactionProfile,
|
|
46
|
+
NormalizedActionBattleHitReactionProfile,
|
|
47
|
+
NormalizedActionBattleAttackProfile,
|
|
48
|
+
ActionBattleCombatOptions,
|
|
49
|
+
ActionBattleSystemOptions,
|
|
50
|
+
ActionBattleAiSystemOptions,
|
|
21
51
|
} from "./types";
|
|
52
|
+
export type {
|
|
53
|
+
ActionBattleAiBehavior,
|
|
54
|
+
ActionBattleAiContext,
|
|
55
|
+
ActionBattleAiDecision,
|
|
56
|
+
ActionBattleAttackContext,
|
|
57
|
+
ActionBattleCombatSystem,
|
|
58
|
+
ActionBattleDamageContext,
|
|
59
|
+
ActionBattleDamageResult,
|
|
60
|
+
ActionBattleDirection,
|
|
61
|
+
ActionBattleEntity,
|
|
62
|
+
ActionBattleHitContext,
|
|
63
|
+
ActionBattleHitHooks,
|
|
64
|
+
ActionBattleHitResult,
|
|
65
|
+
ActionBattleHitbox,
|
|
66
|
+
ActionBattleKnockbackContext,
|
|
67
|
+
ActionBattleKnockbackResult,
|
|
68
|
+
ActionBattleSystems,
|
|
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";
|
|
97
|
+
export {
|
|
98
|
+
DEFAULT_ZELDA_PLAYER_HITBOXES,
|
|
99
|
+
createDefaultPlayerHitboxResolver,
|
|
100
|
+
defaultCombatSystem,
|
|
101
|
+
defaultEnemyBehaviors,
|
|
102
|
+
defaultKnockbackResolver,
|
|
103
|
+
defaultRpgjsDamageResolver,
|
|
104
|
+
} from "./core/defaults";
|
|
105
|
+
export {
|
|
106
|
+
createActionBattleSystems,
|
|
107
|
+
getActionBattleSystems,
|
|
108
|
+
} from "./core/context";
|
|
109
|
+
export { applyActionBattleHit } from "./core/hit";
|
|
110
|
+
export {
|
|
111
|
+
createActionEnemy,
|
|
112
|
+
type ActionBattleEnemyPreset,
|
|
113
|
+
type ActionBattleEnemyPresetMap,
|
|
114
|
+
} from "./enemies/factory";
|
|
22
115
|
|
|
23
116
|
// Server exports
|
|
24
117
|
export {
|