@rpgjs/action-battle 5.0.0-alpha.44 → 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,149 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionBattleAnimationContext,
|
|
3
|
+
ActionBattleAnimationEntity,
|
|
4
|
+
ActionBattleAnimationKey,
|
|
5
|
+
ActionBattleAnimationOptions,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_DIE_ANIMATION_DELAY_MS = 500;
|
|
9
|
+
|
|
10
|
+
export interface ResolvedActionBattleAnimation {
|
|
11
|
+
animationName: string;
|
|
12
|
+
graphic?: string | string[];
|
|
13
|
+
repeat: number;
|
|
14
|
+
waitEnd: boolean;
|
|
15
|
+
delayMs?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ActionBattleAnimationDefaults {
|
|
19
|
+
animationName?: string;
|
|
20
|
+
repeat?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_ANIMATION_BY_KEY: Record<ActionBattleAnimationKey, string> = {
|
|
24
|
+
attack: "attack",
|
|
25
|
+
hurt: "hurt",
|
|
26
|
+
die: "die",
|
|
27
|
+
castSkill: "skill",
|
|
28
|
+
castSpell: "skill",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getConfiguredAnimation = (
|
|
32
|
+
key: ActionBattleAnimationKey,
|
|
33
|
+
animations?: ActionBattleAnimationOptions,
|
|
34
|
+
) => {
|
|
35
|
+
if (!animations) {
|
|
36
|
+
return {
|
|
37
|
+
hasConfiguredAnimation: false,
|
|
38
|
+
configured: undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasConfiguredAnimation = Object.prototype.hasOwnProperty.call(
|
|
43
|
+
animations,
|
|
44
|
+
key,
|
|
45
|
+
);
|
|
46
|
+
if (hasConfiguredAnimation) {
|
|
47
|
+
return {
|
|
48
|
+
hasConfiguredAnimation,
|
|
49
|
+
configured: animations[key],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (key === "castSkill") {
|
|
54
|
+
const hasCastSpellAlias = Object.prototype.hasOwnProperty.call(
|
|
55
|
+
animations,
|
|
56
|
+
"castSpell",
|
|
57
|
+
);
|
|
58
|
+
return {
|
|
59
|
+
hasConfiguredAnimation: hasCastSpellAlias,
|
|
60
|
+
configured: animations.castSpell,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
hasConfiguredAnimation: false,
|
|
66
|
+
configured: undefined,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function resolveActionBattleAnimation(
|
|
71
|
+
key: ActionBattleAnimationKey,
|
|
72
|
+
entity: ActionBattleAnimationEntity,
|
|
73
|
+
animations?: ActionBattleAnimationOptions,
|
|
74
|
+
context?: ActionBattleAnimationContext,
|
|
75
|
+
defaults: ActionBattleAnimationDefaults = {}
|
|
76
|
+
): ResolvedActionBattleAnimation | null {
|
|
77
|
+
const defaultAnimationName =
|
|
78
|
+
defaults.animationName ?? DEFAULT_ANIMATION_BY_KEY[key];
|
|
79
|
+
const defaultRepeat = defaults.repeat ?? 1;
|
|
80
|
+
const { hasConfiguredAnimation, configured: configuredAnimation } =
|
|
81
|
+
getConfiguredAnimation(key, animations);
|
|
82
|
+
if (!hasConfiguredAnimation && key !== "attack") {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const configured = hasConfiguredAnimation
|
|
87
|
+
? configuredAnimation
|
|
88
|
+
: defaultAnimationName;
|
|
89
|
+
const result =
|
|
90
|
+
typeof configured === "function"
|
|
91
|
+
? configured(entity, context)
|
|
92
|
+
: configured;
|
|
93
|
+
|
|
94
|
+
if (result == null) return null;
|
|
95
|
+
|
|
96
|
+
if (typeof result === "string") {
|
|
97
|
+
return {
|
|
98
|
+
animationName: result,
|
|
99
|
+
repeat: defaultRepeat,
|
|
100
|
+
waitEnd: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const animationName = result.animationName ?? defaultAnimationName;
|
|
105
|
+
return {
|
|
106
|
+
animationName,
|
|
107
|
+
graphic: result.graphic,
|
|
108
|
+
repeat: result.repeat ?? defaultRepeat,
|
|
109
|
+
waitEnd: result.waitEnd ?? false,
|
|
110
|
+
delayMs: result.delayMs,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function playActionBattleAnimation(
|
|
115
|
+
key: ActionBattleAnimationKey,
|
|
116
|
+
entity: ActionBattleAnimationEntity,
|
|
117
|
+
animations?: ActionBattleAnimationOptions,
|
|
118
|
+
context?: ActionBattleAnimationContext,
|
|
119
|
+
defaults: ActionBattleAnimationDefaults = {}
|
|
120
|
+
): ResolvedActionBattleAnimation | null {
|
|
121
|
+
const animation = resolveActionBattleAnimation(
|
|
122
|
+
key,
|
|
123
|
+
entity,
|
|
124
|
+
animations,
|
|
125
|
+
context,
|
|
126
|
+
defaults
|
|
127
|
+
);
|
|
128
|
+
if (!animation) return null;
|
|
129
|
+
|
|
130
|
+
if (animation.graphic !== undefined) {
|
|
131
|
+
entity.setGraphicAnimation(
|
|
132
|
+
animation.animationName,
|
|
133
|
+
animation.graphic,
|
|
134
|
+
animation.repeat
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
entity.setGraphicAnimation(animation.animationName, animation.repeat);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return animation;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getActionBattleAnimationRemovalDelay(
|
|
144
|
+
animation: ResolvedActionBattleAnimation | null
|
|
145
|
+
): number {
|
|
146
|
+
if (!animation) return 0;
|
|
147
|
+
if (animation.delayMs !== undefined) return animation.delayMs;
|
|
148
|
+
return animation.waitEnd ? DEFAULT_DIE_ANIMATION_DELAY_MS : 0;
|
|
149
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,10 +1,114 @@
|
|
|
1
1
|
import { inject, PrebuiltComponentAnimations, RpgClient, RpgClientEngine, RpgGui } from "@rpgjs/client";
|
|
2
2
|
import { defineModule } from "@rpgjs/common";
|
|
3
|
+
// @ts-ignore CanvasEngine components are compiled by @canvasengine/compiler.
|
|
3
4
|
import ActionBarComponent from "./components/action-bar.ce";
|
|
5
|
+
// @ts-ignore CanvasEngine components are compiled by @canvasengine/compiler.
|
|
4
6
|
import TargetingOverlayComponent from "./components/targeting-overlay.ce";
|
|
5
|
-
|
|
7
|
+
// @ts-ignore CanvasEngine components are compiled by @canvasengine/compiler.
|
|
8
|
+
import AttackPreviewComponent from "./components/attack-preview.ce";
|
|
9
|
+
import {
|
|
10
|
+
setActionBattleOptions,
|
|
11
|
+
startAttackPreview,
|
|
12
|
+
stopAttackPreview,
|
|
13
|
+
} from "./ui/state";
|
|
6
14
|
import { ActionBattleOptions } from "./types";
|
|
7
15
|
import { normalizeActionBattleOptions } from "./config";
|
|
16
|
+
import { resolveActionBattleAnimation } from "./animations";
|
|
17
|
+
import { getNormalizedActionBattleAttackProfile } from "./core/attack-runtime";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_ATTACK_LOCK_DURATION_MS = 350;
|
|
20
|
+
|
|
21
|
+
const beginLocalPlayerAttackLock = (
|
|
22
|
+
engine: RpgClientEngine,
|
|
23
|
+
durationMs: number,
|
|
24
|
+
locks: { movement: boolean; direction: boolean }
|
|
25
|
+
): boolean => {
|
|
26
|
+
if (durationMs <= 0) return true;
|
|
27
|
+
|
|
28
|
+
const player = engine.scene?.getCurrentPlayer?.() as any;
|
|
29
|
+
if (!player) return true;
|
|
30
|
+
|
|
31
|
+
const runtimePlayer = player as any;
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (
|
|
34
|
+
typeof runtimePlayer.__actionBattleAttackLockedUntil === "number" &&
|
|
35
|
+
runtimePlayer.__actionBattleAttackLockedUntil > now
|
|
36
|
+
) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const lockId = (runtimePlayer.__actionBattleAttackLockId ?? 0) + 1;
|
|
41
|
+
runtimePlayer.__actionBattleAttackLockId = lockId;
|
|
42
|
+
runtimePlayer.__actionBattleAttackLockedUntil = now + durationMs;
|
|
43
|
+
|
|
44
|
+
const previousCanMove = player.canMove;
|
|
45
|
+
const previousDirectionFixed = player.directionFixed;
|
|
46
|
+
const previousAnimationFixed = player.animationFixed;
|
|
47
|
+
|
|
48
|
+
if (locks.movement) {
|
|
49
|
+
if (typeof engine.interruptCurrentPlayerMovement === "function") {
|
|
50
|
+
engine.interruptCurrentPlayerMovement(player);
|
|
51
|
+
} else {
|
|
52
|
+
(engine.scene as any)?.stopMovement?.(player);
|
|
53
|
+
}
|
|
54
|
+
player.canMove = false;
|
|
55
|
+
}
|
|
56
|
+
if (locks.direction) {
|
|
57
|
+
player.directionFixed = true;
|
|
58
|
+
}
|
|
59
|
+
player.animationFixed = true;
|
|
60
|
+
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
if (runtimePlayer.__actionBattleAttackLockId !== lockId) return;
|
|
63
|
+
runtimePlayer.__actionBattleAttackLockedUntil = 0;
|
|
64
|
+
player.canMove = previousCanMove;
|
|
65
|
+
player.directionFixed = previousDirectionFixed;
|
|
66
|
+
player.animationFixed = previousAnimationFixed;
|
|
67
|
+
}, durationMs);
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const resolveLocalPlayerDirection = (player: any) => {
|
|
73
|
+
if (typeof player.getDirection === "function") return player.getDirection();
|
|
74
|
+
if (typeof player.direction === "function") return player.direction();
|
|
75
|
+
return player.direction ?? "down";
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const playLocalPlayerAttackAnimation = (
|
|
79
|
+
player: any,
|
|
80
|
+
options: ActionBattleOptions
|
|
81
|
+
) => {
|
|
82
|
+
if (!player || typeof player.setAnimation !== "function") return;
|
|
83
|
+
const animation = resolveActionBattleAnimation(
|
|
84
|
+
"attack",
|
|
85
|
+
player,
|
|
86
|
+
options.animations
|
|
87
|
+
);
|
|
88
|
+
if (!animation) return;
|
|
89
|
+
|
|
90
|
+
if (animation.graphic !== undefined) {
|
|
91
|
+
player.setAnimation(
|
|
92
|
+
animation.animationName,
|
|
93
|
+
animation.graphic,
|
|
94
|
+
animation.repeat
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
player.setAnimation(animation.animationName, animation.repeat);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const showLocalAttackPreview = (player: any, options: ActionBattleOptions) => {
|
|
102
|
+
if (!player || options.attack?.showPreview === false) return;
|
|
103
|
+
const durationMs = Math.max(1, options.attack?.previewDurationMs ?? 180);
|
|
104
|
+
const previewId = startAttackPreview({
|
|
105
|
+
direction: resolveLocalPlayerDirection(player),
|
|
106
|
+
durationMs,
|
|
107
|
+
color: options.attack?.previewColor,
|
|
108
|
+
accentColor: options.attack?.previewAccentColor,
|
|
109
|
+
});
|
|
110
|
+
setTimeout(() => stopAttackPreview(previewId), durationMs);
|
|
111
|
+
};
|
|
8
112
|
|
|
9
113
|
export const createActionBattleClient = (
|
|
10
114
|
options: ActionBattleOptions = {}
|
|
@@ -13,6 +117,10 @@ export const createActionBattleClient = (
|
|
|
13
117
|
setActionBattleOptions(normalized);
|
|
14
118
|
const actionBarEnabled = normalized.ui?.actionBar?.enabled;
|
|
15
119
|
const targetingEnabled = normalized.ui?.targeting?.enabled;
|
|
120
|
+
const componentsInFront = [
|
|
121
|
+
...(targetingEnabled ? [TargetingOverlayComponent] : []),
|
|
122
|
+
AttackPreviewComponent,
|
|
123
|
+
];
|
|
16
124
|
const hitComponent = PrebuiltComponentAnimations?.Hit;
|
|
17
125
|
return defineModule<RpgClient>({
|
|
18
126
|
componentAnimations: hitComponent
|
|
@@ -36,7 +144,7 @@ export const createActionBattleClient = (
|
|
|
36
144
|
]
|
|
37
145
|
: [],
|
|
38
146
|
sprite: {
|
|
39
|
-
componentsInFront
|
|
147
|
+
componentsInFront,
|
|
40
148
|
},
|
|
41
149
|
sceneMap: {
|
|
42
150
|
onAfterLoading() {
|
|
@@ -45,6 +153,26 @@ export const createActionBattleClient = (
|
|
|
45
153
|
gui.display('action-battle-action-bar')
|
|
46
154
|
}
|
|
47
155
|
}
|
|
156
|
+
},
|
|
157
|
+
engine: {
|
|
158
|
+
onInput(engine: RpgClientEngine, { input }: { input: string }) {
|
|
159
|
+
if (input !== "action") return;
|
|
160
|
+
const player = engine.scene?.getCurrentPlayer?.() as any;
|
|
161
|
+
if (!player) return;
|
|
162
|
+
const attackProfile = getNormalizedActionBattleAttackProfile(normalized);
|
|
163
|
+
const lockDurationMs = Math.max(
|
|
164
|
+
0,
|
|
165
|
+
attackProfile.totalDurationMs ?? DEFAULT_ATTACK_LOCK_DURATION_MS
|
|
166
|
+
);
|
|
167
|
+
if (attackProfile.movementLock || attackProfile.directionLock) {
|
|
168
|
+
beginLocalPlayerAttackLock(engine, lockDurationMs, {
|
|
169
|
+
movement: attackProfile.movementLock,
|
|
170
|
+
direction: attackProfile.directionLock,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
playLocalPlayerAttackAnimation(player, normalized);
|
|
174
|
+
showLocalAttackPreview(player, normalized);
|
|
175
|
+
},
|
|
48
176
|
}
|
|
49
177
|
});
|
|
50
178
|
};
|
|
@@ -72,7 +72,6 @@
|
|
|
72
72
|
const engine = inject(RpgClientEngine);
|
|
73
73
|
const keyboardControls = engine.globalConfig.keyboardControls;
|
|
74
74
|
const { data, onInteraction, onBack } = defineProps();
|
|
75
|
-
const currentPlayer = engine.getCurrentPlayer();
|
|
76
75
|
const ACTION_BAR_SIZE = 10;
|
|
77
76
|
const SLOT_LABELS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
|
|
78
77
|
const SLOT_CONFIG_KEYS = [
|
|
@@ -107,10 +106,13 @@
|
|
|
107
106
|
playing: "default"
|
|
108
107
|
});
|
|
109
108
|
|
|
109
|
+
const resolveProp = (value) => typeof value === "function" ? value() : value;
|
|
110
|
+
const actionBarData = computed(() => resolveProp(data) || { items: [], skills: [] });
|
|
111
|
+
|
|
110
112
|
const actionBarSlots = computed(() => {
|
|
111
113
|
const entries = [];
|
|
112
114
|
if (showSkills()) {
|
|
113
|
-
|
|
115
|
+
(actionBarData().skills || []).forEach((skill, index) => {
|
|
114
116
|
entries.push({
|
|
115
117
|
type: "skill",
|
|
116
118
|
skill,
|
|
@@ -120,7 +122,7 @@
|
|
|
120
122
|
});
|
|
121
123
|
}
|
|
122
124
|
if (showItems()) {
|
|
123
|
-
|
|
125
|
+
(actionBarData().items || []).forEach((item, index) => {
|
|
124
126
|
entries.push({
|
|
125
127
|
type: "item",
|
|
126
128
|
skill: null,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<Container>
|
|
2
|
+
@if (shouldRender) {
|
|
3
|
+
<Graphics draw={drawSlash} />
|
|
4
|
+
}
|
|
5
|
+
</Container>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
import { computed, signal, tick } from "canvasengine";
|
|
9
|
+
import { inject, RpgClientEngine } from "@rpgjs/client";
|
|
10
|
+
import { actionBattleAttackPreviewState } from "../ui/state";
|
|
11
|
+
|
|
12
|
+
const { object } = defineProps();
|
|
13
|
+
const engine = inject(RpgClientEngine);
|
|
14
|
+
const now = signal(Date.now());
|
|
15
|
+
|
|
16
|
+
tick(() => {
|
|
17
|
+
if (actionBattleAttackPreviewState().active) {
|
|
18
|
+
now.set(Date.now());
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const isCurrentPlayer = computed(() => {
|
|
23
|
+
if (!object?.id) return false;
|
|
24
|
+
const idValue = typeof object.id === "function" ? object.id() : object.id;
|
|
25
|
+
return idValue === engine.playerId;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const preview = computed(() => actionBattleAttackPreviewState());
|
|
29
|
+
const progress = computed(() => {
|
|
30
|
+
const state = preview();
|
|
31
|
+
if (!state.active) return 1;
|
|
32
|
+
const elapsed = now() - state.startedAt;
|
|
33
|
+
return Math.max(0, Math.min(1, elapsed / state.durationMs));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const shouldRender = computed(() => {
|
|
37
|
+
const state = preview();
|
|
38
|
+
return isCurrentPlayer() && state.active && progress() < 1;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const getHitbox = () => object.hitbox?.() || { w: 32, h: 32 };
|
|
42
|
+
|
|
43
|
+
const drawRect = (g, x, y, width, height, color, alpha) => {
|
|
44
|
+
g.rect(x, y, width, height);
|
|
45
|
+
g.fill({ color, alpha });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const drawSlash = (g) => {
|
|
49
|
+
g.clear();
|
|
50
|
+
if (!shouldRender()) return;
|
|
51
|
+
|
|
52
|
+
const state = preview();
|
|
53
|
+
const p = progress();
|
|
54
|
+
const alpha = Math.sin(Math.PI * p);
|
|
55
|
+
if (alpha <= 0) return;
|
|
56
|
+
|
|
57
|
+
const hitbox = getHitbox();
|
|
58
|
+
const width = hitbox.w || 32;
|
|
59
|
+
const height = hitbox.h || 32;
|
|
60
|
+
const reach = 16 + 18 * p;
|
|
61
|
+
const thickness = 4 + 3 * (1 - p);
|
|
62
|
+
const color = state.color;
|
|
63
|
+
const accent = state.accentColor;
|
|
64
|
+
|
|
65
|
+
if (state.direction === "left") {
|
|
66
|
+
drawRect(g, -reach - 6, height * 0.24, reach, thickness, accent, alpha * 0.55);
|
|
67
|
+
drawRect(g, -reach - 10, height * 0.46, reach + 4, thickness + 2, color, alpha);
|
|
68
|
+
drawRect(g, -reach - 6, height * 0.70, reach, thickness, accent, alpha * 0.4);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (state.direction === "right") {
|
|
73
|
+
drawRect(g, width + 6, height * 0.24, reach, thickness, accent, alpha * 0.55);
|
|
74
|
+
drawRect(g, width + 6, height * 0.46, reach + 4, thickness + 2, color, alpha);
|
|
75
|
+
drawRect(g, width + 6, height * 0.70, reach, thickness, accent, alpha * 0.4);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (state.direction === "up") {
|
|
80
|
+
drawRect(g, width * 0.24, -reach - 6, thickness, reach, accent, alpha * 0.55);
|
|
81
|
+
drawRect(g, width * 0.46, -reach - 10, thickness + 2, reach + 4, color, alpha);
|
|
82
|
+
drawRect(g, width * 0.70, -reach - 6, thickness, reach, accent, alpha * 0.4);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
drawRect(g, width * 0.24, height + 6, thickness, reach, accent, alpha * 0.55);
|
|
87
|
+
drawRect(g, width * 0.46, height + 6, thickness + 2, reach + 4, color, alpha);
|
|
88
|
+
drawRect(g, width * 0.70, height + 6, thickness, reach, accent, alpha * 0.4);
|
|
89
|
+
};
|
|
90
|
+
</script>
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ActionBattleOptions } from "./types";
|
|
2
|
+
import { normalizeActionBattleAttackProfile } from "./core/attack-profile";
|
|
2
3
|
|
|
3
4
|
export const DEFAULT_ACTION_BATTLE_OPTIONS: ActionBattleOptions = {
|
|
4
5
|
ui: {
|
|
@@ -24,11 +25,33 @@ export const DEFAULT_ACTION_BATTLE_OPTIONS: ActionBattleOptions = {
|
|
|
24
25
|
affects: "events",
|
|
25
26
|
allowEmptyTarget: true,
|
|
26
27
|
},
|
|
28
|
+
attack: {
|
|
29
|
+
lockMovement: true,
|
|
30
|
+
lockDurationMs: 350,
|
|
31
|
+
showPreview: true,
|
|
32
|
+
previewDurationMs: 180,
|
|
33
|
+
previewColor: 0xfff3b0,
|
|
34
|
+
previewAccentColor: 0xffffff,
|
|
35
|
+
},
|
|
36
|
+
animations: {},
|
|
27
37
|
};
|
|
28
38
|
|
|
39
|
+
let currentActionBattleOptions: ActionBattleOptions =
|
|
40
|
+
DEFAULT_ACTION_BATTLE_OPTIONS;
|
|
41
|
+
|
|
29
42
|
export function normalizeActionBattleOptions(
|
|
30
43
|
options: ActionBattleOptions = {}
|
|
31
44
|
): ActionBattleOptions {
|
|
45
|
+
const attack = {
|
|
46
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.attack,
|
|
47
|
+
...options.attack,
|
|
48
|
+
};
|
|
49
|
+
const attackProfile = normalizeActionBattleAttackProfile(attack.profile, {
|
|
50
|
+
lockMovement: attack.lockMovement,
|
|
51
|
+
lockDurationMs: attack.lockDurationMs,
|
|
52
|
+
hitboxes: attack.hitboxes,
|
|
53
|
+
});
|
|
54
|
+
|
|
32
55
|
return {
|
|
33
56
|
ui: {
|
|
34
57
|
actionBar: {
|
|
@@ -52,5 +75,43 @@ export function normalizeActionBattleOptions(
|
|
|
52
75
|
...DEFAULT_ACTION_BATTLE_OPTIONS.targeting,
|
|
53
76
|
...options.targeting,
|
|
54
77
|
},
|
|
78
|
+
debug: {
|
|
79
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.debug,
|
|
80
|
+
...options.debug,
|
|
81
|
+
},
|
|
82
|
+
attack: {
|
|
83
|
+
...attack,
|
|
84
|
+
profile: attackProfile,
|
|
85
|
+
},
|
|
86
|
+
animations: {
|
|
87
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.animations,
|
|
88
|
+
...options.animations,
|
|
89
|
+
},
|
|
90
|
+
systems: {
|
|
91
|
+
combat: {
|
|
92
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.combat,
|
|
93
|
+
...options.systems?.combat,
|
|
94
|
+
hooks: {
|
|
95
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.combat?.hooks,
|
|
96
|
+
...options.systems?.combat?.hooks,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
ai: {
|
|
100
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.ai,
|
|
101
|
+
...options.systems?.ai,
|
|
102
|
+
behaviors: {
|
|
103
|
+
...DEFAULT_ACTION_BATTLE_OPTIONS.systems?.ai?.behaviors,
|
|
104
|
+
...options.systems?.ai?.behaviors,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
55
108
|
};
|
|
56
109
|
}
|
|
110
|
+
|
|
111
|
+
export function setActionBattleOptions(options: ActionBattleOptions) {
|
|
112
|
+
currentActionBattleOptions = options;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getActionBattleOptions(): ActionBattleOptions {
|
|
116
|
+
return currentActionBattleOptions;
|
|
117
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ACTION_BATTLE_ATTACK_PROFILE,
|
|
4
|
+
normalizeActionBattleAttackProfile,
|
|
5
|
+
} from "./attack-profile";
|
|
6
|
+
import { normalizeActionBattleOptions } from "../config";
|
|
7
|
+
import type { NormalizedActionBattleAttackProfile } from "../types";
|
|
8
|
+
|
|
9
|
+
describe("normalizeActionBattleAttackProfile", () => {
|
|
10
|
+
test("creates a default profile compatible with the legacy 350ms attack lock", () => {
|
|
11
|
+
const profile = normalizeActionBattleAttackProfile();
|
|
12
|
+
|
|
13
|
+
expect(profile).toEqual(DEFAULT_ACTION_BATTLE_ATTACK_PROFILE);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("derives recovery from the legacy lock duration when recovery is omitted", () => {
|
|
17
|
+
const profile = normalizeActionBattleAttackProfile(
|
|
18
|
+
{
|
|
19
|
+
startupMs: 80,
|
|
20
|
+
activeMs: 90,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
lockDurationMs: 400,
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(profile.recoveryMs).toBe(230);
|
|
28
|
+
expect(profile.totalDurationMs).toBe(400);
|
|
29
|
+
expect(profile.cooldownMs).toBe(400);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("keeps explicit timing, movement, hit policy, animation, and hitboxes", () => {
|
|
33
|
+
const hitboxes = {
|
|
34
|
+
right: { offsetX: 18, offsetY: -18, width: 42, height: 36 },
|
|
35
|
+
};
|
|
36
|
+
const profile = normalizeActionBattleAttackProfile({
|
|
37
|
+
id: "heavy-sword",
|
|
38
|
+
startupMs: 140,
|
|
39
|
+
activeMs: 100,
|
|
40
|
+
recoveryMs: 260,
|
|
41
|
+
cooldownMs: 650,
|
|
42
|
+
movementLock: false,
|
|
43
|
+
directionLock: false,
|
|
44
|
+
animationKey: "castSkill",
|
|
45
|
+
hitPolicy: "allowRepeatHits",
|
|
46
|
+
hitboxes,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(profile).toMatchObject({
|
|
50
|
+
id: "heavy-sword",
|
|
51
|
+
startupMs: 140,
|
|
52
|
+
activeMs: 100,
|
|
53
|
+
recoveryMs: 260,
|
|
54
|
+
cooldownMs: 650,
|
|
55
|
+
movementLock: false,
|
|
56
|
+
directionLock: false,
|
|
57
|
+
animationKey: "castSkill",
|
|
58
|
+
hitPolicy: "allowRepeatHits",
|
|
59
|
+
totalDurationMs: 500,
|
|
60
|
+
});
|
|
61
|
+
expect(profile.hitboxes).toBe(hitboxes);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("normalizes unsafe timing values to playable bounds", () => {
|
|
65
|
+
const profile = normalizeActionBattleAttackProfile({
|
|
66
|
+
startupMs: -20,
|
|
67
|
+
activeMs: 0,
|
|
68
|
+
recoveryMs: -10,
|
|
69
|
+
cooldownMs: -1,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(profile.startupMs).toBe(0);
|
|
73
|
+
expect(profile.activeMs).toBe(1);
|
|
74
|
+
expect(profile.recoveryMs).toBe(0);
|
|
75
|
+
expect(profile.cooldownMs).toBe(0);
|
|
76
|
+
expect(profile.totalDurationMs).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("normalizes attack.profile through action battle options", () => {
|
|
80
|
+
const options = normalizeActionBattleOptions({
|
|
81
|
+
attack: {
|
|
82
|
+
lockMovement: false,
|
|
83
|
+
lockDurationMs: 300,
|
|
84
|
+
profile: {
|
|
85
|
+
id: "quick-slash",
|
|
86
|
+
startupMs: 60,
|
|
87
|
+
activeMs: 80,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const profile = options.attack
|
|
92
|
+
?.profile as NormalizedActionBattleAttackProfile;
|
|
93
|
+
|
|
94
|
+
expect(profile).toMatchObject({
|
|
95
|
+
id: "quick-slash",
|
|
96
|
+
startupMs: 60,
|
|
97
|
+
activeMs: 80,
|
|
98
|
+
recoveryMs: 160,
|
|
99
|
+
cooldownMs: 300,
|
|
100
|
+
movementLock: false,
|
|
101
|
+
totalDurationMs: 300,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("keeps legacy lockDurationMs when no explicit profile is provided", () => {
|
|
106
|
+
const options = normalizeActionBattleOptions({
|
|
107
|
+
attack: {
|
|
108
|
+
lockDurationMs: 500,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const profile = options.attack
|
|
112
|
+
?.profile as NormalizedActionBattleAttackProfile;
|
|
113
|
+
|
|
114
|
+
expect(profile.totalDurationMs).toBe(500);
|
|
115
|
+
expect(profile.recoveryMs).toBe(380);
|
|
116
|
+
expect(profile.cooldownMs).toBe(500);
|
|
117
|
+
});
|
|
118
|
+
});
|