@rpgjs/action-battle 5.0.0-beta.11 → 5.0.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/client/ai.server.d.ts +45 -8
- package/dist/client/attack-input.d.ts +3 -0
- package/dist/client/core/action-use.d.ts +18 -0
- package/dist/client/core/ai-behavior-tree.d.ts +99 -0
- package/dist/client/core/attack-runtime.d.ts +2 -0
- package/dist/client/core/defaults.d.ts +2 -1
- package/dist/client/core/equipment.d.ts +1 -0
- package/dist/client/core/targets.d.ts +15 -0
- package/dist/client/enemies/factory.d.ts +2 -0
- package/dist/client/index.d.ts +12 -7
- package/dist/client/index.js +16 -11
- package/dist/client/index10.js +32 -56
- package/dist/client/index11.js +99 -52
- package/dist/client/index12.js +76 -103
- package/dist/client/index13.js +72 -135
- package/dist/client/index14.js +67 -23
- package/dist/client/index15.js +197 -63
- package/dist/client/index16.js +112 -1337
- package/dist/client/index17.js +193 -7
- package/dist/client/index18.js +32 -58
- package/dist/client/index19.js +70 -8
- package/dist/client/index20.js +57 -501
- package/dist/client/index21.js +69 -0
- package/dist/client/index22.js +225 -0
- package/dist/client/index23.js +16 -0
- package/dist/client/index24.js +25 -0
- package/dist/client/index25.js +107 -0
- package/dist/client/index26.js +1707 -0
- package/dist/client/index27.js +12 -0
- package/dist/client/index28.js +589 -0
- package/dist/client/index4.js +79 -38
- package/dist/client/index6.js +65 -306
- package/dist/client/index7.js +33 -33
- package/dist/client/index8.js +24 -100
- package/dist/client/index9.js +293 -61
- package/dist/client/locomotion.d.ts +16 -0
- package/dist/client/movement.d.ts +14 -0
- package/dist/client/server.d.ts +7 -3
- package/dist/client/ui.d.ts +22 -0
- package/dist/client/visual.d.ts +15 -0
- package/dist/server/ai.server.d.ts +45 -8
- package/dist/server/attack-input.d.ts +3 -0
- package/dist/server/core/action-use.d.ts +18 -0
- package/dist/server/core/ai-behavior-tree.d.ts +99 -0
- package/dist/server/core/attack-runtime.d.ts +2 -0
- package/dist/server/core/defaults.d.ts +2 -1
- package/dist/server/core/equipment.d.ts +1 -0
- package/dist/server/core/targets.d.ts +15 -0
- package/dist/server/enemies/factory.d.ts +2 -0
- package/dist/server/index.d.ts +12 -7
- package/dist/server/index.js +14 -9
- package/dist/server/index10.js +64 -1336
- package/dist/server/index11.js +33 -33
- package/dist/server/index13.js +66 -11
- package/dist/server/index14.js +206 -484
- package/dist/server/index15.js +15 -9
- package/dist/server/index16.js +26 -0
- package/dist/server/index17.js +25 -0
- package/dist/server/index18.js +107 -0
- package/dist/server/index19.js +1707 -0
- package/dist/server/index2.js +10 -2
- package/dist/server/index20.js +37 -0
- package/dist/server/index21.js +588 -0
- package/dist/server/index22.js +78 -0
- package/dist/server/index23.js +12 -0
- package/dist/server/index5.js +79 -38
- package/dist/server/index6.js +192 -129
- package/dist/server/index7.js +198 -24
- package/dist/server/index8.js +28 -66
- package/dist/server/index9.js +68 -51
- package/dist/server/locomotion.d.ts +16 -0
- package/dist/server/movement.d.ts +14 -0
- package/dist/server/server.d.ts +7 -3
- package/dist/server/ui.d.ts +22 -0
- package/dist/server/visual.d.ts +15 -0
- package/package.json +5 -5
- package/src/ai.server.spec.ts +233 -0
- package/src/ai.server.ts +627 -108
- package/src/animations.spec.ts +40 -0
- package/src/animations.ts +31 -9
- package/src/attack-input.spec.ts +51 -0
- package/src/attack-input.ts +59 -0
- package/src/client.ts +75 -62
- package/src/config.ts +84 -37
- package/src/core/action-use.spec.ts +317 -0
- package/src/core/action-use.ts +386 -0
- package/src/core/ai-behavior-tree.spec.ts +116 -0
- package/src/core/ai-behavior-tree.ts +272 -0
- package/src/core/attack-profile.spec.ts +46 -0
- package/src/core/attack-runtime.spec.ts +35 -0
- package/src/core/attack-runtime.ts +32 -0
- package/src/core/context.ts +9 -0
- package/src/core/contracts.ts +146 -1
- package/src/core/defaults.ts +56 -0
- package/src/core/equipment.ts +9 -5
- package/src/core/targets.spec.ts +112 -0
- package/src/core/targets.ts +147 -0
- package/src/enemies/factory.ts +8 -0
- package/src/index.ts +111 -2
- package/src/locomotion.spec.ts +51 -0
- package/src/locomotion.ts +48 -0
- package/src/movement.spec.ts +78 -0
- package/src/movement.ts +46 -0
- package/src/server.ts +242 -66
- package/src/types.ts +105 -35
- package/src/ui.ts +113 -0
- package/src/visual.spec.ts +166 -0
- package/src/visual.ts +285 -0
- package/README.md +0 -1242
package/src/ui.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { inject, RpgClientEngine } from "@rpgjs/client";
|
|
2
|
+
// @ts-ignore CanvasEngine components are compiled by @canvasengine/compiler.
|
|
3
|
+
import ActionBarComponent from "./components/action-bar.ce";
|
|
4
|
+
// @ts-ignore CanvasEngine components are compiled by @canvasengine/compiler.
|
|
5
|
+
import TargetingOverlayComponent from "./components/targeting-overlay.ce";
|
|
6
|
+
// @ts-ignore CanvasEngine components are compiled by @canvasengine/compiler.
|
|
7
|
+
import AttackPreviewComponent from "./components/attack-preview.ce";
|
|
8
|
+
import type {
|
|
9
|
+
ActionBattleUiActionBarOptions,
|
|
10
|
+
ActionBattleUiAttackPreviewOptions,
|
|
11
|
+
ActionBattleUiOptions,
|
|
12
|
+
ActionBattleUiTargetingOptions,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
export const ActionBattleUi = {
|
|
16
|
+
ActionBar: ActionBarComponent,
|
|
17
|
+
TargetingOverlay: TargetingOverlayComponent,
|
|
18
|
+
AttackPreview: AttackPreviewComponent,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface ResolvedActionBattleUi {
|
|
22
|
+
gui: Array<{ id: string; component: any; dependencies?: Function }>;
|
|
23
|
+
sprite: {
|
|
24
|
+
componentsInFront: any[];
|
|
25
|
+
componentsBehind: any[];
|
|
26
|
+
};
|
|
27
|
+
actionBar: ActionBattleUiActionBarOptions;
|
|
28
|
+
targeting: ActionBattleUiTargetingOptions;
|
|
29
|
+
attackPreview: ActionBattleUiAttackPreviewOptions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalizeToggle = <T extends { enabled?: boolean }>(
|
|
33
|
+
value: boolean | T | undefined,
|
|
34
|
+
defaults: T
|
|
35
|
+
): T => {
|
|
36
|
+
if (value === false) {
|
|
37
|
+
return { ...defaults, enabled: false };
|
|
38
|
+
}
|
|
39
|
+
if (value === true) {
|
|
40
|
+
return { ...defaults, enabled: true };
|
|
41
|
+
}
|
|
42
|
+
if (value === undefined) {
|
|
43
|
+
return { ...defaults };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...defaults,
|
|
47
|
+
...value,
|
|
48
|
+
enabled: value.enabled ?? defaults.enabled,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function createActionBattleUi(
|
|
53
|
+
input: "classic" | ActionBattleUiOptions = "classic"
|
|
54
|
+
): ActionBattleUiOptions {
|
|
55
|
+
if (input === "classic") {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
return input;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveActionBattleUi(options: ActionBattleUiOptions = {}): ResolvedActionBattleUi {
|
|
62
|
+
const actionBar = normalizeToggle(options.actionBar, {
|
|
63
|
+
enabled: false,
|
|
64
|
+
autoOpen: false,
|
|
65
|
+
mode: "both",
|
|
66
|
+
component: ActionBattleUi.ActionBar,
|
|
67
|
+
});
|
|
68
|
+
const targeting = normalizeToggle(options.targeting, {
|
|
69
|
+
enabled: true,
|
|
70
|
+
showGrid: true,
|
|
71
|
+
component: ActionBattleUi.TargetingOverlay,
|
|
72
|
+
colors: {
|
|
73
|
+
area: 0x2f9ef7,
|
|
74
|
+
edge: 0x1b6a98,
|
|
75
|
+
cursor: 0xffd166,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const attackPreview = normalizeToggle(options.attackPreview, {
|
|
79
|
+
enabled: true,
|
|
80
|
+
component: ActionBattleUi.AttackPreview,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const gui = [...(options.gui ?? [])];
|
|
84
|
+
if (actionBar.enabled && actionBar.component) {
|
|
85
|
+
gui.unshift({
|
|
86
|
+
id: "action-battle-action-bar",
|
|
87
|
+
component: actionBar.component,
|
|
88
|
+
dependencies: () => {
|
|
89
|
+
const engine = inject(RpgClientEngine);
|
|
90
|
+
return [engine.scene.currentPlayer];
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const configuredSpriteComponents = Array.isArray(options.spriteComponents)
|
|
96
|
+
? { front: options.spriteComponents, back: [] }
|
|
97
|
+
: options.spriteComponents ?? {};
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
gui,
|
|
101
|
+
sprite: {
|
|
102
|
+
componentsInFront: [
|
|
103
|
+
...(targeting.enabled && targeting.component ? [targeting.component] : []),
|
|
104
|
+
...(attackPreview.enabled && attackPreview.component ? [attackPreview.component] : []),
|
|
105
|
+
...(configuredSpriteComponents.front ?? []),
|
|
106
|
+
],
|
|
107
|
+
componentsBehind: configuredSpriteComponents.back ?? [],
|
|
108
|
+
},
|
|
109
|
+
actionBar,
|
|
110
|
+
targeting,
|
|
111
|
+
attackPreview,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ACTION_BATTLE_CLIENT_VISUAL_ID,
|
|
4
|
+
ACTION_BATTLE_HIT_FX_COMPONENT_ID,
|
|
5
|
+
createActionBattleVisual,
|
|
6
|
+
createActionBattleClientVisuals,
|
|
7
|
+
emitActionBattleClientVisual,
|
|
8
|
+
usesActionBattleFxVisual,
|
|
9
|
+
} from "./visual";
|
|
10
|
+
import { setActionBattleOptions } from "./config";
|
|
11
|
+
|
|
12
|
+
const createEntity = () => ({
|
|
13
|
+
flash: vi.fn(),
|
|
14
|
+
showHit: vi.fn(),
|
|
15
|
+
showComponentAnimation: vi.fn(),
|
|
16
|
+
setGraphicAnimation: vi.fn(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("action battle visual composer", () => {
|
|
20
|
+
test("classic hit uses the low-level flash and damage text primitives", () => {
|
|
21
|
+
const target = createEntity();
|
|
22
|
+
const visual = createActionBattleVisual("classic");
|
|
23
|
+
|
|
24
|
+
visual({
|
|
25
|
+
moment: "hit",
|
|
26
|
+
target,
|
|
27
|
+
damage: 12,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(target.flash).toHaveBeenCalledWith({
|
|
31
|
+
type: "tint",
|
|
32
|
+
tint: "red",
|
|
33
|
+
duration: 200,
|
|
34
|
+
cycles: 1,
|
|
35
|
+
});
|
|
36
|
+
expect(target.showHit).toHaveBeenCalledWith("-12");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("classic hit falls back to component animation when client objects have no showHit helper", () => {
|
|
40
|
+
const target = {
|
|
41
|
+
flash: vi.fn(),
|
|
42
|
+
showComponentAnimation: vi.fn(),
|
|
43
|
+
};
|
|
44
|
+
const visual = createActionBattleVisual("classic");
|
|
45
|
+
|
|
46
|
+
visual({
|
|
47
|
+
moment: "hit",
|
|
48
|
+
target,
|
|
49
|
+
damage: 12,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(target.flash).toHaveBeenCalledWith({
|
|
53
|
+
type: "tint",
|
|
54
|
+
tint: "red",
|
|
55
|
+
duration: 200,
|
|
56
|
+
cycles: 1,
|
|
57
|
+
});
|
|
58
|
+
expect(target.showComponentAnimation).toHaveBeenCalledWith("hit", {
|
|
59
|
+
text: "-12",
|
|
60
|
+
direction: undefined,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("fx hit keeps classic feedback and adds a CanvasEngine Fx component animation", () => {
|
|
65
|
+
const target = createEntity();
|
|
66
|
+
const visual = createActionBattleVisual("fx");
|
|
67
|
+
|
|
68
|
+
visual({
|
|
69
|
+
moment: "hit",
|
|
70
|
+
target,
|
|
71
|
+
damage: 9,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(target.showHit).toHaveBeenCalledWith("-9");
|
|
75
|
+
expect(target.showComponentAnimation).toHaveBeenCalledWith(
|
|
76
|
+
ACTION_BATTLE_HIT_FX_COMPONENT_ID,
|
|
77
|
+
expect.objectContaining({
|
|
78
|
+
name: "hitSpark",
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
expect(usesActionBattleFxVisual(visual)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("custom composer parts receive helpers", () => {
|
|
85
|
+
const target = createEntity();
|
|
86
|
+
const visual = createActionBattleVisual({
|
|
87
|
+
hit({ target }, fx) {
|
|
88
|
+
fx.component(target, "custom-hit", { name: "impactBurst" });
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
visual({
|
|
93
|
+
moment: "hit",
|
|
94
|
+
target,
|
|
95
|
+
damage: 3,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(target.showComponentAnimation).toHaveBeenCalledWith("custom-hit", {
|
|
99
|
+
name: "impactBurst",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("server visual emission sends one action-battle client visual packet", () => {
|
|
104
|
+
setActionBattleOptions({ visual: "classic" } as any);
|
|
105
|
+
const clientVisual = vi.fn();
|
|
106
|
+
const attacker = {
|
|
107
|
+
id: "player-1",
|
|
108
|
+
getCurrentMap: () => ({ clientVisual }),
|
|
109
|
+
};
|
|
110
|
+
const target = {
|
|
111
|
+
id: "enemy-1",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
emitActionBattleClientVisual({
|
|
115
|
+
moment: "hit",
|
|
116
|
+
entity: attacker,
|
|
117
|
+
target,
|
|
118
|
+
damage: 7,
|
|
119
|
+
result: {
|
|
120
|
+
damage: 7,
|
|
121
|
+
defeated: false,
|
|
122
|
+
attacker,
|
|
123
|
+
target,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(clientVisual).toHaveBeenCalledWith(
|
|
128
|
+
ACTION_BATTLE_CLIENT_VISUAL_ID,
|
|
129
|
+
expect.objectContaining({
|
|
130
|
+
moment: "hit",
|
|
131
|
+
objectId: "player-1",
|
|
132
|
+
sourceId: "player-1",
|
|
133
|
+
targetId: "enemy-1",
|
|
134
|
+
damage: 7,
|
|
135
|
+
result: expect.objectContaining({
|
|
136
|
+
damage: 7,
|
|
137
|
+
defeated: false,
|
|
138
|
+
}),
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("client visual handler replays configured action-battle visual locally", () => {
|
|
144
|
+
const target = createEntity();
|
|
145
|
+
const visuals = createActionBattleClientVisuals({
|
|
146
|
+
visual: "fx",
|
|
147
|
+
animations: {},
|
|
148
|
+
} as any);
|
|
149
|
+
|
|
150
|
+
visuals[ACTION_BATTLE_CLIENT_VISUAL_ID]({
|
|
151
|
+
target,
|
|
152
|
+
data: {
|
|
153
|
+
moment: "hit",
|
|
154
|
+
damage: 11,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(target.showHit).toHaveBeenCalledWith("-11");
|
|
159
|
+
expect(target.showComponentAnimation).toHaveBeenCalledWith(
|
|
160
|
+
ACTION_BATTLE_HIT_FX_COMPONENT_ID,
|
|
161
|
+
expect.objectContaining({
|
|
162
|
+
name: "hitSpark",
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
package/src/visual.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionBattleAnimationKey,
|
|
3
|
+
ActionBattleVisualComposer,
|
|
4
|
+
ActionBattleVisualContext,
|
|
5
|
+
ActionBattleVisualHelpers,
|
|
6
|
+
ActionBattleVisualInput,
|
|
7
|
+
ActionBattleVisualPart,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import { getActionBattleOptions } from "./config";
|
|
10
|
+
import { playActionBattleAnimation } from "./animations";
|
|
11
|
+
|
|
12
|
+
export const ACTION_BATTLE_CLIENT_VISUAL_ID = "action-battle.visual";
|
|
13
|
+
export const ACTION_BATTLE_HIT_FX_COMPONENT_ID = "action-battle-hit-fx";
|
|
14
|
+
|
|
15
|
+
type PreviewStarter = (
|
|
16
|
+
entity: any,
|
|
17
|
+
options?: Record<string, any>
|
|
18
|
+
) => void;
|
|
19
|
+
|
|
20
|
+
let previewStarter: PreviewStarter | undefined;
|
|
21
|
+
|
|
22
|
+
export function setActionBattlePreviewStarter(starter?: PreviewStarter) {
|
|
23
|
+
previewStarter = starter;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const entityId = (entity: any): string | undefined =>
|
|
27
|
+
typeof entity?.id === "string" ? entity.id : undefined;
|
|
28
|
+
|
|
29
|
+
const serializeSkill = (skill: any) => {
|
|
30
|
+
const id = skill?.id;
|
|
31
|
+
if (typeof id === "string") return { id };
|
|
32
|
+
if (typeof id === "function") {
|
|
33
|
+
const value = id.call(skill);
|
|
34
|
+
if (typeof value === "string") return { id: value };
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const serializeResult = (result: any) => {
|
|
40
|
+
if (!result) return undefined;
|
|
41
|
+
return {
|
|
42
|
+
damage: result.damage,
|
|
43
|
+
knockbackForce: result.knockbackForce,
|
|
44
|
+
knockbackDuration: result.knockbackDuration,
|
|
45
|
+
defeated: result.defeated,
|
|
46
|
+
attackerId: entityId(result.attacker),
|
|
47
|
+
targetId: entityId(result.target),
|
|
48
|
+
rawDamage: result.rawDamage,
|
|
49
|
+
reaction: result.reaction,
|
|
50
|
+
cancelled: result.cancelled,
|
|
51
|
+
metadata: result.metadata,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const serializeActionBattleVisualContext = (
|
|
56
|
+
context: ActionBattleVisualContext
|
|
57
|
+
) => ({
|
|
58
|
+
moment: context.moment,
|
|
59
|
+
objectId: entityId(context.entity),
|
|
60
|
+
sourceId: entityId(context.attacker ?? context.entity),
|
|
61
|
+
targetId: entityId(context.target),
|
|
62
|
+
damage: context.damage,
|
|
63
|
+
defeated: context.defeated,
|
|
64
|
+
result: serializeResult(context.result),
|
|
65
|
+
skill: serializeSkill(context.skill),
|
|
66
|
+
pattern: context.pattern,
|
|
67
|
+
animations: context.animations,
|
|
68
|
+
animationDefaults: context.animationDefaults,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export function emitActionBattleClientVisual(
|
|
72
|
+
context: ActionBattleVisualContext
|
|
73
|
+
) {
|
|
74
|
+
if (getActionBattleOptions().visual === "none") return;
|
|
75
|
+
const anchor = context.entity ?? context.target ?? context.attacker;
|
|
76
|
+
const map = anchor?.getCurrentMap?.();
|
|
77
|
+
if (!map?.clientVisual) return;
|
|
78
|
+
map.clientVisual(
|
|
79
|
+
ACTION_BATTLE_CLIENT_VISUAL_ID,
|
|
80
|
+
serializeActionBattleVisualContext(context)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createActionBattleClientVisuals(
|
|
85
|
+
options = getActionBattleOptions()
|
|
86
|
+
) {
|
|
87
|
+
return {
|
|
88
|
+
[ACTION_BATTLE_CLIENT_VISUAL_ID]: (context: any) => {
|
|
89
|
+
const data = context.data ?? {};
|
|
90
|
+
playActionBattleVisual(options.visual, {
|
|
91
|
+
moment: data.moment,
|
|
92
|
+
entity: context.object ?? context.source ?? context.target,
|
|
93
|
+
target: context.target,
|
|
94
|
+
attacker: context.source,
|
|
95
|
+
damage: data.damage,
|
|
96
|
+
defeated: data.defeated,
|
|
97
|
+
result: data.result,
|
|
98
|
+
skill: data.skill,
|
|
99
|
+
pattern: data.pattern,
|
|
100
|
+
animations: data.animations ?? options.animations,
|
|
101
|
+
animationDefaults: data.animationDefaults,
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const callGraphic = (
|
|
108
|
+
entity: any,
|
|
109
|
+
keyOrOptions: ActionBattleAnimationKey | any,
|
|
110
|
+
context?: ActionBattleVisualContext
|
|
111
|
+
) => {
|
|
112
|
+
if (!entity || keyOrOptions == null) return;
|
|
113
|
+
|
|
114
|
+
if (typeof keyOrOptions === "string") {
|
|
115
|
+
playActionBattleAnimation(
|
|
116
|
+
keyOrOptions as ActionBattleAnimationKey,
|
|
117
|
+
entity,
|
|
118
|
+
context?.animations ?? getActionBattleOptions().animations,
|
|
119
|
+
{
|
|
120
|
+
attacker: context?.attacker,
|
|
121
|
+
target: context?.target,
|
|
122
|
+
skill: context?.skill,
|
|
123
|
+
},
|
|
124
|
+
context?.animationDefaults
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const animationName = keyOrOptions.animationName;
|
|
130
|
+
if (!animationName) return;
|
|
131
|
+
const repeat = keyOrOptions.repeat ?? 1;
|
|
132
|
+
const graphic = keyOrOptions.graphic;
|
|
133
|
+
if (typeof entity.setGraphicAnimation === "function") {
|
|
134
|
+
if (graphic !== undefined) {
|
|
135
|
+
entity.setGraphicAnimation(animationName, graphic, repeat);
|
|
136
|
+
} else {
|
|
137
|
+
entity.setGraphicAnimation(animationName, repeat);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (typeof entity.setAnimation === "function") {
|
|
142
|
+
if (graphic !== undefined) {
|
|
143
|
+
entity.setAnimation(animationName, graphic, repeat);
|
|
144
|
+
} else {
|
|
145
|
+
entity.setAnimation(animationName, repeat);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const createHelpers = (context: ActionBattleVisualContext): ActionBattleVisualHelpers => ({
|
|
151
|
+
graphic(entity, keyOrOptions) {
|
|
152
|
+
callGraphic(entity, keyOrOptions, context);
|
|
153
|
+
},
|
|
154
|
+
flash(entity, options = {}) {
|
|
155
|
+
entity?.flash?.({
|
|
156
|
+
type: "tint",
|
|
157
|
+
tint: "red",
|
|
158
|
+
duration: 200,
|
|
159
|
+
cycles: 1,
|
|
160
|
+
...options,
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
damageText(entity, damageOrText) {
|
|
164
|
+
if (typeof damageOrText === "string") {
|
|
165
|
+
if (entity?.showHit) {
|
|
166
|
+
entity.showHit(damageOrText);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
entity?.showComponentAnimation?.("hit", {
|
|
170
|
+
text: damageOrText,
|
|
171
|
+
direction: entity?.direction?.() ?? entity?.direction,
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const damage = damageOrText ?? context.damage ?? context.result?.damage;
|
|
176
|
+
if (damage === undefined) return;
|
|
177
|
+
const text = `-${damage}`;
|
|
178
|
+
if (entity?.showHit) {
|
|
179
|
+
entity.showHit(text);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
entity?.showComponentAnimation?.("hit", {
|
|
183
|
+
text,
|
|
184
|
+
direction: entity?.direction?.() ?? entity?.direction,
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
component(entity, id, params = {}) {
|
|
188
|
+
entity?.showComponentAnimation?.(id, params);
|
|
189
|
+
},
|
|
190
|
+
preview(entity, options = {}) {
|
|
191
|
+
previewStarter?.(entity, options);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const classicParts: Partial<Record<ActionBattleVisualContext["moment"], ActionBattleVisualPart>> = {
|
|
196
|
+
attack({ entity }, fx) {
|
|
197
|
+
fx.graphic(entity, "attack");
|
|
198
|
+
},
|
|
199
|
+
castSkill({ entity }, fx) {
|
|
200
|
+
fx.graphic(entity, "castSkill");
|
|
201
|
+
},
|
|
202
|
+
preview({ entity }, fx) {
|
|
203
|
+
fx.preview(entity);
|
|
204
|
+
},
|
|
205
|
+
hit({ target, damage }, fx) {
|
|
206
|
+
fx.flash(target);
|
|
207
|
+
fx.damageText(target, damage);
|
|
208
|
+
},
|
|
209
|
+
hurt({ entity, target }, fx) {
|
|
210
|
+
const hurtTarget = target ?? entity;
|
|
211
|
+
fx.flash(hurtTarget);
|
|
212
|
+
fx.damageText(hurtTarget);
|
|
213
|
+
fx.graphic(hurtTarget, "hurt");
|
|
214
|
+
},
|
|
215
|
+
defeat({ entity, target }, fx) {
|
|
216
|
+
fx.graphic(target ?? entity, "die");
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const fxParts: Partial<Record<ActionBattleVisualContext["moment"], ActionBattleVisualPart>> = {
|
|
221
|
+
...classicParts,
|
|
222
|
+
hit(context, fx) {
|
|
223
|
+
classicParts.hit?.(context, fx);
|
|
224
|
+
fx.component(context.target, ACTION_BATTLE_HIT_FX_COMPONENT_ID, {
|
|
225
|
+
name: "hitSpark",
|
|
226
|
+
scale: 0.8,
|
|
227
|
+
zIndex: 1000,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
hurt(context, fx) {
|
|
231
|
+
classicParts.hurt?.(context, fx);
|
|
232
|
+
fx.component(context.target ?? context.entity, ACTION_BATTLE_HIT_FX_COMPONENT_ID, {
|
|
233
|
+
name: "hitSpark",
|
|
234
|
+
scale: 0.8,
|
|
235
|
+
zIndex: 1000,
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const resolveParts = (
|
|
241
|
+
input: ActionBattleVisualInput | undefined
|
|
242
|
+
): Partial<Record<ActionBattleVisualContext["moment"], ActionBattleVisualPart>> | null => {
|
|
243
|
+
const visual = input ?? "classic";
|
|
244
|
+
if (visual === "none") return null;
|
|
245
|
+
if (visual === "classic") return classicParts;
|
|
246
|
+
if (visual === "fx") return fxParts;
|
|
247
|
+
if (typeof visual === "function") return null;
|
|
248
|
+
return visual;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export function createActionBattleVisual(
|
|
252
|
+
input: ActionBattleVisualInput = "classic"
|
|
253
|
+
): ActionBattleVisualComposer {
|
|
254
|
+
if (typeof input === "function") {
|
|
255
|
+
return input;
|
|
256
|
+
}
|
|
257
|
+
const parts = resolveParts(input);
|
|
258
|
+
const composer: ActionBattleVisualComposer = (context) => {
|
|
259
|
+
if (!parts) return;
|
|
260
|
+
const part = parts[context.moment];
|
|
261
|
+
if (!part) return;
|
|
262
|
+
part(context, createHelpers(context));
|
|
263
|
+
};
|
|
264
|
+
(composer as any).__actionBattleUsesFx = input === "fx";
|
|
265
|
+
return composer;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export const createClassicActionBattleVisual = () =>
|
|
269
|
+
createActionBattleVisual("classic");
|
|
270
|
+
|
|
271
|
+
export const createFxActionBattleVisual = () =>
|
|
272
|
+
createActionBattleVisual("fx");
|
|
273
|
+
|
|
274
|
+
export function playActionBattleVisual(
|
|
275
|
+
visual: ActionBattleVisualInput | ActionBattleVisualComposer | undefined,
|
|
276
|
+
context: ActionBattleVisualContext
|
|
277
|
+
) {
|
|
278
|
+
const composer =
|
|
279
|
+
typeof visual === "function" ? visual : createActionBattleVisual(visual);
|
|
280
|
+
composer(context);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function usesActionBattleFxVisual(visual: ActionBattleVisualInput | undefined): boolean {
|
|
284
|
+
return visual === "fx" || Boolean((visual as any)?.__actionBattleUsesFx);
|
|
285
|
+
}
|