@aura3d/engine 1.0.3 → 1.0.5
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 +309 -7
- package/dist/animation/AnimationClipEvents.d.ts +57 -0
- package/dist/animation/AnimationClipEvents.d.ts.map +1 -0
- package/dist/animation/AnimationClipEvents.js +171 -0
- package/dist/animation/AnimationClipEvents.js.map +1 -0
- package/dist/animation/AnimationClipRegistry.d.ts +76 -0
- package/dist/animation/AnimationClipRegistry.d.ts.map +1 -0
- package/dist/animation/AnimationClipRegistry.js +130 -0
- package/dist/animation/AnimationClipRegistry.js.map +1 -0
- package/dist/animation/AnimationController.d.ts +168 -0
- package/dist/animation/AnimationController.d.ts.map +1 -0
- package/dist/animation/AnimationController.js +619 -0
- package/dist/animation/AnimationController.js.map +1 -0
- package/dist/animation/HumanoidRetargeting.d.ts +76 -0
- package/dist/animation/HumanoidRetargeting.d.ts.map +1 -0
- package/dist/animation/HumanoidRetargeting.js +331 -0
- package/dist/animation/HumanoidRetargeting.js.map +1 -0
- package/dist/animation/browser-index.d.ts +18 -0
- package/dist/animation/browser-index.d.ts.map +1 -1
- package/dist/animation/browser-index.js +13 -0
- package/dist/animation/browser-index.js.map +1 -1
- package/dist/animation/index.d.ts +16 -1
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/animation/index.js +11 -1
- package/dist/animation/index.js.map +1 -1
- package/dist/animation/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -1
- package/dist/animation/threejs-compatibility/AnimationDiagnostics.js +3 -5
- package/dist/animation/threejs-compatibility/AnimationDiagnostics.js.map +1 -1
- package/dist/assets/GLTFAnimationRuntime.js +1 -1
- package/dist/assets/GLTFLoader.js +1 -1
- package/dist/aura3d-cli/cli.js +194 -8
- package/dist/aura3d-cli/cli.js.map +1 -1
- package/dist/aura3d-cli/index.d.ts +280 -3
- package/dist/aura3d-cli/index.d.ts.map +1 -1
- package/dist/aura3d-cli/index.js +886 -4
- package/dist/aura3d-cli/index.js.map +1 -1
- package/dist/aura3d-cli/pull-bridge.d.ts +95 -0
- package/dist/aura3d-cli/pull-bridge.d.ts.map +1 -0
- package/dist/aura3d-cli/pull-bridge.js +247 -0
- package/dist/aura3d-cli/pull-bridge.js.map +1 -0
- package/dist/create-aura3d/index.d.ts +1 -1
- package/dist/create-aura3d/index.d.ts.map +1 -1
- package/dist/create-aura3d/index.js +9 -2
- package/dist/create-aura3d/index.js.map +1 -1
- package/dist/editor-runtime/ProjectSerializer.d.ts +74 -1
- package/dist/editor-runtime/ProjectSerializer.d.ts.map +1 -1
- package/dist/editor-runtime/ProjectSerializer.js +123 -6
- package/dist/editor-runtime/ProjectSerializer.js.map +1 -1
- package/dist/editor-runtime/TimelineModel.d.ts +18 -0
- package/dist/editor-runtime/TimelineModel.d.ts.map +1 -1
- package/dist/editor-runtime/TimelineModel.js +67 -3
- package/dist/editor-runtime/TimelineModel.js.map +1 -1
- package/dist/editor-runtime/TimelineRuntimeBridge.d.ts +98 -0
- package/dist/editor-runtime/TimelineRuntimeBridge.d.ts.map +1 -0
- package/dist/editor-runtime/TimelineRuntimeBridge.js +186 -0
- package/dist/editor-runtime/TimelineRuntimeBridge.js.map +1 -0
- package/dist/editor-runtime/index.d.ts +3 -1
- package/dist/editor-runtime/index.d.ts.map +1 -1
- package/dist/editor-runtime/index.js +1 -0
- package/dist/editor-runtime/index.js.map +1 -1
- package/dist/engine/agent-api/AnimationController.d.ts +607 -0
- package/dist/engine/agent-api/AnimationController.d.ts.map +1 -0
- package/dist/engine/agent-api/AnimationController.js +2192 -0
- package/dist/engine/agent-api/AnimationController.js.map +1 -0
- package/dist/engine/agent-api/AssetEvidence.d.ts +88 -0
- package/dist/engine/agent-api/AssetEvidence.d.ts.map +1 -0
- package/dist/engine/agent-api/AssetEvidence.js +157 -0
- package/dist/engine/agent-api/AssetEvidence.js.map +1 -0
- package/dist/engine/agent-api/AuraAppHandle.d.ts +55 -0
- package/dist/engine/agent-api/AuraAppHandle.d.ts.map +1 -0
- package/dist/engine/agent-api/AuraAppHandle.js +15 -0
- package/dist/engine/agent-api/AuraAppHandle.js.map +1 -0
- package/dist/engine/agent-api/AuraVoiceBridge.d.ts +96 -0
- package/dist/engine/agent-api/AuraVoiceBridge.d.ts.map +1 -0
- package/dist/engine/agent-api/AuraVoiceBridge.js +370 -0
- package/dist/engine/agent-api/AuraVoiceBridge.js.map +1 -0
- package/dist/engine/agent-api/CartoonDirector.d.ts +95 -0
- package/dist/engine/agent-api/CartoonDirector.d.ts.map +1 -0
- package/dist/engine/agent-api/CartoonDirector.js +342 -0
- package/dist/engine/agent-api/CartoonDirector.js.map +1 -0
- package/dist/engine/agent-api/CartoonPerformance.d.ts +149 -0
- package/dist/engine/agent-api/CartoonPerformance.d.ts.map +1 -0
- package/dist/engine/agent-api/CartoonPerformance.js +317 -0
- package/dist/engine/agent-api/CartoonPerformance.js.map +1 -0
- package/dist/engine/agent-api/CartoonRenderQueue.d.ts +132 -0
- package/dist/engine/agent-api/CartoonRenderQueue.d.ts.map +1 -0
- package/dist/engine/agent-api/CartoonRenderQueue.js +385 -0
- package/dist/engine/agent-api/CartoonRenderQueue.js.map +1 -0
- package/dist/engine/agent-api/CharacterAssembly.d.ts +126 -0
- package/dist/engine/agent-api/CharacterAssembly.d.ts.map +1 -0
- package/dist/engine/agent-api/CharacterAssembly.js +280 -0
- package/dist/engine/agent-api/CharacterAssembly.js.map +1 -0
- package/dist/engine/agent-api/DialoguePerformance.d.ts +150 -0
- package/dist/engine/agent-api/DialoguePerformance.d.ts.map +1 -0
- package/dist/engine/agent-api/DialoguePerformance.js +335 -0
- package/dist/engine/agent-api/DialoguePerformance.js.map +1 -0
- package/dist/engine/agent-api/FrameLoop.d.ts +70 -0
- package/dist/engine/agent-api/FrameLoop.d.ts.map +1 -0
- package/dist/engine/agent-api/FrameLoop.js +165 -0
- package/dist/engine/agent-api/FrameLoop.js.map +1 -0
- package/dist/engine/agent-api/GameAssetValidation.d.ts +279 -0
- package/dist/engine/agent-api/GameAssetValidation.d.ts.map +1 -0
- package/dist/engine/agent-api/GameAssetValidation.js +719 -0
- package/dist/engine/agent-api/GameAssetValidation.js.map +1 -0
- package/dist/engine/agent-api/GameEvidence.d.ts +148 -0
- package/dist/engine/agent-api/GameEvidence.d.ts.map +1 -0
- package/dist/engine/agent-api/GameEvidence.js +269 -0
- package/dist/engine/agent-api/GameEvidence.js.map +1 -0
- package/dist/engine/agent-api/GameRuntime.d.ts +931 -0
- package/dist/engine/agent-api/GameRuntime.d.ts.map +1 -0
- package/dist/engine/agent-api/GameRuntime.js +2229 -0
- package/dist/engine/agent-api/GameRuntime.js.map +1 -0
- package/dist/engine/agent-api/GameSceneBridge.d.ts +54 -0
- package/dist/engine/agent-api/GameSceneBridge.d.ts.map +1 -0
- package/dist/engine/agent-api/GameSceneBridge.js +110 -0
- package/dist/engine/agent-api/GameSceneBridge.js.map +1 -0
- package/dist/engine/agent-api/PromptAnimationContract.d.ts +278 -0
- package/dist/engine/agent-api/PromptAnimationContract.d.ts.map +1 -0
- package/dist/engine/agent-api/PromptAnimationContract.js +238 -0
- package/dist/engine/agent-api/PromptAnimationContract.js.map +1 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.d.ts +183 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.d.ts.map +1 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.js +454 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.js.map +1 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.d.ts +100 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.d.ts.map +1 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.js +36 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.js.map +1 -0
- package/dist/engine/agent-api/ShotTimeline.d.ts +179 -0
- package/dist/engine/agent-api/ShotTimeline.d.ts.map +1 -0
- package/dist/engine/agent-api/ShotTimeline.js +264 -0
- package/dist/engine/agent-api/ShotTimeline.js.map +1 -0
- package/dist/engine/agent-api/VisemeController.d.ts +89 -0
- package/dist/engine/agent-api/VisemeController.d.ts.map +1 -0
- package/dist/engine/agent-api/VisemeController.js +207 -0
- package/dist/engine/agent-api/VisemeController.js.map +1 -0
- package/dist/engine/agent-api/game-kits/fighting.d.ts +123 -0
- package/dist/engine/agent-api/game-kits/fighting.d.ts.map +1 -0
- package/dist/engine/agent-api/game-kits/fighting.js +483 -0
- package/dist/engine/agent-api/game-kits/fighting.js.map +1 -0
- package/dist/engine/agent-api/game-kits/index.d.ts +15 -0
- package/dist/engine/agent-api/game-kits/index.d.ts.map +1 -0
- package/dist/engine/agent-api/game-kits/index.js +6 -0
- package/dist/engine/agent-api/game-kits/index.js.map +1 -0
- package/dist/engine/agent-api/humanoid-walk-runtime.d.ts +1 -0
- package/dist/engine/agent-api/humanoid-walk-runtime.d.ts.map +1 -1
- package/dist/engine/agent-api/index.d.ts +487 -1
- package/dist/engine/agent-api/index.d.ts.map +1 -1
- package/dist/engine/agent-api/index.js +740 -6
- package/dist/engine/agent-api/index.js.map +1 -1
- package/dist/engine/agent-api/product-viewer-runtime.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/physics/CollisionVolumes.d.ts +57 -0
- package/dist/physics/CollisionVolumes.d.ts.map +1 -0
- package/dist/physics/CollisionVolumes.js +159 -0
- package/dist/physics/CollisionVolumes.js.map +1 -0
- package/dist/physics/HitboxWorld.d.ts +229 -0
- package/dist/physics/HitboxWorld.d.ts.map +1 -0
- package/dist/physics/HitboxWorld.js +640 -0
- package/dist/physics/HitboxWorld.js.map +1 -0
- package/dist/physics/KinematicBody.d.ts +157 -0
- package/dist/physics/KinematicBody.d.ts.map +1 -0
- package/dist/physics/KinematicBody.js +405 -0
- package/dist/physics/KinematicBody.js.map +1 -0
- package/dist/physics/KinematicWorld.d.ts +58 -0
- package/dist/physics/KinematicWorld.d.ts.map +1 -0
- package/dist/physics/KinematicWorld.js +246 -0
- package/dist/physics/KinematicWorld.js.map +1 -0
- package/dist/physics/index.d.ts +4 -0
- package/dist/physics/index.d.ts.map +1 -1
- package/dist/physics/index.js +4 -0
- package/dist/physics/index.js.map +1 -1
- package/dist/rendering/ForwardPass.js +2 -2
- package/dist/rendering/ShaderLibrary.js +2 -2
- package/dist/rendering/SkinnedLitMaterial.js +3 -3
- package/dist/rendering/SkinnedUnlitMaterial.js +3 -3
- package/dist/scene/Renderable.js +2 -2
- package/dist/scripting/VisualGraph.d.ts +2 -1
- package/dist/scripting/VisualGraph.d.ts.map +1 -1
- package/dist/scripting/VisualGraph.js +118 -1
- package/dist/scripting/VisualGraph.js.map +1 -1
- package/dist/scripting/VisualGraphContext.d.ts +123 -0
- package/dist/scripting/VisualGraphContext.d.ts.map +1 -0
- package/dist/scripting/VisualGraphContext.js +2 -0
- package/dist/scripting/VisualGraphContext.js.map +1 -0
- package/dist/scripting/VisualGraphExecutor.d.ts +6 -1
- package/dist/scripting/VisualGraphExecutor.d.ts.map +1 -1
- package/dist/scripting/VisualGraphExecutor.js +364 -7
- package/dist/scripting/VisualGraphExecutor.js.map +1 -1
- package/dist/scripting/VisualNodeCatalog.d.ts +1 -1
- package/dist/scripting/VisualNodeCatalog.d.ts.map +1 -1
- package/dist/scripting/VisualNodeCatalog.js +61 -1
- package/dist/scripting/VisualNodeCatalog.js.map +1 -1
- package/dist/scripting/index.d.ts +1 -0
- package/dist/scripting/index.d.ts.map +1 -1
- package/dist/scripting/index.js.map +1 -1
- package/package.json +192 -118
|
@@ -0,0 +1,2229 @@
|
|
|
1
|
+
export function createGameLoopPlan(options = {}) {
|
|
2
|
+
return {
|
|
3
|
+
kind: "aura-game-loop-plan",
|
|
4
|
+
fixedDt: options.fixedDt ?? 1 / 60,
|
|
5
|
+
maxSubSteps: options.maxSubSteps ?? 5,
|
|
6
|
+
timeScale: options.timeScale ?? 1
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function createGameJumpAssist(options = {}) {
|
|
10
|
+
const coyoteMs = Math.max(0, options.coyoteMs ?? 100);
|
|
11
|
+
const bufferMs = Math.max(0, options.bufferMs ?? 120);
|
|
12
|
+
const coyoteSeconds = coyoteMs / 1000;
|
|
13
|
+
const bufferSeconds = bufferMs / 1000;
|
|
14
|
+
let time = 0;
|
|
15
|
+
let grounded = false;
|
|
16
|
+
let lastGroundedAt = Number.NEGATIVE_INFINITY;
|
|
17
|
+
let lastJumpRequestedAt = Number.NEGATIVE_INFINITY;
|
|
18
|
+
let consumed = false;
|
|
19
|
+
const coyoteAvailable = () => grounded || time - lastGroundedAt <= coyoteSeconds;
|
|
20
|
+
const jumpBuffered = () => time - lastJumpRequestedAt <= bufferSeconds;
|
|
21
|
+
const snapshot = () => ({
|
|
22
|
+
kind: "aura-game-jump-assist",
|
|
23
|
+
time,
|
|
24
|
+
grounded,
|
|
25
|
+
coyoteMs,
|
|
26
|
+
bufferMs,
|
|
27
|
+
lastGroundedAt,
|
|
28
|
+
lastJumpRequestedAt,
|
|
29
|
+
coyoteAvailable: coyoteAvailable(),
|
|
30
|
+
jumpBuffered: jumpBuffered(),
|
|
31
|
+
consumed,
|
|
32
|
+
canJump: coyoteAvailable() && jumpBuffered() && !consumed
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
kind: "aura-game-jump-assist",
|
|
36
|
+
update(dt, state) {
|
|
37
|
+
time += Math.max(0, dt);
|
|
38
|
+
grounded = state.grounded;
|
|
39
|
+
if (grounded) {
|
|
40
|
+
lastGroundedAt = time;
|
|
41
|
+
consumed = false;
|
|
42
|
+
}
|
|
43
|
+
if (state.jumpPressed || state.jumpRequested)
|
|
44
|
+
lastJumpRequestedAt = time;
|
|
45
|
+
return snapshot();
|
|
46
|
+
},
|
|
47
|
+
requestJump() {
|
|
48
|
+
lastJumpRequestedAt = time;
|
|
49
|
+
},
|
|
50
|
+
canJump() {
|
|
51
|
+
return coyoteAvailable() && jumpBuffered() && !consumed;
|
|
52
|
+
},
|
|
53
|
+
consume() {
|
|
54
|
+
if (!this.canJump())
|
|
55
|
+
return false;
|
|
56
|
+
consumed = true;
|
|
57
|
+
lastJumpRequestedAt = Number.NEGATIVE_INFINITY;
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
reset(state = {}) {
|
|
61
|
+
time = 0;
|
|
62
|
+
grounded = state.grounded ?? false;
|
|
63
|
+
lastGroundedAt = grounded ? 0 : Number.NEGATIVE_INFINITY;
|
|
64
|
+
lastJumpRequestedAt = state.jumpPressed || state.jumpRequested ? 0 : Number.NEGATIVE_INFINITY;
|
|
65
|
+
consumed = false;
|
|
66
|
+
return snapshot();
|
|
67
|
+
},
|
|
68
|
+
snapshot
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function createGameInputReplay(events, options = {}) {
|
|
72
|
+
const fps = Math.max(1, options.fps ?? 60);
|
|
73
|
+
const seed = options.seed ?? 0;
|
|
74
|
+
const normalized = normalizeReplayEvents(events);
|
|
75
|
+
const frameCount = normalized.reduce((max, event) => Math.max(max, event.frame), 0);
|
|
76
|
+
const duration = normalized.reduce((max, event) => Math.max(max, event.time), frameCount / fps);
|
|
77
|
+
const checksum = replayChecksum(normalized, seed, fps);
|
|
78
|
+
return {
|
|
79
|
+
kind: "aura-game-input-replay",
|
|
80
|
+
label: options.label,
|
|
81
|
+
fps,
|
|
82
|
+
seed,
|
|
83
|
+
frameCount,
|
|
84
|
+
duration,
|
|
85
|
+
checksum,
|
|
86
|
+
events: normalized
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function gameInputReplayEventsAt(replay, frame) {
|
|
90
|
+
return replay.events.filter((event) => event.frame === frame);
|
|
91
|
+
}
|
|
92
|
+
export function createGameInputReplayDriver(input, replay) {
|
|
93
|
+
let frame = 0;
|
|
94
|
+
let time = 0;
|
|
95
|
+
const seek = (targetFrame, dt = 1 / replay.fps) => {
|
|
96
|
+
frame = Math.max(0, Math.floor(targetFrame));
|
|
97
|
+
time = frame / replay.fps;
|
|
98
|
+
return input.replay(replay.events.filter((event) => event.frame <= frame), dt);
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
kind: "aura-game-input-replay-driver",
|
|
102
|
+
replay,
|
|
103
|
+
step(dt = 1 / replay.fps) {
|
|
104
|
+
frame += 1;
|
|
105
|
+
time += Math.max(0, dt);
|
|
106
|
+
return input.replay(replay.events.filter((event) => event.frame <= frame), dt);
|
|
107
|
+
},
|
|
108
|
+
seek,
|
|
109
|
+
reset() {
|
|
110
|
+
frame = 0;
|
|
111
|
+
time = 0;
|
|
112
|
+
input.replay([], 0);
|
|
113
|
+
},
|
|
114
|
+
snapshot() {
|
|
115
|
+
return {
|
|
116
|
+
kind: "aura-game-input-replay-driver",
|
|
117
|
+
frame,
|
|
118
|
+
time,
|
|
119
|
+
checksum: replay.checksum,
|
|
120
|
+
complete: frame >= replay.frameCount
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function createGameTouchControlLayout(options) {
|
|
126
|
+
const width = Math.max(1, options.width);
|
|
127
|
+
const height = Math.max(1, options.height);
|
|
128
|
+
const safeArea = {
|
|
129
|
+
top: options.safeArea?.top ?? 0,
|
|
130
|
+
right: options.safeArea?.right ?? 0,
|
|
131
|
+
bottom: options.safeArea?.bottom ?? 0,
|
|
132
|
+
left: options.safeArea?.left ?? 0
|
|
133
|
+
};
|
|
134
|
+
const scale = options.scale ?? clamp(Math.min(width, height) / 720, 0.72, 1.35);
|
|
135
|
+
const gap = options.gap ?? 16 * scale;
|
|
136
|
+
const margin = 32 * scale;
|
|
137
|
+
const controls = [];
|
|
138
|
+
const bindings = {};
|
|
139
|
+
const addControl = (request, center, anchor, zIndex) => {
|
|
140
|
+
const kind = request.kind ?? "button";
|
|
141
|
+
const radius = request.radius ?? (request.size ?? (kind === "button" ? 58 : 72)) * scale;
|
|
142
|
+
const id = request.id ?? request.action ?? `touch-${controls.length + 1}`;
|
|
143
|
+
const binding = request.binding ?? `Touch:${id}`;
|
|
144
|
+
const label = request.label ?? request.action ?? id;
|
|
145
|
+
const region = {
|
|
146
|
+
id,
|
|
147
|
+
kind,
|
|
148
|
+
action: request.action,
|
|
149
|
+
label,
|
|
150
|
+
binding,
|
|
151
|
+
anchor,
|
|
152
|
+
center,
|
|
153
|
+
radius,
|
|
154
|
+
rect: [center[0] - radius, center[1] - radius, radius * 2, radius * 2],
|
|
155
|
+
zIndex
|
|
156
|
+
};
|
|
157
|
+
controls.push(region);
|
|
158
|
+
if (request.action)
|
|
159
|
+
bindings[request.action] = binding;
|
|
160
|
+
};
|
|
161
|
+
if (options.stick !== false) {
|
|
162
|
+
const stick = options.stick ?? {};
|
|
163
|
+
const radius = stick.radius ?? (stick.size ?? 76) * scale;
|
|
164
|
+
addControl({ id: "move", kind: "stick", label: "Move", binding: "TouchStickMove", ...stick }, [safeArea.left + margin + radius, height - safeArea.bottom - margin - radius], "bottom-left", 10);
|
|
165
|
+
}
|
|
166
|
+
const buttons = options.buttons ?? [
|
|
167
|
+
{ action: "jump", label: "Jump", binding: "TouchJump" },
|
|
168
|
+
{ action: "light", label: "Light", binding: "TouchLight" },
|
|
169
|
+
{ action: "special", label: "Special", binding: "TouchSpecial" }
|
|
170
|
+
];
|
|
171
|
+
buttons.forEach((button, index) => {
|
|
172
|
+
const radius = button.radius ?? (button.size ?? 54) * scale;
|
|
173
|
+
const column = index % 2;
|
|
174
|
+
const row = Math.floor(index / 2);
|
|
175
|
+
const x = width - safeArea.right - margin - radius - column * (radius * 2 + gap);
|
|
176
|
+
const y = height - safeArea.bottom - margin - radius - row * (radius * 2 + gap);
|
|
177
|
+
addControl(button, [x, y], "bottom-right", 20 + index);
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
kind: "aura-game-touch-layout",
|
|
181
|
+
width,
|
|
182
|
+
height,
|
|
183
|
+
safeArea,
|
|
184
|
+
controls,
|
|
185
|
+
bindings
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export function createGameInput(options) {
|
|
189
|
+
const actions = options.actions;
|
|
190
|
+
const axes = options.axes ?? {};
|
|
191
|
+
const axisDefaults = options.axisDefaults ?? {};
|
|
192
|
+
const bufferMs = options.bufferMs ?? 120;
|
|
193
|
+
const activeBindings = new Set();
|
|
194
|
+
const activeActionOverrides = new Set();
|
|
195
|
+
const previousHeld = new Map();
|
|
196
|
+
const currentHeld = new Map();
|
|
197
|
+
const pressedEdges = new Set();
|
|
198
|
+
const releasedEdges = new Set();
|
|
199
|
+
const lastPressedAt = new Map();
|
|
200
|
+
const actionPressHistory = [];
|
|
201
|
+
const replayEvents = [];
|
|
202
|
+
const gamepadBindings = new Set();
|
|
203
|
+
const axisValues = new Map();
|
|
204
|
+
let frame = 0;
|
|
205
|
+
let time = 0;
|
|
206
|
+
let pointer = { active: false, x: 0, y: 0, dx: 0, dy: 0, buttons: [] };
|
|
207
|
+
let latestGamepads = [];
|
|
208
|
+
let latestSnapshot = {
|
|
209
|
+
kind: "aura-game-input-snapshot",
|
|
210
|
+
frame,
|
|
211
|
+
time,
|
|
212
|
+
activeBindings: [],
|
|
213
|
+
actions: {},
|
|
214
|
+
axes: {},
|
|
215
|
+
pointer,
|
|
216
|
+
gamepads: []
|
|
217
|
+
};
|
|
218
|
+
const resolveHeld = (action) => {
|
|
219
|
+
if (activeActionOverrides.has(action))
|
|
220
|
+
return true;
|
|
221
|
+
const bindings = actions[action] ?? [];
|
|
222
|
+
return bindings.some((binding) => activeBindings.has(binding));
|
|
223
|
+
};
|
|
224
|
+
const isActionHeld = (action) => currentHeld.get(action) ?? resolveHeld(action);
|
|
225
|
+
const record = (type, binding) => {
|
|
226
|
+
replayEvents.push({ frame, time, type, binding });
|
|
227
|
+
};
|
|
228
|
+
const pressBinding = (binding, shouldRecord = true) => {
|
|
229
|
+
activeBindings.add(binding);
|
|
230
|
+
if (actions[binding])
|
|
231
|
+
activeActionOverrides.add(binding);
|
|
232
|
+
if (shouldRecord)
|
|
233
|
+
record("press", binding);
|
|
234
|
+
};
|
|
235
|
+
const releaseBinding = (binding, shouldRecord = true) => {
|
|
236
|
+
activeBindings.delete(binding);
|
|
237
|
+
activeActionOverrides.delete(binding);
|
|
238
|
+
if (shouldRecord)
|
|
239
|
+
record("release", binding);
|
|
240
|
+
};
|
|
241
|
+
const pollGamepads = () => {
|
|
242
|
+
for (const binding of gamepadBindings)
|
|
243
|
+
activeBindings.delete(binding);
|
|
244
|
+
gamepadBindings.clear();
|
|
245
|
+
latestGamepads = [];
|
|
246
|
+
if (options.gamepad === false || typeof navigator === "undefined" || typeof navigator.getGamepads !== "function")
|
|
247
|
+
return;
|
|
248
|
+
const requestedIndex = typeof options.gamepad === "object" ? options.gamepad.index : undefined;
|
|
249
|
+
const pads = navigator.getGamepads();
|
|
250
|
+
for (const pad of pads) {
|
|
251
|
+
if (!pad || (requestedIndex !== undefined && pad.index !== requestedIndex))
|
|
252
|
+
continue;
|
|
253
|
+
const buttonNames = [];
|
|
254
|
+
pad.buttons.forEach((button, index) => {
|
|
255
|
+
if (!button.pressed)
|
|
256
|
+
return;
|
|
257
|
+
const names = gamepadButtonNames(index, pad.index);
|
|
258
|
+
for (const name of names) {
|
|
259
|
+
activeBindings.add(name);
|
|
260
|
+
gamepadBindings.add(name);
|
|
261
|
+
buttonNames.push(name);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
latestGamepads.push({
|
|
265
|
+
connected: true,
|
|
266
|
+
index: pad.index,
|
|
267
|
+
axes: [...pad.axes],
|
|
268
|
+
buttons: buttonNames
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const resolveAxisRaw = (name, negativeAction, positiveAction, framePointer = latestSnapshot.pointer ?? pointer) => {
|
|
273
|
+
const binding = axes[name];
|
|
274
|
+
const negative = negativeAction ?? binding?.negative;
|
|
275
|
+
const positive = positiveAction ?? binding?.positive;
|
|
276
|
+
const digital = (positive && isActionHeld(positive) ? 1 : 0) - (negative && isActionHeld(negative) ? 1 : 0);
|
|
277
|
+
const gamepadAxis = binding?.gamepadAxis;
|
|
278
|
+
const pad = latestGamepads.find((snapshot) => binding?.gamepadIndex === undefined || snapshot.index === binding.gamepadIndex);
|
|
279
|
+
const deadzone = binding?.deadzone ?? axisDefaults.deadzone ?? 0.18;
|
|
280
|
+
const deadzoneMode = binding?.deadzoneMode ?? axisDefaults.deadzoneMode ?? "axial";
|
|
281
|
+
const analog = gamepadAxis === undefined ? 0 : applyDeadzone(pad?.axes[gamepadAxis] ?? 0, deadzone, deadzoneMode);
|
|
282
|
+
const pointerAxis = binding?.pointerDelta === "x" ? framePointer.dx : binding?.pointerDelta === "y" ? framePointer.dy : 0;
|
|
283
|
+
if (!binding && !negative && !positive)
|
|
284
|
+
return isActionHeld(name) ? 1 : 0;
|
|
285
|
+
const invert = binding?.invert ?? axisDefaults.invert ?? false;
|
|
286
|
+
const scale = binding?.scale ?? axisDefaults.scale ?? 1;
|
|
287
|
+
return clamp((digital || analog || pointerAxis / 96) * scale * (invert ? -1 : 1), -1, 1);
|
|
288
|
+
};
|
|
289
|
+
const smoothAxis = (name, raw, dt) => {
|
|
290
|
+
const binding = axes[name];
|
|
291
|
+
const smoothing = binding?.smoothing ?? axisDefaults.smoothing ?? 0;
|
|
292
|
+
const previous = axisValues.get(name) ?? 0;
|
|
293
|
+
const snap = binding?.snap ?? axisDefaults.snap ?? false;
|
|
294
|
+
if (smoothing <= 0 || dt <= 0 || (snap && Math.sign(previous) !== 0 && Math.sign(raw) !== 0 && Math.sign(previous) !== Math.sign(raw)))
|
|
295
|
+
return raw;
|
|
296
|
+
const t = 1 - Math.exp(-smoothing * dt);
|
|
297
|
+
return clamp(previous + (raw - previous) * t, -1, 1);
|
|
298
|
+
};
|
|
299
|
+
const toSnapshot = (framePointer, axesSnapshot) => {
|
|
300
|
+
const actionStates = {};
|
|
301
|
+
const nowMs = time * 1000;
|
|
302
|
+
for (const action of Object.keys(actions)) {
|
|
303
|
+
const held = currentHeld.get(action) ?? false;
|
|
304
|
+
actionStates[action] = {
|
|
305
|
+
pressed: pressedEdges.has(action),
|
|
306
|
+
held,
|
|
307
|
+
released: releasedEdges.has(action),
|
|
308
|
+
buffered: pressedEdges.has(action) || nowMs - (lastPressedAt.get(action) ?? Number.NEGATIVE_INFINITY) <= bufferMs,
|
|
309
|
+
value: held ? 1 : 0
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
kind: "aura-game-input-snapshot",
|
|
314
|
+
frame,
|
|
315
|
+
time,
|
|
316
|
+
activeBindings: [...activeBindings].sort(),
|
|
317
|
+
actions: actionStates,
|
|
318
|
+
axes: axesSnapshot,
|
|
319
|
+
pointer: framePointer,
|
|
320
|
+
gamepads: latestGamepads
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
const update = (dt = 1 / 60) => {
|
|
324
|
+
const seconds = Math.max(0, dt);
|
|
325
|
+
frame += 1;
|
|
326
|
+
time += seconds;
|
|
327
|
+
const framePointer = pointer;
|
|
328
|
+
pollGamepads();
|
|
329
|
+
pressedEdges.clear();
|
|
330
|
+
releasedEdges.clear();
|
|
331
|
+
for (const action of Object.keys(actions)) {
|
|
332
|
+
const held = resolveHeld(action);
|
|
333
|
+
const wasHeld = previousHeld.get(action) ?? false;
|
|
334
|
+
currentHeld.set(action, held);
|
|
335
|
+
if (held && !wasHeld) {
|
|
336
|
+
pressedEdges.add(action);
|
|
337
|
+
const nowMs = time * 1000;
|
|
338
|
+
lastPressedAt.set(action, nowMs);
|
|
339
|
+
actionPressHistory.push({ action, timeMs: nowMs });
|
|
340
|
+
while (actionPressHistory.length > 64)
|
|
341
|
+
actionPressHistory.shift();
|
|
342
|
+
}
|
|
343
|
+
if (!held && wasHeld)
|
|
344
|
+
releasedEdges.add(action);
|
|
345
|
+
previousHeld.set(action, held);
|
|
346
|
+
}
|
|
347
|
+
const axesSnapshot = {};
|
|
348
|
+
for (const axisName of Object.keys(axes)) {
|
|
349
|
+
const raw = resolveAxisRaw(axisName, undefined, undefined, framePointer);
|
|
350
|
+
const value = smoothAxis(axisName, raw, seconds);
|
|
351
|
+
axisValues.set(axisName, value);
|
|
352
|
+
axesSnapshot[axisName] = value;
|
|
353
|
+
}
|
|
354
|
+
latestSnapshot = toSnapshot(framePointer, axesSnapshot);
|
|
355
|
+
pointer = { ...pointer, dx: 0, dy: 0 };
|
|
356
|
+
return latestSnapshot;
|
|
357
|
+
};
|
|
358
|
+
const target = options.target ?? (typeof window !== "undefined" ? window : undefined);
|
|
359
|
+
const onKeyDown = (event) => {
|
|
360
|
+
const keyboard = event;
|
|
361
|
+
if (keyboard.repeat)
|
|
362
|
+
return;
|
|
363
|
+
if (keyboard.code)
|
|
364
|
+
pressBinding(keyboard.code);
|
|
365
|
+
if (keyboard.key && keyboard.key !== keyboard.code)
|
|
366
|
+
pressBinding(keyboard.key);
|
|
367
|
+
};
|
|
368
|
+
const onKeyUp = (event) => {
|
|
369
|
+
const keyboard = event;
|
|
370
|
+
if (keyboard.code)
|
|
371
|
+
releaseBinding(keyboard.code);
|
|
372
|
+
if (keyboard.key && keyboard.key !== keyboard.code)
|
|
373
|
+
releaseBinding(keyboard.key);
|
|
374
|
+
};
|
|
375
|
+
const onPointerDown = (event) => {
|
|
376
|
+
if (options.pointer === false)
|
|
377
|
+
return;
|
|
378
|
+
const next = event;
|
|
379
|
+
pressBinding(next.button === 2 ? "PointerSecondary" : "PointerPrimary");
|
|
380
|
+
pressBinding(`PointerButton${next.button}`);
|
|
381
|
+
pointer = {
|
|
382
|
+
active: true,
|
|
383
|
+
x: next.clientX,
|
|
384
|
+
y: next.clientY,
|
|
385
|
+
dx: 0,
|
|
386
|
+
dy: 0,
|
|
387
|
+
buttons: [...new Set([...pointer.buttons, next.button])].sort()
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
const onPointerMove = (event) => {
|
|
391
|
+
if (options.pointer === false)
|
|
392
|
+
return;
|
|
393
|
+
const next = event;
|
|
394
|
+
pointer = {
|
|
395
|
+
...pointer,
|
|
396
|
+
x: next.clientX,
|
|
397
|
+
y: next.clientY,
|
|
398
|
+
dx: pointer.dx + next.clientX - pointer.x,
|
|
399
|
+
dy: pointer.dy + next.clientY - pointer.y
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
const onPointerUp = (event) => {
|
|
403
|
+
if (options.pointer === false)
|
|
404
|
+
return;
|
|
405
|
+
const next = event;
|
|
406
|
+
releaseBinding(next.button === 2 ? "PointerSecondary" : "PointerPrimary");
|
|
407
|
+
releaseBinding(`PointerButton${next.button}`);
|
|
408
|
+
const buttons = pointer.buttons.filter((button) => button !== next.button);
|
|
409
|
+
pointer = {
|
|
410
|
+
active: buttons.length > 0,
|
|
411
|
+
x: next.clientX,
|
|
412
|
+
y: next.clientY,
|
|
413
|
+
dx: pointer.dx + next.clientX - pointer.x,
|
|
414
|
+
dy: pointer.dy + next.clientY - pointer.y,
|
|
415
|
+
buttons
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
const onTouchStart = (event) => {
|
|
419
|
+
if (options.touch === false)
|
|
420
|
+
return;
|
|
421
|
+
const touch = event.touches[0];
|
|
422
|
+
pressBinding("TouchPrimary");
|
|
423
|
+
if (touch)
|
|
424
|
+
pointer = { active: true, x: touch.clientX, y: touch.clientY, dx: 0, dy: 0, buttons: [0] };
|
|
425
|
+
};
|
|
426
|
+
const onTouchEnd = () => {
|
|
427
|
+
if (options.touch === false)
|
|
428
|
+
return;
|
|
429
|
+
releaseBinding("TouchPrimary");
|
|
430
|
+
pointer = { ...pointer, active: false, buttons: [] };
|
|
431
|
+
};
|
|
432
|
+
if (options.autoListen !== false && target?.addEventListener) {
|
|
433
|
+
target.addEventListener("keydown", onKeyDown);
|
|
434
|
+
target.addEventListener("keyup", onKeyUp);
|
|
435
|
+
target.addEventListener("pointerdown", onPointerDown);
|
|
436
|
+
target.addEventListener("pointermove", onPointerMove);
|
|
437
|
+
target.addEventListener("pointerup", onPointerUp);
|
|
438
|
+
target.addEventListener("touchstart", onTouchStart);
|
|
439
|
+
target.addEventListener("touchend", onTouchEnd);
|
|
440
|
+
target.addEventListener("touchcancel", onTouchEnd);
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
kind: "aura-game-input-plan",
|
|
444
|
+
actions,
|
|
445
|
+
axes,
|
|
446
|
+
bufferMs,
|
|
447
|
+
update,
|
|
448
|
+
snapshot() {
|
|
449
|
+
return latestSnapshot;
|
|
450
|
+
},
|
|
451
|
+
pressed(action) {
|
|
452
|
+
return pressedEdges.has(action);
|
|
453
|
+
},
|
|
454
|
+
held(action) {
|
|
455
|
+
return currentHeld.get(action) ?? resolveHeld(action);
|
|
456
|
+
},
|
|
457
|
+
released(action) {
|
|
458
|
+
return releasedEdges.has(action);
|
|
459
|
+
},
|
|
460
|
+
buffered(action, windowMs = bufferMs) {
|
|
461
|
+
return pressedEdges.has(action) || time * 1000 - (lastPressedAt.get(action) ?? Number.NEGATIVE_INFINITY) <= windowMs;
|
|
462
|
+
},
|
|
463
|
+
combo(sequence, windowMs = bufferMs * Math.max(1, sequence.length)) {
|
|
464
|
+
if (sequence.length === 0)
|
|
465
|
+
return false;
|
|
466
|
+
const nowMs = time * 1000;
|
|
467
|
+
const recent = actionPressHistory.filter((entry) => nowMs - entry.timeMs <= windowMs);
|
|
468
|
+
let cursor = sequence.length - 1;
|
|
469
|
+
for (let index = recent.length - 1; index >= 0 && cursor >= 0; index -= 1) {
|
|
470
|
+
if (recent[index]?.action === sequence[cursor])
|
|
471
|
+
cursor -= 1;
|
|
472
|
+
}
|
|
473
|
+
return cursor < 0;
|
|
474
|
+
},
|
|
475
|
+
axis(name, negativeAction, positiveAction) {
|
|
476
|
+
if (!negativeAction && !positiveAction && axisValues.has(name))
|
|
477
|
+
return axisValues.get(name) ?? 0;
|
|
478
|
+
return resolveAxisRaw(name, negativeAction, positiveAction, latestSnapshot.pointer ?? pointer);
|
|
479
|
+
},
|
|
480
|
+
press(binding) {
|
|
481
|
+
pressBinding(binding);
|
|
482
|
+
},
|
|
483
|
+
release(binding) {
|
|
484
|
+
releaseBinding(binding);
|
|
485
|
+
},
|
|
486
|
+
setAction(action, held) {
|
|
487
|
+
if (held) {
|
|
488
|
+
activeActionOverrides.add(action);
|
|
489
|
+
record("press", action);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
activeActionOverrides.delete(action);
|
|
493
|
+
record("release", action);
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
recorded() {
|
|
497
|
+
return [...replayEvents];
|
|
498
|
+
},
|
|
499
|
+
replay(events, dt = 0) {
|
|
500
|
+
activeBindings.clear();
|
|
501
|
+
activeActionOverrides.clear();
|
|
502
|
+
for (const event of events) {
|
|
503
|
+
if (event.type === "press")
|
|
504
|
+
pressBinding(event.binding, false);
|
|
505
|
+
else
|
|
506
|
+
releaseBinding(event.binding, false);
|
|
507
|
+
}
|
|
508
|
+
return update(dt);
|
|
509
|
+
},
|
|
510
|
+
clearReplay() {
|
|
511
|
+
replayEvents.length = 0;
|
|
512
|
+
},
|
|
513
|
+
dispose() {
|
|
514
|
+
if (target?.removeEventListener) {
|
|
515
|
+
target.removeEventListener("keydown", onKeyDown);
|
|
516
|
+
target.removeEventListener("keyup", onKeyUp);
|
|
517
|
+
target.removeEventListener("pointerdown", onPointerDown);
|
|
518
|
+
target.removeEventListener("pointermove", onPointerMove);
|
|
519
|
+
target.removeEventListener("pointerup", onPointerUp);
|
|
520
|
+
target.removeEventListener("touchstart", onTouchStart);
|
|
521
|
+
target.removeEventListener("touchend", onTouchEnd);
|
|
522
|
+
target.removeEventListener("touchcancel", onTouchEnd);
|
|
523
|
+
}
|
|
524
|
+
activeBindings.clear();
|
|
525
|
+
activeActionOverrides.clear();
|
|
526
|
+
previousHeld.clear();
|
|
527
|
+
currentHeld.clear();
|
|
528
|
+
pressedEdges.clear();
|
|
529
|
+
releasedEdges.clear();
|
|
530
|
+
gamepadBindings.clear();
|
|
531
|
+
axisValues.clear();
|
|
532
|
+
actionPressHistory.length = 0;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
export function createGameKinematicBody(options = {}) {
|
|
537
|
+
let position = vec3(options.position, [0, options.groundY ?? 0, 0]);
|
|
538
|
+
let velocity = vec3(options.velocity, [0, 0, 0]);
|
|
539
|
+
const size = vec3(options.size ?? kinematicSizeFromCollider(options.collider), [0.72, 1.7, 0.42]);
|
|
540
|
+
const gravity = typeof options.gravity === "boolean" ? (options.gravity ? -18 : 0) : options.gravity ?? -18;
|
|
541
|
+
const groundY = options.groundY ?? 0;
|
|
542
|
+
const friction = options.friction ?? 10;
|
|
543
|
+
const maxSpeed = options.maxSpeed ?? 5;
|
|
544
|
+
const jumpVelocity = options.jumpVelocity ?? 7.5;
|
|
545
|
+
const bounds = options.bounds ?? {};
|
|
546
|
+
const definedMoves = new Map();
|
|
547
|
+
let grounded = position[1] <= groundY + 0.0001;
|
|
548
|
+
let facing = 1;
|
|
549
|
+
let elapsed = 0;
|
|
550
|
+
const coyoteSeconds = Math.max(0, options.coyoteMs ?? 0) / 1000;
|
|
551
|
+
const jumpBufferSeconds = Math.max(0, options.jumpBufferMs ?? 0) / 1000;
|
|
552
|
+
let lastGroundedAt = grounded ? 0 : Number.NEGATIVE_INFINITY;
|
|
553
|
+
let lastJumpRequestedAt = Number.NEGATIVE_INFINITY;
|
|
554
|
+
let jumpConsumed = false;
|
|
555
|
+
const snapshot = () => ({
|
|
556
|
+
kind: "aura-game-kinematic-body",
|
|
557
|
+
id: options.id,
|
|
558
|
+
position,
|
|
559
|
+
velocity,
|
|
560
|
+
size,
|
|
561
|
+
collider: options.collider,
|
|
562
|
+
grounded,
|
|
563
|
+
facing,
|
|
564
|
+
coyoteAvailable: canUseGroundedJump(),
|
|
565
|
+
jumpBuffered: hasBufferedJump()
|
|
566
|
+
});
|
|
567
|
+
const hasBufferedJump = () => elapsed - lastJumpRequestedAt <= jumpBufferSeconds;
|
|
568
|
+
const canUseGroundedJump = () => !jumpConsumed && (grounded || elapsed - lastGroundedAt <= coyoteSeconds);
|
|
569
|
+
const startJump = (nextVelocity) => {
|
|
570
|
+
if (!canUseGroundedJump())
|
|
571
|
+
return false;
|
|
572
|
+
grounded = false;
|
|
573
|
+
jumpConsumed = true;
|
|
574
|
+
lastJumpRequestedAt = Number.NEGATIVE_INFINITY;
|
|
575
|
+
velocity = [velocity[0], nextVelocity, velocity[2]];
|
|
576
|
+
return true;
|
|
577
|
+
};
|
|
578
|
+
const clampPosition = () => {
|
|
579
|
+
position = [
|
|
580
|
+
clamp(position[0], bounds.minX ?? Number.NEGATIVE_INFINITY, bounds.maxX ?? Number.POSITIVE_INFINITY),
|
|
581
|
+
clamp(position[1], bounds.minY ?? Number.NEGATIVE_INFINITY, bounds.maxY ?? Number.POSITIVE_INFINITY),
|
|
582
|
+
clamp(position[2], bounds.minZ ?? Number.NEGATIVE_INFINITY, bounds.maxZ ?? Number.POSITIVE_INFINITY)
|
|
583
|
+
];
|
|
584
|
+
};
|
|
585
|
+
const dashBody = (direction, speed = maxSpeed * 1.8) => {
|
|
586
|
+
const normalized = normalizeVec3(direction);
|
|
587
|
+
velocity = [normalized[0] * speed, velocity[1], normalized[2] * speed];
|
|
588
|
+
if (Math.abs(normalized[0]) > 0.01)
|
|
589
|
+
facing = normalized[0] >= 0 ? 1 : -1;
|
|
590
|
+
};
|
|
591
|
+
const applyBodyKnockback = (nextVelocity) => {
|
|
592
|
+
velocity = addVec3(velocity, nextVelocity);
|
|
593
|
+
if (Math.abs(nextVelocity[0]) > 0.01)
|
|
594
|
+
facing = nextVelocity[0] >= 0 ? -1 : 1;
|
|
595
|
+
grounded = false;
|
|
596
|
+
};
|
|
597
|
+
const updateBody = (dt) => {
|
|
598
|
+
const seconds = Math.max(0, dt);
|
|
599
|
+
elapsed += seconds;
|
|
600
|
+
if (!grounded || velocity[1] > 0)
|
|
601
|
+
velocity = [velocity[0], velocity[1] + gravity * seconds, velocity[2]];
|
|
602
|
+
position = addVec3(position, scaleVec3(velocity, seconds));
|
|
603
|
+
if (position[1] <= groundY) {
|
|
604
|
+
position = [position[0], groundY, position[2]];
|
|
605
|
+
velocity = [velocity[0], Math.max(0, velocity[1]), velocity[2]];
|
|
606
|
+
grounded = true;
|
|
607
|
+
lastGroundedAt = elapsed;
|
|
608
|
+
jumpConsumed = false;
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
grounded = false;
|
|
612
|
+
}
|
|
613
|
+
if (grounded && friction > 0) {
|
|
614
|
+
const damping = Math.max(0, 1 - friction * seconds);
|
|
615
|
+
velocity = [velocity[0] * damping, velocity[1], velocity[2] * damping];
|
|
616
|
+
}
|
|
617
|
+
clampPosition();
|
|
618
|
+
return snapshot();
|
|
619
|
+
};
|
|
620
|
+
return {
|
|
621
|
+
id: options.id,
|
|
622
|
+
get position() {
|
|
623
|
+
return position;
|
|
624
|
+
},
|
|
625
|
+
set position(next) {
|
|
626
|
+
position = vec3(next, position);
|
|
627
|
+
grounded = position[1] <= groundY + 0.0001;
|
|
628
|
+
if (grounded) {
|
|
629
|
+
lastGroundedAt = elapsed;
|
|
630
|
+
jumpConsumed = false;
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
get velocity() {
|
|
634
|
+
return velocity;
|
|
635
|
+
},
|
|
636
|
+
set velocity(next) {
|
|
637
|
+
velocity = vec3(next, velocity);
|
|
638
|
+
},
|
|
639
|
+
size,
|
|
640
|
+
collider: options.collider,
|
|
641
|
+
get grounded() {
|
|
642
|
+
return grounded;
|
|
643
|
+
},
|
|
644
|
+
get facing() {
|
|
645
|
+
return facing;
|
|
646
|
+
},
|
|
647
|
+
move(axisOrCommand, speedOrDt) {
|
|
648
|
+
if (typeof axisOrCommand === "number") {
|
|
649
|
+
const next = clamp(axisOrCommand, -1, 1);
|
|
650
|
+
velocity = [next * (speedOrDt ?? maxSpeed), velocity[1], velocity[2]];
|
|
651
|
+
if (Math.abs(next) > 0.01)
|
|
652
|
+
facing = next >= 0 ? 1 : -1;
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
const command = axisOrCommand;
|
|
656
|
+
const horizontal = command.axis !== undefined
|
|
657
|
+
? clamp(command.axis, -1, 1) * (command.speed ?? maxSpeed)
|
|
658
|
+
: command.x !== undefined
|
|
659
|
+
? clamp(command.x, -maxSpeed, maxSpeed)
|
|
660
|
+
: velocity[0];
|
|
661
|
+
velocity = [
|
|
662
|
+
horizontal,
|
|
663
|
+
command.y !== undefined ? command.y : velocity[1],
|
|
664
|
+
command.z !== undefined ? clamp(command.z, -maxSpeed, maxSpeed) : velocity[2]
|
|
665
|
+
];
|
|
666
|
+
if (Math.abs(horizontal) > 0.01)
|
|
667
|
+
facing = horizontal >= 0 ? 1 : -1;
|
|
668
|
+
if (command.jump) {
|
|
669
|
+
lastJumpRequestedAt = elapsed;
|
|
670
|
+
startJump(command.jumpVelocity ?? jumpVelocity);
|
|
671
|
+
}
|
|
672
|
+
if (command.dash)
|
|
673
|
+
dashBody(typeof command.dash === "boolean" ? [facing, 0, 0] : command.dash, command.dashSpeed);
|
|
674
|
+
if (command.knockback) {
|
|
675
|
+
const knockback = command.knockback.length === 2
|
|
676
|
+
? [command.knockback[0], command.knockback[1], 0]
|
|
677
|
+
: command.knockback;
|
|
678
|
+
applyBodyKnockback(knockback);
|
|
679
|
+
}
|
|
680
|
+
return speedOrDt !== undefined ? updateBody(speedOrDt) : snapshot();
|
|
681
|
+
},
|
|
682
|
+
jump(nextVelocity = jumpVelocity) {
|
|
683
|
+
return startJump(nextVelocity);
|
|
684
|
+
},
|
|
685
|
+
requestJump() {
|
|
686
|
+
lastJumpRequestedAt = elapsed;
|
|
687
|
+
},
|
|
688
|
+
canJump() {
|
|
689
|
+
return canUseGroundedJump();
|
|
690
|
+
},
|
|
691
|
+
consumeJump(nextVelocity = jumpVelocity) {
|
|
692
|
+
if (!hasBufferedJump())
|
|
693
|
+
return false;
|
|
694
|
+
return startJump(nextVelocity);
|
|
695
|
+
},
|
|
696
|
+
dash(direction, speed = maxSpeed * 1.8) {
|
|
697
|
+
dashBody(direction, speed);
|
|
698
|
+
},
|
|
699
|
+
applyKnockback(nextVelocity) {
|
|
700
|
+
applyBodyKnockback(nextVelocity);
|
|
701
|
+
},
|
|
702
|
+
defineMove(id, move) {
|
|
703
|
+
const nextMove = { ...move, id: move.id ?? id };
|
|
704
|
+
definedMoves.set(id, nextMove);
|
|
705
|
+
return nextMove;
|
|
706
|
+
},
|
|
707
|
+
attack(moveId) {
|
|
708
|
+
return definedMoves.get(moveId);
|
|
709
|
+
},
|
|
710
|
+
moves() {
|
|
711
|
+
return [...definedMoves.values()];
|
|
712
|
+
},
|
|
713
|
+
update: updateBody,
|
|
714
|
+
snapToGround(nextGroundY = groundY) {
|
|
715
|
+
position = [position[0], nextGroundY, position[2]];
|
|
716
|
+
velocity = [velocity[0], Math.max(0, velocity[1]), velocity[2]];
|
|
717
|
+
grounded = true;
|
|
718
|
+
lastGroundedAt = elapsed;
|
|
719
|
+
jumpConsumed = false;
|
|
720
|
+
},
|
|
721
|
+
bounds() {
|
|
722
|
+
return aabb(position, size);
|
|
723
|
+
},
|
|
724
|
+
snapshot
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
export function createGameFighting2DRules(options = {}) {
|
|
728
|
+
return {
|
|
729
|
+
kind: "aura-game-fighting-2d-rules",
|
|
730
|
+
gravity: options.gravity ?? 24,
|
|
731
|
+
roundSeconds: options.roundSeconds ?? 90,
|
|
732
|
+
maxHealth: options.maxHealth ?? 100,
|
|
733
|
+
maxGuard: options.maxGuard ?? 100,
|
|
734
|
+
maxMeter: options.maxMeter ?? 100,
|
|
735
|
+
stageBounds: options.stageBounds ?? { minX: -4.5, maxX: 4.5, minZ: -0.72, maxZ: 0.72 },
|
|
736
|
+
fps: options.fps ?? 60,
|
|
737
|
+
pushboxSeparation: options.pushboxSeparation ?? true
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
export function createCombatWorld(options = {}) {
|
|
741
|
+
const actors = new Map();
|
|
742
|
+
const fighterBodies = new Map();
|
|
743
|
+
const moves = new Map();
|
|
744
|
+
const activeAttacks = [];
|
|
745
|
+
const rules = options.rules ?? createGameFighting2DRules();
|
|
746
|
+
const stageBounds = options.stageBounds ?? rules.stageBounds;
|
|
747
|
+
let events = [];
|
|
748
|
+
let frame = 0;
|
|
749
|
+
let time = 0;
|
|
750
|
+
const snapshot = () => ({
|
|
751
|
+
kind: "aura-game-combat-world",
|
|
752
|
+
frame,
|
|
753
|
+
time,
|
|
754
|
+
actors: [...actors.values()].map(actorSnapshot),
|
|
755
|
+
activeAttacks: activeAttacks.map((attack) => {
|
|
756
|
+
const activeFrames = moveActiveFrames(attack.move, rules.fps);
|
|
757
|
+
const durationFrames = moveDurationFrames(attack.move, activeFrames, rules.fps);
|
|
758
|
+
return {
|
|
759
|
+
attackerId: attack.attackerId,
|
|
760
|
+
moveId: attack.move.id,
|
|
761
|
+
frame: attack.frame,
|
|
762
|
+
activeFrames,
|
|
763
|
+
durationFrames,
|
|
764
|
+
active: attack.frame >= activeFrames[0] && attack.frame <= activeFrames[1],
|
|
765
|
+
hitboxes: moveHitboxes(attack.move),
|
|
766
|
+
hitTargets: [...attack.hitTargets].sort()
|
|
767
|
+
};
|
|
768
|
+
}),
|
|
769
|
+
events: [...events]
|
|
770
|
+
});
|
|
771
|
+
const controller = {
|
|
772
|
+
addActor(actor) {
|
|
773
|
+
const normalized = normalizeActor(actor);
|
|
774
|
+
actors.set(actor.id, {
|
|
775
|
+
...normalized,
|
|
776
|
+
health: actor.health ?? rules.maxHealth,
|
|
777
|
+
guard: actor.guard ?? rules.maxGuard,
|
|
778
|
+
meter: actor.meter ?? 0
|
|
779
|
+
});
|
|
780
|
+
},
|
|
781
|
+
setActor(id, patch) {
|
|
782
|
+
const actor = actors.get(id);
|
|
783
|
+
if (!actor) {
|
|
784
|
+
actors.set(id, normalizeActor({ id, ...patch }));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const stun = actor.stun;
|
|
788
|
+
const recovery = actor.recovery;
|
|
789
|
+
Object.assign(actor, normalizeActor({ ...actorSnapshot(actor), ...patch, id }));
|
|
790
|
+
actor.stun = stun;
|
|
791
|
+
actor.recovery = recovery;
|
|
792
|
+
},
|
|
793
|
+
removeActor(id) {
|
|
794
|
+
actors.delete(id);
|
|
795
|
+
fighterBodies.delete(id);
|
|
796
|
+
moves.delete(id);
|
|
797
|
+
},
|
|
798
|
+
defineMove(actorId, move) {
|
|
799
|
+
const actorMoves = moves.get(actorId) ?? new Map();
|
|
800
|
+
actorMoves.set(move.id, normalizeCombatMove(move));
|
|
801
|
+
moves.set(actorId, actorMoves);
|
|
802
|
+
},
|
|
803
|
+
attack(attackerId, move) {
|
|
804
|
+
const resolvedMove = typeof move === "string" ? moves.get(attackerId)?.get(move) ?? fighterBodies.get(attackerId)?.attack(move) : move;
|
|
805
|
+
if (resolvedMove)
|
|
806
|
+
this.beginAttack(attackerId, resolvedMove);
|
|
807
|
+
},
|
|
808
|
+
beginAttack(attackerId, move) {
|
|
809
|
+
if (!actors.has(attackerId))
|
|
810
|
+
return;
|
|
811
|
+
activeAttacks.push({ attackerId, move: normalizeCombatMove(move), frame: 0, hitTargets: new Set() });
|
|
812
|
+
},
|
|
813
|
+
update(dt) {
|
|
814
|
+
frame += 1;
|
|
815
|
+
time += Math.max(0, dt);
|
|
816
|
+
events = [];
|
|
817
|
+
syncActorsFromBodies(fighterBodies, actors);
|
|
818
|
+
for (const actor of actors.values()) {
|
|
819
|
+
actor.stun = Math.max(0, actor.stun - 1);
|
|
820
|
+
actor.recovery = Math.max(0, actor.recovery - 1);
|
|
821
|
+
}
|
|
822
|
+
if (rules.pushboxSeparation)
|
|
823
|
+
resolvePushboxSeparation(actors, stageBounds, frame, time, events);
|
|
824
|
+
for (let index = activeAttacks.length - 1; index >= 0; index -= 1) {
|
|
825
|
+
const attack = activeAttacks[index];
|
|
826
|
+
attack.frame += 1;
|
|
827
|
+
const attacker = actors.get(attack.attackerId);
|
|
828
|
+
if (!attacker) {
|
|
829
|
+
activeAttacks.splice(index, 1);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const [activeStart, activeEnd] = moveActiveFrames(attack.move, rules.fps);
|
|
833
|
+
if (attack.frame >= activeStart && attack.frame <= activeEnd) {
|
|
834
|
+
resolveAttack(attacker, attack, actors, stageBounds, frame, time, events);
|
|
835
|
+
}
|
|
836
|
+
if (attack.frame >= moveDurationFrames(attack.move, [activeStart, activeEnd], rules.fps)) {
|
|
837
|
+
if (attack.hitTargets.size === 0) {
|
|
838
|
+
events.push({
|
|
839
|
+
type: "whiff",
|
|
840
|
+
frame,
|
|
841
|
+
time,
|
|
842
|
+
attackerId: attacker.id,
|
|
843
|
+
moveId: attack.move.id,
|
|
844
|
+
position: attacker.position
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
activeAttacks.splice(index, 1);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
syncBodiesFromActors(fighterBodies, actors);
|
|
851
|
+
return snapshot();
|
|
852
|
+
},
|
|
853
|
+
step(dt) {
|
|
854
|
+
return this.update(dt).events;
|
|
855
|
+
},
|
|
856
|
+
events() {
|
|
857
|
+
return [...events];
|
|
858
|
+
},
|
|
859
|
+
consumeEvents() {
|
|
860
|
+
const consumed = [...events];
|
|
861
|
+
events = [];
|
|
862
|
+
return consumed;
|
|
863
|
+
},
|
|
864
|
+
snapshot,
|
|
865
|
+
clear() {
|
|
866
|
+
actors.clear();
|
|
867
|
+
activeAttacks.length = 0;
|
|
868
|
+
events = [];
|
|
869
|
+
frame = 0;
|
|
870
|
+
time = 0;
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
for (const [index, fighter] of (options.fighters ?? []).entries()) {
|
|
874
|
+
if (isGameKinematicBody(fighter)) {
|
|
875
|
+
const id = fighter.id ?? `fighter-${index + 1}`;
|
|
876
|
+
fighterBodies.set(id, fighter);
|
|
877
|
+
controller.addActor({ id, position: fighter.position, facing: fighter.facing });
|
|
878
|
+
for (const move of fighter.moves())
|
|
879
|
+
controller.defineMove(id, move);
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
controller.addActor(fighter);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return controller;
|
|
886
|
+
}
|
|
887
|
+
export function createGameCameraDirector(options = {}) {
|
|
888
|
+
const baseFov = options.baseFov ?? 42;
|
|
889
|
+
const baseDistance = options.distance ?? 6.2;
|
|
890
|
+
const targetY = options.targetY ?? 0.95;
|
|
891
|
+
const minZoom = options.minZoom ?? 0.86;
|
|
892
|
+
const maxZoom = options.maxZoom ?? 1.24;
|
|
893
|
+
const reducedMotion = options.reducedMotion ?? false;
|
|
894
|
+
const mode = options.mode ?? "side-fighter";
|
|
895
|
+
const stageBounds = options.stageBounds ?? options.bounds;
|
|
896
|
+
const impactShake = options.impactShake ?? true;
|
|
897
|
+
let state = {
|
|
898
|
+
kind: "aura-game-camera-director",
|
|
899
|
+
position: [0, 1.35, baseDistance],
|
|
900
|
+
target: [0, targetY, 0],
|
|
901
|
+
fov: baseFov,
|
|
902
|
+
zoom: 1,
|
|
903
|
+
shake: 0,
|
|
904
|
+
reducedMotion,
|
|
905
|
+
mode,
|
|
906
|
+
targetIds: options.targetIds
|
|
907
|
+
};
|
|
908
|
+
let shakeRemaining = 0;
|
|
909
|
+
let shakeIntensity = 0;
|
|
910
|
+
let specialRemaining = 0;
|
|
911
|
+
let specialTarget;
|
|
912
|
+
return {
|
|
913
|
+
update(dt, targets) {
|
|
914
|
+
const seconds = Math.max(0, dt);
|
|
915
|
+
shakeRemaining = Math.max(0, shakeRemaining - seconds);
|
|
916
|
+
specialRemaining = Math.max(0, specialRemaining - seconds);
|
|
917
|
+
const positions = targets.length ? targets.map((target) => target.position) : [state.target];
|
|
918
|
+
const minX = Math.min(...positions.map((position) => position[0]));
|
|
919
|
+
const maxX = Math.max(...positions.map((position) => position[0]));
|
|
920
|
+
const centerX = clamp((minX + maxX) / 2, stageBounds?.minX ?? -100, stageBounds?.maxX ?? 100);
|
|
921
|
+
const distance = Math.max(1, maxX - minX);
|
|
922
|
+
const zoom = clamp(1 + (distance - 2.2) * 0.08, minZoom, maxZoom);
|
|
923
|
+
const focus = specialRemaining > 0 && specialTarget ? specialTarget : [centerX, targetY, 0];
|
|
924
|
+
const shake = reducedMotion ? 0 : shakeRemaining > 0 ? shakeIntensity * (shakeRemaining / Math.max(0.001, shakeRemaining + seconds)) : 0;
|
|
925
|
+
state = {
|
|
926
|
+
kind: "aura-game-camera-director",
|
|
927
|
+
target: focus,
|
|
928
|
+
position: [focus[0] + shake * 0.04, focus[1] + 0.42 + shake * 0.02, baseDistance * zoom],
|
|
929
|
+
fov: baseFov / zoom,
|
|
930
|
+
zoom,
|
|
931
|
+
shake,
|
|
932
|
+
reducedMotion,
|
|
933
|
+
mode,
|
|
934
|
+
targetIds: options.targetIds ?? targets.map((target) => target.id).filter((id) => Boolean(id))
|
|
935
|
+
};
|
|
936
|
+
return state;
|
|
937
|
+
},
|
|
938
|
+
impact(intensity = 1, duration = 0.16) {
|
|
939
|
+
if (reducedMotion || !impactShake)
|
|
940
|
+
return;
|
|
941
|
+
shakeIntensity = Math.max(shakeIntensity, intensity);
|
|
942
|
+
shakeRemaining = Math.max(shakeRemaining, duration);
|
|
943
|
+
},
|
|
944
|
+
special(target, duration = 0.8) {
|
|
945
|
+
specialTarget = target;
|
|
946
|
+
specialRemaining = duration;
|
|
947
|
+
},
|
|
948
|
+
snapshot() {
|
|
949
|
+
return state;
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
export function createGameEffects(options = {}) {
|
|
954
|
+
const poolSize = options.poolSize ?? 96;
|
|
955
|
+
const reducedMotion = options.reducedMotion ?? false;
|
|
956
|
+
const reducedFlash = options.reducedFlash ?? false;
|
|
957
|
+
let spawned = 0;
|
|
958
|
+
let effects = [];
|
|
959
|
+
const snapshot = () => ({
|
|
960
|
+
kind: "aura-game-effects",
|
|
961
|
+
active: effects.length,
|
|
962
|
+
spawned,
|
|
963
|
+
pooled: poolSize,
|
|
964
|
+
reducedMotion,
|
|
965
|
+
reducedFlash,
|
|
966
|
+
effects: effects.map(publicGameEffectInstance)
|
|
967
|
+
});
|
|
968
|
+
const spawn = (kind, position, effectOptions = {}) => {
|
|
969
|
+
spawned += 1;
|
|
970
|
+
const flashLimited = reducedFlash && (kind === "impact-flash" || kind === "super-flash");
|
|
971
|
+
const motionLimited = reducedMotion && (kind === "dash-trail" || kind === "slash-trail" || kind === "shockwave" || kind === "ring-shockwave");
|
|
972
|
+
const attachment = effectOptions.attachment;
|
|
973
|
+
const effect = {
|
|
974
|
+
id: effectOptions.ownerId ? `${effectOptions.ownerId}:${kind}:${spawned}` : `${kind}:${spawned}`,
|
|
975
|
+
kind,
|
|
976
|
+
position: resolveEffectAttachmentPosition(attachment, position),
|
|
977
|
+
color: effectOptions.color ?? defaultEffectColor(kind),
|
|
978
|
+
intensity: Math.min(effectOptions.intensity ?? 1, flashLimited ? 0.35 : motionLimited ? 0.55 : Number.POSITIVE_INFINITY),
|
|
979
|
+
duration: Math.min(effectOptions.duration ?? defaultEffectDuration(kind), motionLimited ? 0.18 : Number.POSITIVE_INFINITY),
|
|
980
|
+
radius: effectOptions.radius ?? defaultEffectRadius(kind),
|
|
981
|
+
ownerId: effectOptions.ownerId,
|
|
982
|
+
attachmentId: attachment?.targetId ?? attachment?.id,
|
|
983
|
+
attachmentOffset: attachment?.offset,
|
|
984
|
+
attachment,
|
|
985
|
+
age: 0
|
|
986
|
+
};
|
|
987
|
+
if (effects.length >= poolSize)
|
|
988
|
+
effects.shift();
|
|
989
|
+
effects.push(effect);
|
|
990
|
+
return publicGameEffectInstance(effect);
|
|
991
|
+
};
|
|
992
|
+
return {
|
|
993
|
+
spawn,
|
|
994
|
+
emit(combatEvents, effectOptions) {
|
|
995
|
+
const emitted = [];
|
|
996
|
+
for (const event of combatEvents) {
|
|
997
|
+
if (event.type === "hit")
|
|
998
|
+
emitted.push(spawn("hit-spark", event.position, { ownerId: event.attackerId, ...effectOptions }));
|
|
999
|
+
if (event.type === "blocked")
|
|
1000
|
+
emitted.push(spawn("block-spark", event.position, { ownerId: event.attackerId, ...effectOptions }));
|
|
1001
|
+
if (event.type === "push")
|
|
1002
|
+
emitted.push(spawn("ground-dust", event.position, { ownerId: event.attackerId, intensity: 0.45, duration: 0.14, ...effectOptions }));
|
|
1003
|
+
}
|
|
1004
|
+
return emitted;
|
|
1005
|
+
},
|
|
1006
|
+
hitSpark: (position, effectOptions) => spawn("hit-spark", position, effectOptions),
|
|
1007
|
+
blockSpark: (position, effectOptions) => spawn("block-spark", position, effectOptions),
|
|
1008
|
+
impactDecal: (position, effectOptions) => spawn("impact-decal", position, effectOptions),
|
|
1009
|
+
groundDust: (position, effectOptions) => spawn("ground-dust", position, effectOptions),
|
|
1010
|
+
dashTrail: (position, effectOptions) => spawn("dash-trail", position, effectOptions),
|
|
1011
|
+
slashTrail: (position, effectOptions) => spawn("slash-trail", position, effectOptions),
|
|
1012
|
+
impactFlash: (position, effectOptions) => spawn("impact-flash", position, effectOptions),
|
|
1013
|
+
auraBurst: (position, effectOptions) => spawn("aura-burst", position, effectOptions),
|
|
1014
|
+
shockwave: (position, effectOptions) => spawn("shockwave", position, effectOptions),
|
|
1015
|
+
ringShockwave: (position, effectOptions) => spawn("ring-shockwave", position, effectOptions),
|
|
1016
|
+
superFlash: (position, effectOptions) => spawn("super-flash", position, effectOptions),
|
|
1017
|
+
update(dt) {
|
|
1018
|
+
const seconds = Math.max(0, dt);
|
|
1019
|
+
effects = effects
|
|
1020
|
+
.map((effect) => ({
|
|
1021
|
+
...effect,
|
|
1022
|
+
position: resolveEffectAttachmentPosition(effect.attachment, effect.position),
|
|
1023
|
+
age: effect.age + seconds
|
|
1024
|
+
}))
|
|
1025
|
+
.filter((effect) => effect.age <= effect.duration);
|
|
1026
|
+
return snapshot();
|
|
1027
|
+
},
|
|
1028
|
+
snapshot,
|
|
1029
|
+
nodes() {
|
|
1030
|
+
return effects.map(effectToSceneNode);
|
|
1031
|
+
},
|
|
1032
|
+
clear() {
|
|
1033
|
+
effects = [];
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
export function applyGameCombatEventsToRuntime(events, options = {}) {
|
|
1038
|
+
const effectIds = [];
|
|
1039
|
+
let cameraImpacts = 0;
|
|
1040
|
+
for (const event of events) {
|
|
1041
|
+
if (event.type === "hit") {
|
|
1042
|
+
const effect = options.effects?.hitSpark(event.position, { ownerId: event.attackerId });
|
|
1043
|
+
if (effect)
|
|
1044
|
+
effectIds.push(effect.id);
|
|
1045
|
+
options.camera?.impact(1.1);
|
|
1046
|
+
if (options.camera)
|
|
1047
|
+
cameraImpacts += 1;
|
|
1048
|
+
}
|
|
1049
|
+
else if (event.type === "blocked") {
|
|
1050
|
+
const effect = options.effects?.blockSpark(event.position, { ownerId: event.attackerId });
|
|
1051
|
+
if (effect)
|
|
1052
|
+
effectIds.push(effect.id);
|
|
1053
|
+
options.camera?.impact(0.55);
|
|
1054
|
+
if (options.camera)
|
|
1055
|
+
cameraImpacts += 1;
|
|
1056
|
+
}
|
|
1057
|
+
else if (event.type === "push") {
|
|
1058
|
+
const effect = options.effects?.groundDust(event.position, { ownerId: event.attackerId, intensity: 0.45, duration: 0.14 });
|
|
1059
|
+
if (effect)
|
|
1060
|
+
effectIds.push(effect.id);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const hud = options.hudBindings && options.combat
|
|
1064
|
+
? createGameHudSnapshot({
|
|
1065
|
+
bindings: options.hudBindings,
|
|
1066
|
+
combat: options.combat,
|
|
1067
|
+
events,
|
|
1068
|
+
round: options.round,
|
|
1069
|
+
rules: options.rules,
|
|
1070
|
+
input: options.input,
|
|
1071
|
+
runtime: options.runtime,
|
|
1072
|
+
debug: options.debug,
|
|
1073
|
+
appState: options.appState
|
|
1074
|
+
})
|
|
1075
|
+
: undefined;
|
|
1076
|
+
return {
|
|
1077
|
+
kind: "aura-game-combat-event-runtime-bridge",
|
|
1078
|
+
events: [...events],
|
|
1079
|
+
effectIds,
|
|
1080
|
+
cameraImpacts,
|
|
1081
|
+
hud
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
export function createGameBoxCollider(options = {}) {
|
|
1085
|
+
const size = size3(options.size, [
|
|
1086
|
+
options.width ?? 1,
|
|
1087
|
+
options.height ?? 1,
|
|
1088
|
+
options.depth ?? (options.dimension === 2 ? 0 : 1)
|
|
1089
|
+
]);
|
|
1090
|
+
return {
|
|
1091
|
+
...colliderBase(options, size[2] === 0 ? 2 : 3),
|
|
1092
|
+
kind: "box",
|
|
1093
|
+
size
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
export function createGameSphereCollider(options = {}) {
|
|
1097
|
+
const radius = Math.max(0, options.radius ?? 0.5);
|
|
1098
|
+
return {
|
|
1099
|
+
...colliderBase(options, options.dimension ?? 3),
|
|
1100
|
+
kind: "sphere",
|
|
1101
|
+
radius
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
export function createGameCapsuleCollider(options = {}) {
|
|
1105
|
+
return {
|
|
1106
|
+
...colliderBase(options, options.dimension ?? 3),
|
|
1107
|
+
kind: "capsule",
|
|
1108
|
+
radius: Math.max(0, options.radius ?? 0.35),
|
|
1109
|
+
height: Math.max(0, options.height ?? 1.7),
|
|
1110
|
+
axis: options.axis ?? "y"
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
export function createGameRectCollider(options = {}) {
|
|
1114
|
+
return {
|
|
1115
|
+
...colliderBase(options, 2),
|
|
1116
|
+
kind: "rect",
|
|
1117
|
+
size: vec2(options.size, [options.width ?? 1, options.height ?? 1]),
|
|
1118
|
+
plane: options.plane ?? "xy"
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
export const gameColliders = {
|
|
1122
|
+
box: createGameBoxCollider,
|
|
1123
|
+
sphere: createGameSphereCollider,
|
|
1124
|
+
capsule: createGameCapsuleCollider,
|
|
1125
|
+
rect: createGameRectCollider
|
|
1126
|
+
};
|
|
1127
|
+
export function createGameCollisionBox(options = {}, tag = "hitbox") {
|
|
1128
|
+
const size = size3(options.size, [options.width ?? 1, options.height ?? 1, options.depth ?? 0.5]);
|
|
1129
|
+
return {
|
|
1130
|
+
id: options.id,
|
|
1131
|
+
offset: vec3Flexible(options.offset, [0, 0, 0]),
|
|
1132
|
+
center: options.center ? vec3Flexible(options.center, [0, 0, 0]) : undefined,
|
|
1133
|
+
size,
|
|
1134
|
+
tags: [...(options.tags ?? []), tag]
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
export function createGameHitboxRect(options = {}) {
|
|
1138
|
+
return createGameCollisionBox(options, "hitbox");
|
|
1139
|
+
}
|
|
1140
|
+
export function createGameHurtboxRect(options = {}) {
|
|
1141
|
+
return createGameCollisionBox(options, "hurtbox");
|
|
1142
|
+
}
|
|
1143
|
+
export function createGameGuardboxRect(options = {}) {
|
|
1144
|
+
return createGameCollisionBox(options, "guardbox");
|
|
1145
|
+
}
|
|
1146
|
+
export function createGamePushboxRect(options = {}) {
|
|
1147
|
+
return createGameCollisionBox(options, "pushbox");
|
|
1148
|
+
}
|
|
1149
|
+
export const gameHitboxes = {
|
|
1150
|
+
rect: createGameHitboxRect,
|
|
1151
|
+
box: createGameHitboxRect
|
|
1152
|
+
};
|
|
1153
|
+
export const gameHurtboxes = {
|
|
1154
|
+
rect: createGameHurtboxRect,
|
|
1155
|
+
box: createGameHurtboxRect
|
|
1156
|
+
};
|
|
1157
|
+
export const gameGuardboxes = {
|
|
1158
|
+
rect: createGameGuardboxRect,
|
|
1159
|
+
box: createGameGuardboxRect
|
|
1160
|
+
};
|
|
1161
|
+
export const gamePushboxes = {
|
|
1162
|
+
rect: createGamePushboxRect,
|
|
1163
|
+
box: createGamePushboxRect
|
|
1164
|
+
};
|
|
1165
|
+
export const gameTriggerVolumes = {
|
|
1166
|
+
box: (options = {}) => createGameBoxCollider({ ...options, sensor: true, tags: [...(options.tags ?? []), "trigger"] }),
|
|
1167
|
+
sphere: (options = {}) => createGameSphereCollider({ ...options, sensor: true, tags: [...(options.tags ?? []), "trigger"] }),
|
|
1168
|
+
capsule: (options = {}) => createGameCapsuleCollider({ ...options, sensor: true, tags: [...(options.tags ?? []), "trigger"] }),
|
|
1169
|
+
rect: (options = {}) => createGameRectCollider({ ...options, sensor: true, tags: [...(options.tags ?? []), "trigger"] })
|
|
1170
|
+
};
|
|
1171
|
+
export const gameEffectPresets = {
|
|
1172
|
+
hitSpark: (options = {}) => ({ kind: "hit-spark", options }),
|
|
1173
|
+
blockSpark: (options = {}) => ({ kind: "block-spark", options }),
|
|
1174
|
+
groundDust: (options = {}) => ({ kind: "ground-dust", options }),
|
|
1175
|
+
dashTrail: (options = {}) => ({ kind: "dash-trail", options }),
|
|
1176
|
+
slashTrail: (options = {}) => ({ kind: "slash-trail", options }),
|
|
1177
|
+
shockwave: (options = {}) => ({ kind: "shockwave", options }),
|
|
1178
|
+
auraBurst: (options = {}) => ({ kind: "aura-burst", options }),
|
|
1179
|
+
superBurst: (options = {}) => ({ kind: "super-flash", options })
|
|
1180
|
+
};
|
|
1181
|
+
export function gameColliderAabb(collider) {
|
|
1182
|
+
if (collider.kind === "box")
|
|
1183
|
+
return aabb(collider.center, collider.size);
|
|
1184
|
+
if (collider.kind === "sphere") {
|
|
1185
|
+
const diameter = collider.radius * 2;
|
|
1186
|
+
return aabb(collider.center, [diameter, diameter, collider.dimension === 2 ? 0 : diameter]);
|
|
1187
|
+
}
|
|
1188
|
+
if (collider.kind === "capsule") {
|
|
1189
|
+
const diameter = collider.radius * 2;
|
|
1190
|
+
const size = collider.axis === "x"
|
|
1191
|
+
? [collider.height, diameter, collider.dimension === 2 ? 0 : diameter]
|
|
1192
|
+
: collider.axis === "z"
|
|
1193
|
+
? [diameter, diameter, collider.height]
|
|
1194
|
+
: [diameter, collider.height, collider.dimension === 2 ? 0 : diameter];
|
|
1195
|
+
return aabb(collider.center, size);
|
|
1196
|
+
}
|
|
1197
|
+
const size = collider.plane === "xz"
|
|
1198
|
+
? [collider.size[0], 0, collider.size[1]]
|
|
1199
|
+
: collider.plane === "yz"
|
|
1200
|
+
? [0, collider.size[0], collider.size[1]]
|
|
1201
|
+
: [collider.size[0], collider.size[1], 0];
|
|
1202
|
+
return aabb(collider.center, size);
|
|
1203
|
+
}
|
|
1204
|
+
export function createGameColliderDebugGeometry(colliders, options = {}) {
|
|
1205
|
+
return colliders.map((collider, index) => debugGeometryFromCollider(collider, options, index));
|
|
1206
|
+
}
|
|
1207
|
+
export function createGameHitboxDebugGeometry(hitboxes, options = {}) {
|
|
1208
|
+
const origin = vec3(options.origin, [0, 0, 0]);
|
|
1209
|
+
const facing = options.facing ?? 1;
|
|
1210
|
+
const mirrorX = options.mirrorX ?? true;
|
|
1211
|
+
return hitboxes.map((hitbox, index) => {
|
|
1212
|
+
const offset = vec3(hitbox.offset ?? hitbox.center, [0, 0, 0]);
|
|
1213
|
+
const center = addVec3(origin, [offset[0] * (mirrorX ? facing : 1), offset[1], offset[2]]);
|
|
1214
|
+
return debugGeometryFromCollider(createGameBoxCollider({
|
|
1215
|
+
id: hitbox.id,
|
|
1216
|
+
center,
|
|
1217
|
+
size: hitbox.size,
|
|
1218
|
+
tags: hitbox.tags,
|
|
1219
|
+
dimension: hitbox.size[2] === 0 ? 2 : 3
|
|
1220
|
+
}), { color: "#ff5c8a", source: "hitbox", ...options }, index);
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
export function createGameCombatDebugGeometry(snapshot, options = {}) {
|
|
1224
|
+
const nodes = [];
|
|
1225
|
+
for (const actor of snapshot.actors) {
|
|
1226
|
+
nodes.push(...createGameHitboxDebugGeometry(actor.hurtboxes, {
|
|
1227
|
+
color: "#47d16c",
|
|
1228
|
+
source: `${actor.id}:hurtbox`,
|
|
1229
|
+
origin: actor.position,
|
|
1230
|
+
facing: actor.facing,
|
|
1231
|
+
...options
|
|
1232
|
+
}));
|
|
1233
|
+
nodes.push(...createGameHitboxDebugGeometry(actor.guardboxes, {
|
|
1234
|
+
color: "#5cc8ff",
|
|
1235
|
+
source: `${actor.id}:guardbox`,
|
|
1236
|
+
origin: actor.position,
|
|
1237
|
+
facing: actor.facing,
|
|
1238
|
+
...options
|
|
1239
|
+
}));
|
|
1240
|
+
nodes.push(...createGameHitboxDebugGeometry(actor.pushboxes, {
|
|
1241
|
+
color: "#ffd166",
|
|
1242
|
+
source: `${actor.id}:pushbox`,
|
|
1243
|
+
origin: actor.position,
|
|
1244
|
+
facing: actor.facing,
|
|
1245
|
+
...options
|
|
1246
|
+
}));
|
|
1247
|
+
}
|
|
1248
|
+
for (const attack of snapshot.activeAttacks) {
|
|
1249
|
+
const actor = snapshot.actors.find((candidate) => candidate.id === attack.attackerId);
|
|
1250
|
+
if (!actor)
|
|
1251
|
+
continue;
|
|
1252
|
+
const source = `${attack.attackerId}:${attack.moveId}:${attack.active ? "active-hitbox" : "inactive-hitbox"}`;
|
|
1253
|
+
const activeTags = [
|
|
1254
|
+
"attack-hitbox",
|
|
1255
|
+
attack.active ? "active-frame" : "inactive-frame",
|
|
1256
|
+
`move:${attack.moveId}`,
|
|
1257
|
+
`frame:${attack.frame}`,
|
|
1258
|
+
`active:${attack.activeFrames[0]}-${attack.activeFrames[1]}`
|
|
1259
|
+
];
|
|
1260
|
+
nodes.push(...createGameHitboxDebugGeometry(attack.hitboxes, {
|
|
1261
|
+
color: attack.active ? "#ff5c8a" : "#7c2d12",
|
|
1262
|
+
source,
|
|
1263
|
+
origin: actor.position,
|
|
1264
|
+
facing: actor.facing,
|
|
1265
|
+
...options
|
|
1266
|
+
}).map((node) => ({
|
|
1267
|
+
...node,
|
|
1268
|
+
tags: [...node.tags, ...activeTags]
|
|
1269
|
+
})));
|
|
1270
|
+
}
|
|
1271
|
+
snapshot.events.forEach((event, index) => {
|
|
1272
|
+
nodes.push(debugGeometryFromCollider(createGameSphereCollider({
|
|
1273
|
+
id: `contact:${event.frame}:${event.attackerId}:${event.targetId ?? "none"}:${index}`,
|
|
1274
|
+
center: event.position,
|
|
1275
|
+
radius: event.type === "hit" || event.type === "blocked" ? 0.075 : 0.045,
|
|
1276
|
+
tags: ["contact-point", `event:${event.type}`, `attacker:${event.attackerId}`, event.targetId ? `target:${event.targetId}` : "target:none"]
|
|
1277
|
+
}), {
|
|
1278
|
+
color: event.type === "blocked" ? "#5cc8ff" : event.type === "hit" ? "#ffffff" : "#ffd166",
|
|
1279
|
+
opacity: 0.82,
|
|
1280
|
+
wireframe: false,
|
|
1281
|
+
source: `contact:${event.type}`,
|
|
1282
|
+
...options
|
|
1283
|
+
}, index));
|
|
1284
|
+
});
|
|
1285
|
+
return nodes;
|
|
1286
|
+
}
|
|
1287
|
+
export function createGameDebugOverlayData(options = {}) {
|
|
1288
|
+
const inputSnapshot = options.input && "kind" in options.input && options.input.kind === "aura-game-input-snapshot"
|
|
1289
|
+
? options.input
|
|
1290
|
+
: options.input && "snapshot" in options.input
|
|
1291
|
+
? options.input.snapshot()
|
|
1292
|
+
: undefined;
|
|
1293
|
+
const bodySnapshots = options.bodySnapshots ?? options.bodies?.map((body) => body.snapshot()) ?? [];
|
|
1294
|
+
const combatSnapshot = options.combat && "kind" in options.combat && options.combat.kind === "aura-game-combat-world"
|
|
1295
|
+
? options.combat
|
|
1296
|
+
: options.combat && "snapshot" in options.combat
|
|
1297
|
+
? options.combat.snapshot()
|
|
1298
|
+
: undefined;
|
|
1299
|
+
const effectsSnapshot = options.effects && "kind" in options.effects && options.effects.kind === "aura-game-effects"
|
|
1300
|
+
? options.effects
|
|
1301
|
+
: options.effects && "snapshot" in options.effects
|
|
1302
|
+
? options.effects.snapshot()
|
|
1303
|
+
: undefined;
|
|
1304
|
+
const cameraSnapshot = options.camera && "kind" in options.camera && options.camera.kind === "aura-game-camera-director"
|
|
1305
|
+
? options.camera
|
|
1306
|
+
: options.camera && "snapshot" in options.camera
|
|
1307
|
+
? options.camera.snapshot()
|
|
1308
|
+
: undefined;
|
|
1309
|
+
const sections = [];
|
|
1310
|
+
if (options.runtime) {
|
|
1311
|
+
sections.push({
|
|
1312
|
+
id: "runtime",
|
|
1313
|
+
title: "Runtime",
|
|
1314
|
+
metrics: [
|
|
1315
|
+
{ id: "frame", label: "Frame", value: options.runtime.frame },
|
|
1316
|
+
{ id: "time", label: "Time", value: round(options.runtime.time, 3) },
|
|
1317
|
+
{ id: "paused", label: "Paused", value: options.runtime.paused ?? false }
|
|
1318
|
+
]
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
if (inputSnapshot) {
|
|
1322
|
+
sections.push({
|
|
1323
|
+
id: "input",
|
|
1324
|
+
title: "Input",
|
|
1325
|
+
metrics: [
|
|
1326
|
+
{ id: "inputFrame", label: "Input frame", value: inputSnapshot.frame },
|
|
1327
|
+
{ id: "bindings", label: "Active bindings", value: inputSnapshot.activeBindings.join(", ") || "none" },
|
|
1328
|
+
{ id: "actions", label: "Held actions", value: Object.entries(inputSnapshot.actions).filter(([, state]) => state.held).map(([action]) => action).join(", ") || "none" }
|
|
1329
|
+
]
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
if (bodySnapshots.length) {
|
|
1333
|
+
sections.push({
|
|
1334
|
+
id: "physics",
|
|
1335
|
+
title: "Physics",
|
|
1336
|
+
metrics: [
|
|
1337
|
+
{ id: "bodies", label: "Bodies", value: bodySnapshots.length },
|
|
1338
|
+
{ id: "grounded", label: "Grounded", value: bodySnapshots.filter((body) => body.grounded).length },
|
|
1339
|
+
{ id: "bufferedJumps", label: "Buffered jumps", value: bodySnapshots.filter((body) => body.jumpBuffered).length }
|
|
1340
|
+
]
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
if (combatSnapshot) {
|
|
1344
|
+
sections.push({
|
|
1345
|
+
id: "combat",
|
|
1346
|
+
title: "Combat",
|
|
1347
|
+
metrics: [
|
|
1348
|
+
{ id: "actors", label: "Actors", value: combatSnapshot.actors.length },
|
|
1349
|
+
{ id: "activeAttacks", label: "Active attacks", value: combatSnapshot.activeAttacks.length },
|
|
1350
|
+
{ id: "activeFrames", label: "Active frames", value: combatSnapshot.activeAttacks.filter((attack) => attack.active).map((attack) => `${attack.moveId}:${attack.frame}/${attack.activeFrames[0]}-${attack.activeFrames[1]}`).join(", ") || "none" },
|
|
1351
|
+
{ id: "contactPoints", label: "Contact points", value: combatSnapshot.events.filter((event) => event.type === "hit" || event.type === "blocked" || event.type === "push").length },
|
|
1352
|
+
{ id: "events", label: "Events", value: combatSnapshot.events.length }
|
|
1353
|
+
]
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
if (effectsSnapshot) {
|
|
1357
|
+
sections.push({
|
|
1358
|
+
id: "effects",
|
|
1359
|
+
title: "Effects",
|
|
1360
|
+
metrics: [
|
|
1361
|
+
{ id: "active", label: "Active", value: effectsSnapshot.active },
|
|
1362
|
+
{ id: "spawned", label: "Spawned", value: effectsSnapshot.spawned },
|
|
1363
|
+
{ id: "pooled", label: "Pool", value: effectsSnapshot.pooled }
|
|
1364
|
+
]
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
if (cameraSnapshot) {
|
|
1368
|
+
sections.push({
|
|
1369
|
+
id: "camera",
|
|
1370
|
+
title: "Camera",
|
|
1371
|
+
metrics: [
|
|
1372
|
+
{ id: "fov", label: "FOV", value: round(cameraSnapshot.fov, 2) },
|
|
1373
|
+
{ id: "zoom", label: "Zoom", value: round(cameraSnapshot.zoom, 3) },
|
|
1374
|
+
{ id: "shake", label: "Shake", value: round(cameraSnapshot.shake, 3) }
|
|
1375
|
+
]
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
const geometry = [
|
|
1379
|
+
...createGameColliderDebugGeometry(options.colliders ?? [], { source: "collider" }),
|
|
1380
|
+
...createGameHitboxDebugGeometry(options.hitboxes ?? [], { source: "hitbox" }),
|
|
1381
|
+
...(combatSnapshot ? createGameCombatDebugGeometry(combatSnapshot) : [])
|
|
1382
|
+
];
|
|
1383
|
+
return {
|
|
1384
|
+
kind: "aura-game-debug-overlay",
|
|
1385
|
+
frame: options.runtime?.frame ?? inputSnapshot?.frame ?? combatSnapshot?.frame ?? 0,
|
|
1386
|
+
time: options.runtime?.time ?? inputSnapshot?.time ?? combatSnapshot?.time ?? 0,
|
|
1387
|
+
paused: options.runtime?.paused ?? false,
|
|
1388
|
+
sections,
|
|
1389
|
+
geometry,
|
|
1390
|
+
labels: options.labels ?? {},
|
|
1391
|
+
warnings: options.warnings ?? []
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
export function createGameDebugSceneNodes(debug, options = {}) {
|
|
1395
|
+
const geometry = Array.isArray(debug)
|
|
1396
|
+
? debug
|
|
1397
|
+
: debug.geometry;
|
|
1398
|
+
const runtimePrefix = options.runtimePrefix ?? "debug";
|
|
1399
|
+
const primitives = geometry.map((node) => ({
|
|
1400
|
+
kind: "primitive",
|
|
1401
|
+
primitive: debugScenePrimitive(node.primitive),
|
|
1402
|
+
name: `${runtimePrefix}:${node.id}`,
|
|
1403
|
+
position: node.position,
|
|
1404
|
+
scale: debugSceneScale(node),
|
|
1405
|
+
material: {
|
|
1406
|
+
color: node.color,
|
|
1407
|
+
emissive: node.color,
|
|
1408
|
+
emissiveIntensity: node.tags.includes("contact-point") ? 0.95 : 0.38,
|
|
1409
|
+
opacity: node.opacity,
|
|
1410
|
+
transparent: node.opacity < 1,
|
|
1411
|
+
wireframe: node.wireframe
|
|
1412
|
+
},
|
|
1413
|
+
runtime: {
|
|
1414
|
+
id: `${runtimePrefix}:${node.id}`,
|
|
1415
|
+
mutable: true,
|
|
1416
|
+
tags: ["debug", "game-runtime", node.source, ...node.tags]
|
|
1417
|
+
},
|
|
1418
|
+
debug: {
|
|
1419
|
+
source: node.source,
|
|
1420
|
+
tags: node.tags,
|
|
1421
|
+
aabb: node.aabb
|
|
1422
|
+
}
|
|
1423
|
+
}));
|
|
1424
|
+
if (!options.includeLabels)
|
|
1425
|
+
return primitives;
|
|
1426
|
+
const labels = geometry
|
|
1427
|
+
.filter((node) => node.tags.includes("active-frame") || node.tags.includes("contact-point"))
|
|
1428
|
+
.map((node) => ({
|
|
1429
|
+
kind: "label",
|
|
1430
|
+
name: `${runtimePrefix}:${node.id}:label`,
|
|
1431
|
+
position: [node.position[0], node.position[1] + Math.max(0.08, node.scale[1] * 0.5 + 0.08), node.position[2]],
|
|
1432
|
+
text: node.tags.includes("contact-point") ? node.source : `${node.source} ${node.tags.find((tag) => tag.startsWith("frame:")) ?? ""}`.trim(),
|
|
1433
|
+
color: node.color,
|
|
1434
|
+
background: "#07111f",
|
|
1435
|
+
size: 0.12,
|
|
1436
|
+
runtime: {
|
|
1437
|
+
id: `${runtimePrefix}:${node.id}:label`,
|
|
1438
|
+
mutable: true,
|
|
1439
|
+
tags: ["debug", "game-runtime", "label", node.source, ...node.tags]
|
|
1440
|
+
},
|
|
1441
|
+
debug: {
|
|
1442
|
+
source: node.source,
|
|
1443
|
+
tags: node.tags,
|
|
1444
|
+
aabb: node.aabb
|
|
1445
|
+
}
|
|
1446
|
+
}));
|
|
1447
|
+
return [...primitives, ...labels];
|
|
1448
|
+
}
|
|
1449
|
+
function publicGameEffectInstance(effect) {
|
|
1450
|
+
const { attachment: _attachment, ...publicEffect } = effect;
|
|
1451
|
+
return { ...publicEffect };
|
|
1452
|
+
}
|
|
1453
|
+
function resolveEffectAttachmentPosition(attachment, fallback) {
|
|
1454
|
+
if (!attachment?.getPosition)
|
|
1455
|
+
return fallback;
|
|
1456
|
+
return addVec3(attachment.getPosition(), vec3(attachment.offset, [0, 0, 0]));
|
|
1457
|
+
}
|
|
1458
|
+
function normalizeActor(actor) {
|
|
1459
|
+
return {
|
|
1460
|
+
id: actor.id,
|
|
1461
|
+
team: actor.team ?? actor.id,
|
|
1462
|
+
position: vec3(actor.position, [0, 0, 0]),
|
|
1463
|
+
facing: actor.facing ?? 1,
|
|
1464
|
+
health: actor.health ?? 100,
|
|
1465
|
+
guard: actor.guard ?? 100,
|
|
1466
|
+
meter: actor.meter ?? 0,
|
|
1467
|
+
hurtboxes: actor.hurtboxes ?? [{ id: "body", offset: [0, 0.85, 0], size: [0.62, 1.55, 0.48] }],
|
|
1468
|
+
guardboxes: actor.guardboxes ?? [{ id: "guard", offset: [0.22, 0.92, 0], size: [0.54, 1.35, 0.54] }],
|
|
1469
|
+
pushboxes: actor.pushboxes ?? [{ id: "push", offset: [0, 0.72, 0], size: [0.72, 1.2, 0.52] }],
|
|
1470
|
+
guarding: actor.guarding ?? false,
|
|
1471
|
+
stun: 0,
|
|
1472
|
+
recovery: 0
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function normalizeCombatMove(move) {
|
|
1476
|
+
return {
|
|
1477
|
+
...move,
|
|
1478
|
+
hitboxes: moveHitboxes(move)
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
function moveHitboxes(move) {
|
|
1482
|
+
return move.hitboxes ?? (move.hitbox ? [move.hitbox] : []);
|
|
1483
|
+
}
|
|
1484
|
+
function moveActiveFrames(move, fps) {
|
|
1485
|
+
if (move.activeFrames)
|
|
1486
|
+
return move.activeFrames;
|
|
1487
|
+
if (move.startup !== undefined || move.active !== undefined) {
|
|
1488
|
+
const startupFrames = Math.max(0, Math.round((move.startup ?? 0) * fps));
|
|
1489
|
+
const activeFrames = Math.max(1, Math.round((move.active ?? 1 / fps) * fps));
|
|
1490
|
+
return [startupFrames + 1, startupFrames + activeFrames];
|
|
1491
|
+
}
|
|
1492
|
+
return [1, move.durationFrames ?? 12];
|
|
1493
|
+
}
|
|
1494
|
+
function moveDurationFrames(move, activeFrames, fps) {
|
|
1495
|
+
if (move.durationFrames !== undefined)
|
|
1496
|
+
return move.durationFrames;
|
|
1497
|
+
const secondsMode = move.startup !== undefined || move.active !== undefined;
|
|
1498
|
+
const recoveryFrames = secondsMode ? Math.max(0, Math.round((move.recovery ?? 0) * fps)) : move.recovery ?? 8;
|
|
1499
|
+
return activeFrames[1] + recoveryFrames;
|
|
1500
|
+
}
|
|
1501
|
+
function isGameKinematicBody(fighter) {
|
|
1502
|
+
return typeof fighter.update === "function" && typeof fighter.snapshot === "function";
|
|
1503
|
+
}
|
|
1504
|
+
function syncActorsFromBodies(bodies, actors) {
|
|
1505
|
+
for (const [id, body] of bodies) {
|
|
1506
|
+
const actor = actors.get(id);
|
|
1507
|
+
if (!actor)
|
|
1508
|
+
continue;
|
|
1509
|
+
actor.position = body.position;
|
|
1510
|
+
actor.facing = body.facing;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
function syncBodiesFromActors(bodies, actors) {
|
|
1514
|
+
for (const [id, body] of bodies) {
|
|
1515
|
+
const actor = actors.get(id);
|
|
1516
|
+
if (actor)
|
|
1517
|
+
body.position = actor.position;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
function resolvePushboxSeparation(actors, stageBounds, frame, time, events) {
|
|
1521
|
+
const actorList = [...actors.values()];
|
|
1522
|
+
for (let leftIndex = 0; leftIndex < actorList.length; leftIndex += 1) {
|
|
1523
|
+
for (let rightIndex = leftIndex + 1; rightIndex < actorList.length; rightIndex += 1) {
|
|
1524
|
+
const left = actorList[leftIndex];
|
|
1525
|
+
const right = actorList[rightIndex];
|
|
1526
|
+
if (!left || !right || left.team === right.team)
|
|
1527
|
+
continue;
|
|
1528
|
+
const leftBox = left.pushboxes[0] ? worldBox(left, left.pushboxes[0], false) : undefined;
|
|
1529
|
+
const rightBox = right.pushboxes[0] ? worldBox(right, right.pushboxes[0], false) : undefined;
|
|
1530
|
+
if (!leftBox || !rightBox || !intersects(leftBox, rightBox))
|
|
1531
|
+
continue;
|
|
1532
|
+
const overlap = Math.min(leftBox.max[0] - rightBox.min[0], rightBox.max[0] - leftBox.min[0]);
|
|
1533
|
+
if (overlap <= 0)
|
|
1534
|
+
continue;
|
|
1535
|
+
const direction = left.position[0] <= right.position[0] ? -1 : 1;
|
|
1536
|
+
left.position = clampVec3ToBounds([left.position[0] + direction * overlap * 0.5, left.position[1], left.position[2]], stageBounds);
|
|
1537
|
+
right.position = clampVec3ToBounds([right.position[0] - direction * overlap * 0.5, right.position[1], right.position[2]], stageBounds);
|
|
1538
|
+
events.push({
|
|
1539
|
+
type: "push",
|
|
1540
|
+
frame,
|
|
1541
|
+
time,
|
|
1542
|
+
attackerId: left.id,
|
|
1543
|
+
targetId: right.id,
|
|
1544
|
+
position: [(left.position[0] + right.position[0]) / 2, (left.position[1] + right.position[1]) / 2 + 0.72, (left.position[2] + right.position[2]) / 2]
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
function actorSnapshot(actor) {
|
|
1550
|
+
return {
|
|
1551
|
+
id: actor.id,
|
|
1552
|
+
team: actor.team,
|
|
1553
|
+
position: actor.position,
|
|
1554
|
+
facing: actor.facing,
|
|
1555
|
+
health: actor.health,
|
|
1556
|
+
guard: actor.guard,
|
|
1557
|
+
meter: actor.meter,
|
|
1558
|
+
hurtboxes: actor.hurtboxes,
|
|
1559
|
+
guardboxes: actor.guardboxes,
|
|
1560
|
+
pushboxes: actor.pushboxes,
|
|
1561
|
+
guarding: actor.guarding,
|
|
1562
|
+
stun: actor.stun,
|
|
1563
|
+
recovery: actor.recovery
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function resolveAttack(attacker, attack, actors, stageBounds, frame, time, events) {
|
|
1567
|
+
for (const target of actors.values()) {
|
|
1568
|
+
if (target.id === attacker.id || target.team === attacker.team || attack.hitTargets.has(target.id))
|
|
1569
|
+
continue;
|
|
1570
|
+
for (const hitbox of moveHitboxes(attack.move)) {
|
|
1571
|
+
const worldHitbox = worldBox(attacker, hitbox, true);
|
|
1572
|
+
const guarded = target.guarding &&
|
|
1573
|
+
attack.move.blockable !== false &&
|
|
1574
|
+
target.guardboxes.some((guardbox) => intersects(worldHitbox, worldBox(target, guardbox, false)));
|
|
1575
|
+
const hurt = target.hurtboxes.some((hurtbox) => intersects(worldHitbox, worldBox(target, hurtbox, false)));
|
|
1576
|
+
if (!guarded && !hurt)
|
|
1577
|
+
continue;
|
|
1578
|
+
attack.hitTargets.add(target.id);
|
|
1579
|
+
const position = [
|
|
1580
|
+
(attacker.position[0] + target.position[0]) / 2,
|
|
1581
|
+
(attacker.position[1] + target.position[1]) / 2 + 0.9,
|
|
1582
|
+
(attacker.position[2] + target.position[2]) / 2
|
|
1583
|
+
];
|
|
1584
|
+
if (guarded) {
|
|
1585
|
+
const guardDamage = attack.move.guardDamage ?? Math.ceil((attack.move.damage ?? 8) * 0.4);
|
|
1586
|
+
target.guard = Math.max(0, target.guard - guardDamage);
|
|
1587
|
+
target.stun = Math.max(target.stun, attack.move.blockStun ?? 8);
|
|
1588
|
+
events.push({
|
|
1589
|
+
type: "blocked",
|
|
1590
|
+
frame,
|
|
1591
|
+
time,
|
|
1592
|
+
attackerId: attacker.id,
|
|
1593
|
+
targetId: target.id,
|
|
1594
|
+
moveId: attack.move.id,
|
|
1595
|
+
guardDamage,
|
|
1596
|
+
hitStop: attack.move.hitStop ?? 0.045,
|
|
1597
|
+
stun: target.stun,
|
|
1598
|
+
position
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
else {
|
|
1602
|
+
const damage = attack.move.damage ?? 8;
|
|
1603
|
+
const baseKnockback = vec3Flexible(attack.move.knockback, [0.08, 0, 0]);
|
|
1604
|
+
const knockback = [baseKnockback[0] * attacker.facing, baseKnockback[1], baseKnockback[2]];
|
|
1605
|
+
target.health = Math.max(0, target.health - damage);
|
|
1606
|
+
target.stun = Math.max(target.stun, attack.move.hitStun ?? 12);
|
|
1607
|
+
target.recovery = Math.max(target.recovery, attack.move.recovery ?? 8);
|
|
1608
|
+
target.position = clampVec3ToBounds(addVec3(target.position, knockback), stageBounds);
|
|
1609
|
+
attacker.meter += attack.move.meterGain ?? damage * 0.15;
|
|
1610
|
+
events.push({
|
|
1611
|
+
type: "hit",
|
|
1612
|
+
frame,
|
|
1613
|
+
time,
|
|
1614
|
+
attackerId: attacker.id,
|
|
1615
|
+
targetId: target.id,
|
|
1616
|
+
moveId: attack.move.id,
|
|
1617
|
+
damage,
|
|
1618
|
+
hitStop: attack.move.hitStop ?? 0.06,
|
|
1619
|
+
stun: target.stun,
|
|
1620
|
+
knockback,
|
|
1621
|
+
position
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function worldBox(actor, box, mirrorX) {
|
|
1629
|
+
const offset = vec3(box.offset ?? box.center, [0, 0, 0]);
|
|
1630
|
+
const signedOffset = [offset[0] * (mirrorX ? actor.facing : 1), offset[1], offset[2]];
|
|
1631
|
+
return aabb(addVec3(actor.position, signedOffset), box.size);
|
|
1632
|
+
}
|
|
1633
|
+
function normalizeReplayEvents(events) {
|
|
1634
|
+
return [...events]
|
|
1635
|
+
.map((event) => ({
|
|
1636
|
+
frame: Math.max(0, Math.floor(event.frame)),
|
|
1637
|
+
time: Math.max(0, event.time),
|
|
1638
|
+
type: event.type,
|
|
1639
|
+
binding: event.binding
|
|
1640
|
+
}))
|
|
1641
|
+
.sort((a, b) => a.frame - b.frame || a.time - b.time || a.binding.localeCompare(b.binding) || a.type.localeCompare(b.type));
|
|
1642
|
+
}
|
|
1643
|
+
function replayChecksum(events, seed, fps) {
|
|
1644
|
+
let hash = 2166136261 ^ seed ^ Math.floor(fps * 1000);
|
|
1645
|
+
for (const event of events) {
|
|
1646
|
+
const chunk = `${event.frame}:${event.time.toFixed(6)}:${event.type}:${event.binding}`;
|
|
1647
|
+
for (let index = 0; index < chunk.length; index += 1) {
|
|
1648
|
+
hash ^= chunk.charCodeAt(index);
|
|
1649
|
+
hash = Math.imul(hash, 16777619);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
1653
|
+
}
|
|
1654
|
+
function colliderBase(options, fallbackDimension) {
|
|
1655
|
+
return {
|
|
1656
|
+
id: options.id,
|
|
1657
|
+
center: vec3(options.center ?? options.offset, [0, 0, 0]),
|
|
1658
|
+
offset: options.offset,
|
|
1659
|
+
tags: options.tags ?? [],
|
|
1660
|
+
dimension: options.dimension ?? fallbackDimension,
|
|
1661
|
+
sensor: options.sensor ?? false
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
function debugGeometryFromCollider(collider, options, index) {
|
|
1665
|
+
const aabbBox = gameColliderAabb(collider);
|
|
1666
|
+
return {
|
|
1667
|
+
kind: "aura-game-debug-geometry",
|
|
1668
|
+
id: collider.id ?? `${options.source ?? collider.kind}:${index}`,
|
|
1669
|
+
primitive: collider.kind,
|
|
1670
|
+
position: collider.center,
|
|
1671
|
+
scale: debugGeometryScale(collider),
|
|
1672
|
+
radius: collider.kind === "sphere" || collider.kind === "capsule" ? collider.radius : undefined,
|
|
1673
|
+
height: collider.kind === "capsule" ? collider.height : undefined,
|
|
1674
|
+
axis: collider.kind === "capsule" ? collider.axis : undefined,
|
|
1675
|
+
plane: collider.kind === "rect" ? collider.plane : undefined,
|
|
1676
|
+
color: options.color ?? defaultDebugColor(options.source ?? collider.kind),
|
|
1677
|
+
opacity: options.opacity ?? 0.35,
|
|
1678
|
+
wireframe: options.wireframe ?? true,
|
|
1679
|
+
source: options.source ?? collider.kind,
|
|
1680
|
+
tags: collider.tags,
|
|
1681
|
+
aabb: aabbBox
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
function debugGeometryScale(collider) {
|
|
1685
|
+
if (collider.kind === "box")
|
|
1686
|
+
return collider.size;
|
|
1687
|
+
if (collider.kind === "sphere") {
|
|
1688
|
+
const diameter = collider.radius * 2;
|
|
1689
|
+
return [diameter, diameter, collider.dimension === 2 ? 0 : diameter];
|
|
1690
|
+
}
|
|
1691
|
+
if (collider.kind === "capsule")
|
|
1692
|
+
return gameColliderAabb(collider).size;
|
|
1693
|
+
if (collider.plane === "xz")
|
|
1694
|
+
return [collider.size[0], 0, collider.size[1]];
|
|
1695
|
+
if (collider.plane === "yz")
|
|
1696
|
+
return [0, collider.size[0], collider.size[1]];
|
|
1697
|
+
return [collider.size[0], collider.size[1], 0];
|
|
1698
|
+
}
|
|
1699
|
+
function debugScenePrimitive(primitive) {
|
|
1700
|
+
return primitive === "rect" ? "plane" : primitive;
|
|
1701
|
+
}
|
|
1702
|
+
function debugSceneScale(node) {
|
|
1703
|
+
if (node.primitive === "sphere")
|
|
1704
|
+
return node.radius ? node.radius * 2 : node.scale;
|
|
1705
|
+
return node.scale;
|
|
1706
|
+
}
|
|
1707
|
+
function defaultDebugColor(source) {
|
|
1708
|
+
if (source.includes("hurt"))
|
|
1709
|
+
return "#47d16c";
|
|
1710
|
+
if (source.includes("guard"))
|
|
1711
|
+
return "#5cc8ff";
|
|
1712
|
+
if (source.includes("push"))
|
|
1713
|
+
return "#ffd166";
|
|
1714
|
+
if (source.includes("hit"))
|
|
1715
|
+
return "#ff5c8a";
|
|
1716
|
+
if (source.includes("sensor"))
|
|
1717
|
+
return "#b78cff";
|
|
1718
|
+
return "#7dd3fc";
|
|
1719
|
+
}
|
|
1720
|
+
function aabb(center, size) {
|
|
1721
|
+
const half = [size[0] / 2, size[1] / 2, size[2] / 2];
|
|
1722
|
+
return {
|
|
1723
|
+
center,
|
|
1724
|
+
size,
|
|
1725
|
+
min: [center[0] - half[0], center[1] - half[1], center[2] - half[2]],
|
|
1726
|
+
max: [center[0] + half[0], center[1] + half[1], center[2] + half[2]]
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function intersects(a, b) {
|
|
1730
|
+
return (a.min[0] <= b.max[0] &&
|
|
1731
|
+
a.max[0] >= b.min[0] &&
|
|
1732
|
+
a.min[1] <= b.max[1] &&
|
|
1733
|
+
a.max[1] >= b.min[1] &&
|
|
1734
|
+
a.min[2] <= b.max[2] &&
|
|
1735
|
+
a.max[2] >= b.min[2]);
|
|
1736
|
+
}
|
|
1737
|
+
function vec3(value, fallback) {
|
|
1738
|
+
return value ? [value[0], value[1], value[2]] : fallback;
|
|
1739
|
+
}
|
|
1740
|
+
function vec3Flexible(value, fallback) {
|
|
1741
|
+
if (!value)
|
|
1742
|
+
return fallback;
|
|
1743
|
+
return value.length === 2 ? [value[0], value[1], fallback[2]] : [value[0], value[1], value[2]];
|
|
1744
|
+
}
|
|
1745
|
+
function vec2(value, fallback) {
|
|
1746
|
+
return value ? [value[0], value[1]] : fallback;
|
|
1747
|
+
}
|
|
1748
|
+
function size3(value, fallback) {
|
|
1749
|
+
if (!value)
|
|
1750
|
+
return fallback;
|
|
1751
|
+
return value.length === 2 ? [value[0], value[1], 0] : [value[0], value[1], value[2]];
|
|
1752
|
+
}
|
|
1753
|
+
function kinematicSizeFromCollider(collider) {
|
|
1754
|
+
if (!collider)
|
|
1755
|
+
return undefined;
|
|
1756
|
+
return gameColliderAabb(collider).size;
|
|
1757
|
+
}
|
|
1758
|
+
function addVec3(a, b) {
|
|
1759
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
1760
|
+
}
|
|
1761
|
+
function scaleVec3(value, scale) {
|
|
1762
|
+
return [value[0] * scale, value[1] * scale, value[2] * scale];
|
|
1763
|
+
}
|
|
1764
|
+
function normalizeVec3(value) {
|
|
1765
|
+
const length = Math.hypot(value[0], value[1], value[2]);
|
|
1766
|
+
return length <= 0.0001 ? [0, 0, 0] : [value[0] / length, value[1] / length, value[2] / length];
|
|
1767
|
+
}
|
|
1768
|
+
function clamp(value, min, max) {
|
|
1769
|
+
return Math.max(min, Math.min(max, value));
|
|
1770
|
+
}
|
|
1771
|
+
function clampVec3ToBounds(value, bounds) {
|
|
1772
|
+
return [
|
|
1773
|
+
clamp(value[0], bounds.minX ?? Number.NEGATIVE_INFINITY, bounds.maxX ?? Number.POSITIVE_INFINITY),
|
|
1774
|
+
clamp(value[1], bounds.minY ?? Number.NEGATIVE_INFINITY, bounds.maxY ?? Number.POSITIVE_INFINITY),
|
|
1775
|
+
clamp(value[2], bounds.minZ ?? Number.NEGATIVE_INFINITY, bounds.maxZ ?? Number.POSITIVE_INFINITY)
|
|
1776
|
+
];
|
|
1777
|
+
}
|
|
1778
|
+
function round(value, places) {
|
|
1779
|
+
const scale = 10 ** places;
|
|
1780
|
+
return Math.round(value * scale) / scale;
|
|
1781
|
+
}
|
|
1782
|
+
function applyDeadzone(value, deadzone, mode = "axial") {
|
|
1783
|
+
const nextDeadzone = clamp(deadzone, 0, 0.99);
|
|
1784
|
+
const magnitude = Math.abs(value);
|
|
1785
|
+
if (magnitude < nextDeadzone)
|
|
1786
|
+
return 0;
|
|
1787
|
+
if (mode === "scaled")
|
|
1788
|
+
return Math.sign(value) * clamp((magnitude - nextDeadzone) / (1 - nextDeadzone), 0, 1);
|
|
1789
|
+
return value;
|
|
1790
|
+
}
|
|
1791
|
+
function gamepadButtonNames(index, gamepadIndex) {
|
|
1792
|
+
const names = [
|
|
1793
|
+
"GamepadA",
|
|
1794
|
+
"GamepadB",
|
|
1795
|
+
"GamepadX",
|
|
1796
|
+
"GamepadY",
|
|
1797
|
+
"GamepadLB",
|
|
1798
|
+
"GamepadRB",
|
|
1799
|
+
"GamepadLT",
|
|
1800
|
+
"GamepadRT",
|
|
1801
|
+
"GamepadBack",
|
|
1802
|
+
"GamepadStart",
|
|
1803
|
+
"GamepadLStick",
|
|
1804
|
+
"GamepadRStick",
|
|
1805
|
+
"GamepadDPadUp",
|
|
1806
|
+
"GamepadDPadDown",
|
|
1807
|
+
"GamepadDPadLeft",
|
|
1808
|
+
"GamepadDPadRight"
|
|
1809
|
+
];
|
|
1810
|
+
return [`GamepadButton${index}`, `Gamepad${gamepadIndex}:Button${index}`, names[index] ?? `GamepadButton${index}`];
|
|
1811
|
+
}
|
|
1812
|
+
function defaultEffectColor(kind) {
|
|
1813
|
+
if (kind === "block-spark")
|
|
1814
|
+
return "#8ee7ff";
|
|
1815
|
+
if (kind === "impact-decal")
|
|
1816
|
+
return "#ff8b5c";
|
|
1817
|
+
if (kind === "ground-dust")
|
|
1818
|
+
return "#c7b38f";
|
|
1819
|
+
if (kind === "dash-trail")
|
|
1820
|
+
return "#62f6c8";
|
|
1821
|
+
if (kind === "slash-trail")
|
|
1822
|
+
return "#d7fbff";
|
|
1823
|
+
if (kind === "aura-burst")
|
|
1824
|
+
return "#5cff87";
|
|
1825
|
+
if (kind === "shockwave" || kind === "ring-shockwave")
|
|
1826
|
+
return "#ffd166";
|
|
1827
|
+
if (kind === "impact-flash" || kind === "super-flash")
|
|
1828
|
+
return "#ffffff";
|
|
1829
|
+
return "#ffb84d";
|
|
1830
|
+
}
|
|
1831
|
+
function defaultEffectDuration(kind) {
|
|
1832
|
+
if (kind === "impact-decal")
|
|
1833
|
+
return 0.7;
|
|
1834
|
+
if (kind === "ground-dust")
|
|
1835
|
+
return 0.34;
|
|
1836
|
+
if (kind === "dash-trail")
|
|
1837
|
+
return 0.28;
|
|
1838
|
+
if (kind === "slash-trail")
|
|
1839
|
+
return 0.18;
|
|
1840
|
+
if (kind === "aura-burst")
|
|
1841
|
+
return 0.9;
|
|
1842
|
+
if (kind === "shockwave" || kind === "ring-shockwave")
|
|
1843
|
+
return 0.42;
|
|
1844
|
+
if (kind === "impact-flash")
|
|
1845
|
+
return 0.12;
|
|
1846
|
+
if (kind === "super-flash")
|
|
1847
|
+
return 0.2;
|
|
1848
|
+
return 0.22;
|
|
1849
|
+
}
|
|
1850
|
+
function defaultEffectRadius(kind) {
|
|
1851
|
+
if (kind === "impact-decal")
|
|
1852
|
+
return 0.42;
|
|
1853
|
+
if (kind === "aura-burst")
|
|
1854
|
+
return 1.4;
|
|
1855
|
+
if (kind === "shockwave" || kind === "ring-shockwave")
|
|
1856
|
+
return 1.05;
|
|
1857
|
+
if (kind === "dash-trail")
|
|
1858
|
+
return 0.52;
|
|
1859
|
+
if (kind === "slash-trail")
|
|
1860
|
+
return 0.62;
|
|
1861
|
+
if (kind === "ground-dust")
|
|
1862
|
+
return 0.36;
|
|
1863
|
+
if (kind === "super-flash")
|
|
1864
|
+
return 1.2;
|
|
1865
|
+
return 0.28;
|
|
1866
|
+
}
|
|
1867
|
+
function effectToSceneNode(effect) {
|
|
1868
|
+
const life = Math.max(0, 1 - effect.age / Math.max(0.001, effect.duration));
|
|
1869
|
+
const ring = effect.kind === "shockwave" || effect.kind === "ring-shockwave";
|
|
1870
|
+
const trail = effect.kind === "dash-trail" || effect.kind === "slash-trail";
|
|
1871
|
+
const scalarScale = Math.max(0.04, effect.radius * (ring ? 1.4 - life * 0.4 : life));
|
|
1872
|
+
const trailScale = [
|
|
1873
|
+
Math.max(0.08, effect.radius * life * 1.6),
|
|
1874
|
+
Math.max(0.02, effect.radius * life * 0.18),
|
|
1875
|
+
Math.max(0.04, effect.radius * life * 0.32)
|
|
1876
|
+
];
|
|
1877
|
+
if (effect.kind === "aura-burst") {
|
|
1878
|
+
return {
|
|
1879
|
+
kind: "effect",
|
|
1880
|
+
effect: "particles",
|
|
1881
|
+
name: effect.id,
|
|
1882
|
+
color: effect.color,
|
|
1883
|
+
intensity: effect.intensity * life,
|
|
1884
|
+
particleCount: Math.round(160 + effect.intensity * 220),
|
|
1885
|
+
emitter: "swirl",
|
|
1886
|
+
radius: effect.radius,
|
|
1887
|
+
height: 1.4,
|
|
1888
|
+
materialMode: "additive-glow"
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
return {
|
|
1892
|
+
kind: "primitive",
|
|
1893
|
+
primitive: ring ? "torus" : trail ? "box" : "sphere",
|
|
1894
|
+
name: effect.id,
|
|
1895
|
+
position: effect.position,
|
|
1896
|
+
scale: trail ? trailScale : scalarScale,
|
|
1897
|
+
material: {
|
|
1898
|
+
color: effect.color,
|
|
1899
|
+
emissive: effect.color,
|
|
1900
|
+
emissiveIntensity: effect.intensity * life,
|
|
1901
|
+
opacity: life
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
export function createGameHudHealthBinding(options) {
|
|
1906
|
+
return createGameHudBinding({
|
|
1907
|
+
binding: "health",
|
|
1908
|
+
id: options.id ?? `hud:${options.actorId}:health`,
|
|
1909
|
+
label: options.label ?? `${options.actorId} health`,
|
|
1910
|
+
source: "combat",
|
|
1911
|
+
targetId: options.actorId,
|
|
1912
|
+
valuePath: options.valuePath ?? `combat.actors.${options.actorId}.health`,
|
|
1913
|
+
maxPath: options.maxPath ?? "rules.maxHealth",
|
|
1914
|
+
format: "percent",
|
|
1915
|
+
a11yLabel: options.a11yLabel ?? `${options.actorId} health value`,
|
|
1916
|
+
visibleWhen: options.visibleWhen
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
export function createGameHudMeterBinding(options) {
|
|
1920
|
+
return createGameHudBinding({
|
|
1921
|
+
binding: "meter",
|
|
1922
|
+
id: options.id ?? `hud:${options.actorId}:meter`,
|
|
1923
|
+
label: options.label ?? `${options.actorId} meter`,
|
|
1924
|
+
source: "combat",
|
|
1925
|
+
targetId: options.actorId,
|
|
1926
|
+
valuePath: options.valuePath ?? `combat.actors.${options.actorId}.meter`,
|
|
1927
|
+
maxPath: options.maxPath ?? "rules.maxMeter",
|
|
1928
|
+
format: "percent",
|
|
1929
|
+
a11yLabel: options.a11yLabel ?? `${options.actorId} super meter value`,
|
|
1930
|
+
visibleWhen: options.visibleWhen
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
export function createGameHudTimerBinding(options = {}) {
|
|
1934
|
+
return createGameHudBinding({
|
|
1935
|
+
binding: "timer",
|
|
1936
|
+
id: options.id ?? "hud:round:timer",
|
|
1937
|
+
label: options.label ?? "round timer",
|
|
1938
|
+
source: "round",
|
|
1939
|
+
valuePath: options.valuePath ?? "round.timeRemaining",
|
|
1940
|
+
format: "clock",
|
|
1941
|
+
a11yLabel: options.a11yLabel ?? "round timer",
|
|
1942
|
+
visibleWhen: options.visibleWhen
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
export function createGameHudComboBinding(options = {}) {
|
|
1946
|
+
const targetId = options.actorId;
|
|
1947
|
+
return createGameHudBinding({
|
|
1948
|
+
binding: "combo",
|
|
1949
|
+
id: options.id ?? (targetId ? `hud:${targetId}:combo` : "hud:combo"),
|
|
1950
|
+
label: options.label ?? (targetId ? `${targetId} combo` : "combo counter"),
|
|
1951
|
+
source: "combat",
|
|
1952
|
+
targetId,
|
|
1953
|
+
valuePath: options.valuePath ?? (targetId ? `combat.actors.${targetId}.combo` : "combat.combo"),
|
|
1954
|
+
format: "number",
|
|
1955
|
+
a11yLabel: options.a11yLabel ?? (targetId ? `${targetId} combo counter` : "combo counter"),
|
|
1956
|
+
visibleWhen: options.visibleWhen
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
export function createGameHudRoundBinding(options = {}) {
|
|
1960
|
+
return createGameHudBinding({
|
|
1961
|
+
binding: "round",
|
|
1962
|
+
id: options.id ?? "hud:round:index",
|
|
1963
|
+
label: options.label ?? "round",
|
|
1964
|
+
source: "round",
|
|
1965
|
+
valuePath: options.valuePath ?? "round.index",
|
|
1966
|
+
format: "number",
|
|
1967
|
+
a11yLabel: options.a11yLabel ?? "current round",
|
|
1968
|
+
visibleWhen: options.visibleWhen
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
export function createGameHudDebugToggleBinding(options = {}) {
|
|
1972
|
+
return createGameHudBinding({
|
|
1973
|
+
binding: "debug-toggle",
|
|
1974
|
+
id: options.id ?? "hud:debug:toggle",
|
|
1975
|
+
label: options.label ?? "debug overlay",
|
|
1976
|
+
source: "debug",
|
|
1977
|
+
valuePath: options.statePath ?? "debug.visible",
|
|
1978
|
+
format: "boolean",
|
|
1979
|
+
a11yLabel: options.a11yLabel ?? "debug overlay toggle",
|
|
1980
|
+
debugOnly: true,
|
|
1981
|
+
interactive: true,
|
|
1982
|
+
visibleWhen: options.visibleWhen ?? options.action
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
export function createGameHudBindings(bindings) {
|
|
1986
|
+
return [...bindings];
|
|
1987
|
+
}
|
|
1988
|
+
export function createGameHudSnapshot(options) {
|
|
1989
|
+
const combatSnapshot = options.combat && "kind" in options.combat && options.combat.kind === "aura-game-combat-world"
|
|
1990
|
+
? options.combat
|
|
1991
|
+
: options.combat && "snapshot" in options.combat
|
|
1992
|
+
? options.combat.snapshot()
|
|
1993
|
+
: undefined;
|
|
1994
|
+
const inputSnapshot = options.input && "kind" in options.input && options.input.kind === "aura-game-input-snapshot"
|
|
1995
|
+
? options.input
|
|
1996
|
+
: options.input && "snapshot" in options.input
|
|
1997
|
+
? options.input.snapshot()
|
|
1998
|
+
: undefined;
|
|
1999
|
+
const events = options.events ?? combatSnapshot?.events ?? [];
|
|
2000
|
+
const data = createGameHudDataContext({
|
|
2001
|
+
combat: combatSnapshot,
|
|
2002
|
+
round: options.round,
|
|
2003
|
+
rules: options.rules,
|
|
2004
|
+
input: inputSnapshot,
|
|
2005
|
+
runtime: options.runtime,
|
|
2006
|
+
debug: options.debug,
|
|
2007
|
+
appState: options.appState
|
|
2008
|
+
});
|
|
2009
|
+
const values = options.bindings.map((binding) => {
|
|
2010
|
+
const value = readGameValuePath(data, binding.valuePath);
|
|
2011
|
+
const max = binding.maxPath ? readGameValuePath(data, binding.maxPath) : undefined;
|
|
2012
|
+
const changed = gameHudBindingChanged(binding, events);
|
|
2013
|
+
return {
|
|
2014
|
+
kind: "aura-game-hud-value",
|
|
2015
|
+
id: binding.id,
|
|
2016
|
+
binding: binding.binding,
|
|
2017
|
+
label: binding.label,
|
|
2018
|
+
source: binding.source,
|
|
2019
|
+
targetId: binding.targetId,
|
|
2020
|
+
valuePath: binding.valuePath,
|
|
2021
|
+
value,
|
|
2022
|
+
max,
|
|
2023
|
+
formatted: formatGameHudValue(value, max, binding.format),
|
|
2024
|
+
changed,
|
|
2025
|
+
debugOnly: binding.debugOnly,
|
|
2026
|
+
interactive: binding.interactive,
|
|
2027
|
+
a11yLabel: binding.a11yLabel
|
|
2028
|
+
};
|
|
2029
|
+
});
|
|
2030
|
+
return {
|
|
2031
|
+
kind: "aura-game-hud-snapshot",
|
|
2032
|
+
frame: combatSnapshot?.frame ?? inputSnapshot?.frame ?? 0,
|
|
2033
|
+
time: combatSnapshot?.time ?? inputSnapshot?.time ?? 0,
|
|
2034
|
+
values,
|
|
2035
|
+
changedIds: values.filter((value) => value.changed).map((value) => value.id),
|
|
2036
|
+
events
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
export function createGameAccessibilityLabel(options) {
|
|
2040
|
+
return {
|
|
2041
|
+
kind: "aura-game-accessibility-source",
|
|
2042
|
+
feature: "label",
|
|
2043
|
+
id: options.id ?? `a11y:${options.targetId}:label`,
|
|
2044
|
+
owner: "app",
|
|
2045
|
+
label: options.label,
|
|
2046
|
+
targetId: options.targetId,
|
|
2047
|
+
role: options.role ?? "status",
|
|
2048
|
+
actions: [],
|
|
2049
|
+
source: "dom",
|
|
2050
|
+
evidence: options.live
|
|
2051
|
+
? "App owns an aria-live label for this gameplay target."
|
|
2052
|
+
: "App owns a readable label for this gameplay target."
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
export function createGameAccessibilityFocus(options) {
|
|
2056
|
+
return {
|
|
2057
|
+
kind: "aura-game-accessibility-source",
|
|
2058
|
+
feature: "focus",
|
|
2059
|
+
id: options.id ?? `a11y:${options.scopeId}:focus`,
|
|
2060
|
+
owner: "app",
|
|
2061
|
+
label: options.label ?? `${options.scopeId} focus scope`,
|
|
2062
|
+
targetId: options.scopeId,
|
|
2063
|
+
actions: options.targets ?? [],
|
|
2064
|
+
source: "dom",
|
|
2065
|
+
evidence: "App owns keyboard focus order and focus restoration for this gameplay UI scope."
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
export function createGameReducedMotionSource(options = {}) {
|
|
2069
|
+
return {
|
|
2070
|
+
kind: "aura-game-accessibility-source",
|
|
2071
|
+
feature: "reduced-motion",
|
|
2072
|
+
id: options.id ?? "a11y:prefers-reduced-motion",
|
|
2073
|
+
owner: "shared",
|
|
2074
|
+
label: options.label ?? "prefers reduced motion",
|
|
2075
|
+
actions: [],
|
|
2076
|
+
enabled: options.enabled,
|
|
2077
|
+
source: "media-query",
|
|
2078
|
+
evidence: "App reads the user preference; Aura3D camera and effects helpers consume reducedMotion flags."
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
export function createGameReducedFlashSource(options = {}) {
|
|
2082
|
+
return {
|
|
2083
|
+
kind: "aura-game-accessibility-source",
|
|
2084
|
+
feature: "reduced-flash",
|
|
2085
|
+
id: options.id ?? "a11y:reduced-flash",
|
|
2086
|
+
owner: "shared",
|
|
2087
|
+
label: options.label ?? "reduced flash",
|
|
2088
|
+
actions: [],
|
|
2089
|
+
enabled: options.enabled,
|
|
2090
|
+
source: "app-state",
|
|
2091
|
+
evidence: "App stores the flash preference; Aura3D effects helpers consume reducedFlash flags."
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
export function createGameHighContrastSource(options = {}) {
|
|
2095
|
+
return {
|
|
2096
|
+
kind: "aura-game-accessibility-source",
|
|
2097
|
+
feature: "high-contrast",
|
|
2098
|
+
id: options.id ?? "a11y:high-contrast",
|
|
2099
|
+
owner: "app",
|
|
2100
|
+
label: options.label ?? "high contrast HUD",
|
|
2101
|
+
actions: [],
|
|
2102
|
+
enabled: options.enabled,
|
|
2103
|
+
source: "app-state",
|
|
2104
|
+
evidence: "App owns high-contrast CSS/theme state for HUD, menus, labels, and focus rings."
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
export function createGamePauseControlsSource(options = {}) {
|
|
2108
|
+
const actions = options.actions ?? ["pause", "Escape", "GamepadStart"];
|
|
2109
|
+
return {
|
|
2110
|
+
kind: "aura-game-accessibility-source",
|
|
2111
|
+
feature: "pause-controls",
|
|
2112
|
+
id: options.id ?? "a11y:pause-controls",
|
|
2113
|
+
owner: "shared",
|
|
2114
|
+
label: options.label ?? "pause and resume controls",
|
|
2115
|
+
targetId: options.menuId,
|
|
2116
|
+
actions: [...actions, ...(options.resumeActions ?? [])],
|
|
2117
|
+
source: "input",
|
|
2118
|
+
evidence: "App maps pause/resume input and calls Aura3D app.pause(), app.resume(), or app.step() without remounting the scene."
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
export function createGameAccessibilityRuntimeSettings(sources = [], options = {}) {
|
|
2122
|
+
const sourceEnabled = (feature) => sources.some((source) => source.feature === feature && source.enabled === true);
|
|
2123
|
+
const reducedMotion = options.reducedMotion ?? sourceEnabled("reduced-motion");
|
|
2124
|
+
const reducedFlash = options.reducedFlash ?? sourceEnabled("reduced-flash");
|
|
2125
|
+
const highContrast = options.highContrast ?? sourceEnabled("high-contrast");
|
|
2126
|
+
return {
|
|
2127
|
+
kind: "aura-game-accessibility-runtime-settings",
|
|
2128
|
+
reducedMotion,
|
|
2129
|
+
reducedFlash,
|
|
2130
|
+
highContrast,
|
|
2131
|
+
camera: { reducedMotion },
|
|
2132
|
+
effects: { reducedMotion, reducedFlash },
|
|
2133
|
+
evidence: [
|
|
2134
|
+
reducedMotion
|
|
2135
|
+
? "Reduced-motion preference is forwarded to game.cameraDirector({ reducedMotion: true }) and game.effects({ reducedMotion: true })."
|
|
2136
|
+
: "Camera and effects keep normal motion because reduced-motion is not enabled.",
|
|
2137
|
+
reducedFlash
|
|
2138
|
+
? "Reduced-flash preference is forwarded to game.effects({ reducedFlash: true }) to cap flash intensity."
|
|
2139
|
+
: "Effects keep normal flash intensity because reduced-flash is not enabled.",
|
|
2140
|
+
highContrast
|
|
2141
|
+
? "High-contrast preference is available for HUD/menu rendering without querying WebGL state."
|
|
2142
|
+
: "HUD/menu rendering can use the default contrast theme."
|
|
2143
|
+
]
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
function createGameHudDataContext(options) {
|
|
2147
|
+
const actors = {};
|
|
2148
|
+
for (const actor of options.combat?.actors ?? [])
|
|
2149
|
+
actors[actor.id] = actor;
|
|
2150
|
+
return {
|
|
2151
|
+
combat: {
|
|
2152
|
+
...(options.combat ?? {}),
|
|
2153
|
+
actors,
|
|
2154
|
+
events: options.combat?.events ?? []
|
|
2155
|
+
},
|
|
2156
|
+
round: options.round ?? {},
|
|
2157
|
+
rules: {
|
|
2158
|
+
maxHealth: 100,
|
|
2159
|
+
maxGuard: 100,
|
|
2160
|
+
maxMeter: 100,
|
|
2161
|
+
...(options.rules ?? {})
|
|
2162
|
+
},
|
|
2163
|
+
input: options.input ?? {},
|
|
2164
|
+
runtime: options.runtime ?? {},
|
|
2165
|
+
debug: options.debug ?? {},
|
|
2166
|
+
appState: options.appState ?? {}
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
function readGameValuePath(source, path) {
|
|
2170
|
+
const value = path.split(".").reduce((current, key) => {
|
|
2171
|
+
if (current === undefined || current === null)
|
|
2172
|
+
return undefined;
|
|
2173
|
+
if (typeof current !== "object")
|
|
2174
|
+
return undefined;
|
|
2175
|
+
return current[key];
|
|
2176
|
+
}, source);
|
|
2177
|
+
if (typeof value === "number" || typeof value === "string" || typeof value === "boolean" || value === undefined)
|
|
2178
|
+
return value;
|
|
2179
|
+
return JSON.stringify(value);
|
|
2180
|
+
}
|
|
2181
|
+
function formatGameHudValue(value, max, format) {
|
|
2182
|
+
if (value === undefined)
|
|
2183
|
+
return "";
|
|
2184
|
+
if (format === "boolean")
|
|
2185
|
+
return value ? "on" : "off";
|
|
2186
|
+
if (format === "clock")
|
|
2187
|
+
return formatClockValue(value);
|
|
2188
|
+
if (format === "percent") {
|
|
2189
|
+
const numericValue = Number(value);
|
|
2190
|
+
const numericMax = Number(max ?? 100);
|
|
2191
|
+
if (!Number.isFinite(numericValue) || !Number.isFinite(numericMax) || numericMax <= 0)
|
|
2192
|
+
return String(value);
|
|
2193
|
+
return `${Math.round(clamp(numericValue / numericMax, 0, 1) * 100)}%`;
|
|
2194
|
+
}
|
|
2195
|
+
if (format === "number" && typeof value === "number")
|
|
2196
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
2197
|
+
return String(value);
|
|
2198
|
+
}
|
|
2199
|
+
function formatClockValue(value) {
|
|
2200
|
+
const seconds = Number(value);
|
|
2201
|
+
if (!Number.isFinite(seconds))
|
|
2202
|
+
return String(value);
|
|
2203
|
+
const minutes = Math.floor(Math.max(0, seconds) / 60);
|
|
2204
|
+
const remaining = Math.floor(Math.max(0, seconds) % 60);
|
|
2205
|
+
return `${minutes}:${String(remaining).padStart(2, "0")}`;
|
|
2206
|
+
}
|
|
2207
|
+
function gameHudBindingChanged(binding, events) {
|
|
2208
|
+
if (!events.length)
|
|
2209
|
+
return false;
|
|
2210
|
+
if (binding.binding === "health" || binding.binding === "combo") {
|
|
2211
|
+
return events.some((event) => event.targetId === binding.targetId && (event.damage ?? 0) > 0);
|
|
2212
|
+
}
|
|
2213
|
+
if (binding.binding === "meter") {
|
|
2214
|
+
return events.some((event) => event.attackerId === binding.targetId && (event.damage ?? event.guardDamage ?? 0) > 0);
|
|
2215
|
+
}
|
|
2216
|
+
if (binding.binding === "debug-toggle")
|
|
2217
|
+
return events.some((event) => event.type === "hit" || event.type === "blocked");
|
|
2218
|
+
return events.length > 0;
|
|
2219
|
+
}
|
|
2220
|
+
function createGameHudBinding(options) {
|
|
2221
|
+
return {
|
|
2222
|
+
kind: "aura-game-hud-binding",
|
|
2223
|
+
owner: "app",
|
|
2224
|
+
debugOnly: options.debugOnly ?? false,
|
|
2225
|
+
interactive: options.interactive ?? false,
|
|
2226
|
+
...options
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
//# sourceMappingURL=GameRuntime.js.map
|