@jgengine/shell 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -0
- package/dist/GamePlayerShell.js +48 -358
- package/dist/GameUiPreview.js +3 -0
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +111 -26
- package/dist/audio/audioWire.d.ts +22 -4
- package/dist/audio/audioWire.js +46 -3
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/camera/GameCameraRig.js +6 -1
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- package/dist/camera/rigMath.d.ts +20 -0
- package/dist/camera/rigMath.js +29 -0
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +26 -4
- package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
- package/dist/devtools/DevtoolsOverlay.js +28 -12
- package/dist/devtools/agentBridge.js +4 -0
- package/dist/devtools/perfDiagnose.js +1 -1
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
- package/dist/environment/EnvironmentScene.js +12 -3
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +11 -4
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +1 -0
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/modelLoad.js +4 -1
- package/dist/render/resolveModel.d.ts +15 -0
- package/dist/render/resolveModel.js +37 -2
- package/dist/scatter/InstancedScatter.js +5 -0
- package/dist/scatter/scatterProxies.js +24 -0
- package/dist/scene/AuthoredScene.d.ts +3 -1
- package/dist/scene/AuthoredScene.js +7 -2
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GrassFieldRenderer.js +53 -5
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.js +70 -7
- package/dist/scene/WaterRenderer.js +34 -10
- package/dist/scene/builtinSceneKindRenderers.js +4 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +2 -0
- package/dist/settings/appliedSettings.d.ts +2 -1
- package/dist/settings/appliedSettings.js +4 -2
- package/dist/structures/TransformGizmo.d.ts +11 -1
- package/dist/structures/TransformGizmo.js +9 -5
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/grassGeometry.d.ts +33 -1
- package/dist/terrain/grassGeometry.js +83 -21
- package/dist/terrain/grassMaterial.d.ts +18 -0
- package/dist/terrain/grassMaterial.js +66 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/soilPatchMaterial.d.ts +15 -2
- package/dist/terrain/soilPatchMaterial.js +46 -8
- package/dist/terrain/terrainDetailMaterial.js +15 -2
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +9 -0
- package/dist/water/OceanConfig.js +6 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/WorldScene.js +29 -2
- package/dist/worldSky.d.ts +7 -1
- package/dist/worldSky.js +11 -1
- package/package.json +4 -4
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +15 -10
- package/skills/jgengine-ui/api.md +202 -47
- package/skills/jgengine-ui/capabilities.md +71 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +20 -7
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +419 -15
- package/skills/jgengine-world/capabilities.md +108 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge/runtime.d.ts +0 -15
- package/dist/cartridge/runtime.js +0 -556
- package/dist/cartridge/spec.d.ts +0 -188
- package/dist/cartridge/spec.js +0 -19
- package/dist/cartridge/validate.d.ts +0 -2
- package/dist/cartridge/validate.js +0 -78
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { distance3, resolveEmitterGain } from "@jgengine/core/audio/audioFalloff";
|
|
2
|
+
import { patchDuration } from "@jgengine/core/audio/synth";
|
|
2
3
|
import { createDisposer } from "@jgengine/core/game/defineGame";
|
|
4
|
+
import { clampLoopGain, clampLoopRate } from "./loopParams.js";
|
|
3
5
|
import { MusicDirector } from "./musicDirector.js";
|
|
4
6
|
import { createNoiseBuffer, realizeSynthPatch } from "./synthEngine.js";
|
|
7
|
+
/** setTargetAtTime time constant (~20 ms) for zipper-free live rate/gain ramps on retained loops (#1051). */
|
|
8
|
+
const LOOP_PARAM_SMOOTH_TC = 0.02;
|
|
5
9
|
function createNoopEngine() {
|
|
6
10
|
return {
|
|
7
11
|
setListenerPose: () => undefined,
|
|
@@ -20,6 +24,12 @@ function resolveAudioContextCtor() {
|
|
|
20
24
|
return (window.AudioContext ??
|
|
21
25
|
window.webkitAudioContext);
|
|
22
26
|
}
|
|
27
|
+
function resolveOfflineAudioContextCtor() {
|
|
28
|
+
if (typeof window === "undefined")
|
|
29
|
+
return undefined;
|
|
30
|
+
return (window.OfflineAudioContext ??
|
|
31
|
+
window.webkitOfflineAudioContext);
|
|
32
|
+
}
|
|
23
33
|
export function createAudioEngine(config = {}) {
|
|
24
34
|
const sounds = config.sounds ?? {};
|
|
25
35
|
const busDefs = config.buses ?? {};
|
|
@@ -74,65 +84,138 @@ export function createAudioEngine(config = {}) {
|
|
|
74
84
|
}
|
|
75
85
|
return pending;
|
|
76
86
|
}
|
|
87
|
+
// Retained synth loops render the procedural patch to one cached buffer, then loop it as a plain
|
|
88
|
+
// BufferSource — so live pitch is `playbackRate` and live gain is a gain node, exactly like sample loops (#1051).
|
|
89
|
+
const synthBufferCache = new Map();
|
|
90
|
+
function renderSynthBuffer(soundId, patch) {
|
|
91
|
+
let pending = synthBufferCache.get(soundId);
|
|
92
|
+
if (pending === undefined) {
|
|
93
|
+
const OfflineCtor = resolveOfflineAudioContextCtor();
|
|
94
|
+
if (OfflineCtor === undefined) {
|
|
95
|
+
pending = Promise.resolve(null);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
try {
|
|
99
|
+
const seconds = Math.max(patchDuration(patch), 0.05);
|
|
100
|
+
const frames = Math.max(1, Math.ceil(seconds * context.sampleRate));
|
|
101
|
+
const offline = new OfflineCtor(1, frames, context.sampleRate);
|
|
102
|
+
realizeSynthPatch(offline, offline.destination, createNoiseBuffer(offline), patch);
|
|
103
|
+
pending = offline.startRendering().catch(() => null);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
pending = Promise.resolve(null);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
synthBufferCache.set(soundId, pending);
|
|
110
|
+
}
|
|
111
|
+
return pending;
|
|
112
|
+
}
|
|
77
113
|
let listenerPosition = { x: 0, y: 0, z: 0 };
|
|
78
114
|
const activeSpatialUpdaters = new Set();
|
|
115
|
+
const activeLoops = new Set();
|
|
116
|
+
const NOOP_HANDLE = {
|
|
117
|
+
setPosition: () => undefined,
|
|
118
|
+
setRate: () => undefined,
|
|
119
|
+
setGain: () => undefined,
|
|
120
|
+
stop: () => undefined,
|
|
121
|
+
};
|
|
79
122
|
function playInternal(soundId, position, loop) {
|
|
80
123
|
const sound = sounds[soundId];
|
|
81
124
|
if (sound === undefined)
|
|
82
125
|
return null;
|
|
83
126
|
const bus = busGainNode(sound.bus);
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
const currentPosition = position ?? listenerPosition;
|
|
128
|
+
// A one-shot synth cue realises its decaying voices live and is fire-and-forget.
|
|
129
|
+
if (sound.synth !== undefined && !loop) {
|
|
86
130
|
const cueGain = context.createGain();
|
|
87
131
|
cueGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
88
132
|
cueGain.connect(bus);
|
|
89
133
|
realizeSynthPatch(context, cueGain, sharedNoiseBuffer(), sound.synth);
|
|
90
|
-
return
|
|
134
|
+
return NOOP_HANDLE;
|
|
91
135
|
}
|
|
92
|
-
|
|
136
|
+
// Everything else resolves to one AudioBuffer we loop/play: sample URL, or a synth patch rendered once.
|
|
137
|
+
let bufferPromise;
|
|
138
|
+
if (sound.synth !== undefined)
|
|
139
|
+
bufferPromise = renderSynthBuffer(soundId, sound.synth);
|
|
140
|
+
else if (sound.url !== undefined)
|
|
141
|
+
bufferPromise = loadBuffer(sound.url);
|
|
142
|
+
else
|
|
93
143
|
return null;
|
|
144
|
+
return playBufferSource(sound, bus, bufferPromise, currentPosition, loop);
|
|
145
|
+
}
|
|
146
|
+
function playBufferSource(sound, bus, bufferPromise, initialPosition, loop) {
|
|
94
147
|
const disposer = createDisposer();
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
148
|
+
let currentPosition = initialPosition;
|
|
149
|
+
// Live control state, applied on source creation so updates that race the async buffer load stick.
|
|
150
|
+
let currentRate = 1;
|
|
151
|
+
let currentUserGain = 1;
|
|
152
|
+
let sourceNode = null;
|
|
153
|
+
let userGainNode = null;
|
|
154
|
+
let falloffGain = null;
|
|
155
|
+
function updateFalloff() {
|
|
156
|
+
if (falloffGain !== null) {
|
|
157
|
+
falloffGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
99
158
|
}
|
|
100
159
|
}
|
|
101
|
-
void
|
|
160
|
+
void bufferPromise.then((buffer) => {
|
|
102
161
|
if (buffer === null)
|
|
103
162
|
return;
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
163
|
+
const src = context.createBufferSource();
|
|
164
|
+
src.buffer = buffer;
|
|
165
|
+
src.loop = loop || (sound.loop ?? false);
|
|
166
|
+
src.playbackRate.value = currentRate;
|
|
167
|
+
// source → per-loop user gain (setGain) → distance falloff → bus
|
|
168
|
+
const uGain = context.createGain();
|
|
169
|
+
uGain.gain.value = currentUserGain;
|
|
170
|
+
const fGain = context.createGain();
|
|
171
|
+
fGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
172
|
+
src.connect(uGain);
|
|
173
|
+
uGain.connect(fGain);
|
|
174
|
+
fGain.connect(bus);
|
|
175
|
+
src.start();
|
|
176
|
+
sourceNode = src;
|
|
177
|
+
userGainNode = uGain;
|
|
178
|
+
falloffGain = fGain;
|
|
113
179
|
disposer.onDispose(() => {
|
|
114
180
|
try {
|
|
115
|
-
|
|
181
|
+
src.stop();
|
|
116
182
|
}
|
|
117
183
|
catch {
|
|
118
184
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
185
|
+
src.disconnect();
|
|
186
|
+
uGain.disconnect();
|
|
187
|
+
fGain.disconnect();
|
|
188
|
+
sourceNode = null;
|
|
189
|
+
userGainNode = null;
|
|
190
|
+
falloffGain = null;
|
|
122
191
|
});
|
|
123
192
|
});
|
|
124
193
|
if (loop)
|
|
125
|
-
activeSpatialUpdaters.add(
|
|
126
|
-
|
|
194
|
+
activeSpatialUpdaters.add(updateFalloff);
|
|
195
|
+
const handle = {
|
|
127
196
|
setPosition(next) {
|
|
128
197
|
currentPosition = next;
|
|
129
|
-
|
|
198
|
+
updateFalloff();
|
|
199
|
+
},
|
|
200
|
+
setRate(rate) {
|
|
201
|
+
currentRate = clampLoopRate(rate);
|
|
202
|
+
if (sourceNode !== null)
|
|
203
|
+
sourceNode.playbackRate.setTargetAtTime(currentRate, context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
204
|
+
},
|
|
205
|
+
setGain(gain) {
|
|
206
|
+
currentUserGain = clampLoopGain(gain);
|
|
207
|
+
if (userGainNode !== null)
|
|
208
|
+
userGainNode.gain.setTargetAtTime(currentUserGain, context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
130
209
|
},
|
|
131
210
|
stop() {
|
|
132
|
-
activeSpatialUpdaters.delete(
|
|
211
|
+
activeSpatialUpdaters.delete(updateFalloff);
|
|
212
|
+
activeLoops.delete(handle);
|
|
133
213
|
disposer.dispose();
|
|
134
214
|
},
|
|
135
215
|
};
|
|
216
|
+
if (loop)
|
|
217
|
+
activeLoops.add(handle);
|
|
218
|
+
return handle;
|
|
136
219
|
}
|
|
137
220
|
return {
|
|
138
221
|
setListenerPose(position) {
|
|
@@ -161,6 +244,8 @@ export function createAudioEngine(config = {}) {
|
|
|
161
244
|
void context.resume().catch(() => undefined);
|
|
162
245
|
},
|
|
163
246
|
dispose() {
|
|
247
|
+
for (const handle of [...activeLoops])
|
|
248
|
+
handle.stop();
|
|
164
249
|
activeSpatialUpdaters.clear();
|
|
165
250
|
director?.dispose();
|
|
166
251
|
void context.close().catch(() => undefined);
|
|
@@ -7,16 +7,34 @@ type AudioMusicPayload = {
|
|
|
7
7
|
theme: string | null;
|
|
8
8
|
transpose?: number;
|
|
9
9
|
};
|
|
10
|
-
|
|
10
|
+
type AudioLoopStartPayload = {
|
|
11
|
+
id: string;
|
|
12
|
+
sound: string;
|
|
13
|
+
at?: readonly [number, number, number];
|
|
14
|
+
};
|
|
15
|
+
type AudioLoopSetPayload = {
|
|
16
|
+
id: string;
|
|
17
|
+
rate?: number;
|
|
18
|
+
gain?: number;
|
|
19
|
+
at?: readonly [number, number, number];
|
|
20
|
+
};
|
|
21
|
+
type AudioLoopStopPayload = {
|
|
22
|
+
id: string;
|
|
23
|
+
};
|
|
24
|
+
/** Minimal event bus the shell wires to the audio engine, including retained id-keyed loops (#1051). @internal */
|
|
11
25
|
export type AudioEventBus = {
|
|
12
26
|
on(event: "audio.play", handler: (payload: AudioPlayPayload) => void): () => void;
|
|
13
27
|
on(event: "audio.music", handler: (payload: AudioMusicPayload) => void): () => void;
|
|
14
28
|
on(event: "audio.resume", handler: () => void): () => void;
|
|
29
|
+
on(event: "audio.loopStart", handler: (payload: AudioLoopStartPayload) => void): () => void;
|
|
30
|
+
on(event: "audio.loopSet", handler: (payload: AudioLoopSetPayload) => void): () => void;
|
|
31
|
+
on(event: "audio.loopStop", handler: (payload: AudioLoopStopPayload) => void): () => void;
|
|
15
32
|
};
|
|
16
33
|
/**
|
|
17
|
-
* Subscribe game audio events to the shell audio engine.
|
|
18
|
-
*
|
|
34
|
+
* Subscribe game audio events to the shell audio engine. Manages the retained-loop registry
|
|
35
|
+
* (`audio.loopStart`/`audio.loopSet`/`audio.loopStop`), keyed by stable id. Returns a single
|
|
36
|
+
* unsubscribe that tears every listener down and stops any loops still playing.
|
|
19
37
|
* @internal
|
|
20
38
|
*/
|
|
21
|
-
export declare function attachAudioEventWire(events: AudioEventBus, audioEngine: Pick<AudioEngine, "playOneShot" | "playMusic" | "resume">): () => void;
|
|
39
|
+
export declare function attachAudioEventWire(events: AudioEventBus, audioEngine: Pick<AudioEngine, "playOneShot" | "playLoop" | "playMusic" | "resume">): () => void;
|
|
22
40
|
export {};
|
package/dist/audio/audioWire.js
CHANGED
|
@@ -1,19 +1,62 @@
|
|
|
1
|
+
function toVec(at) {
|
|
2
|
+
return at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] };
|
|
3
|
+
}
|
|
1
4
|
/**
|
|
2
|
-
* Subscribe game audio events to the shell audio engine.
|
|
3
|
-
*
|
|
5
|
+
* Subscribe game audio events to the shell audio engine. Manages the retained-loop registry
|
|
6
|
+
* (`audio.loopStart`/`audio.loopSet`/`audio.loopStop`), keyed by stable id. Returns a single
|
|
7
|
+
* unsubscribe that tears every listener down and stops any loops still playing.
|
|
4
8
|
* @internal
|
|
5
9
|
*/
|
|
6
10
|
export function attachAudioEventWire(events, audioEngine) {
|
|
11
|
+
const loops = new Map();
|
|
7
12
|
const offPlay = events.on("audio.play", ({ sound, at }) => {
|
|
8
|
-
audioEngine.playOneShot(sound, at
|
|
13
|
+
audioEngine.playOneShot(sound, toVec(at));
|
|
9
14
|
});
|
|
10
15
|
const offMusic = events.on("audio.music", ({ theme, transpose }) => {
|
|
11
16
|
audioEngine.playMusic(theme, transpose === undefined ? undefined : { transpose });
|
|
12
17
|
});
|
|
13
18
|
const offResume = events.on("audio.resume", () => audioEngine.resume());
|
|
19
|
+
const offLoopStart = events.on("audio.loopStart", ({ id, sound, at }) => {
|
|
20
|
+
const existing = loops.get(id);
|
|
21
|
+
if (existing !== undefined) {
|
|
22
|
+
// Same sound: idempotent — keep the running source so a per-tick re-issue never clicks.
|
|
23
|
+
if (existing.sound === sound)
|
|
24
|
+
return;
|
|
25
|
+
existing.handle.stop();
|
|
26
|
+
loops.delete(id);
|
|
27
|
+
}
|
|
28
|
+
const handle = audioEngine.playLoop(sound, toVec(at));
|
|
29
|
+
if (handle !== null)
|
|
30
|
+
loops.set(id, { sound, handle });
|
|
31
|
+
});
|
|
32
|
+
const offLoopSet = events.on("audio.loopSet", ({ id, rate, gain, at }) => {
|
|
33
|
+
const entry = loops.get(id);
|
|
34
|
+
// Unknown id: a live update may race a stop — silently drop it.
|
|
35
|
+
if (entry === undefined)
|
|
36
|
+
return;
|
|
37
|
+
if (rate !== undefined)
|
|
38
|
+
entry.handle.setRate(rate);
|
|
39
|
+
if (gain !== undefined)
|
|
40
|
+
entry.handle.setGain(gain);
|
|
41
|
+
if (at !== undefined)
|
|
42
|
+
entry.handle.setPosition({ x: at[0], y: at[1], z: at[2] });
|
|
43
|
+
});
|
|
44
|
+
const offLoopStop = events.on("audio.loopStop", ({ id }) => {
|
|
45
|
+
const entry = loops.get(id);
|
|
46
|
+
if (entry === undefined)
|
|
47
|
+
return;
|
|
48
|
+
entry.handle.stop();
|
|
49
|
+
loops.delete(id);
|
|
50
|
+
});
|
|
14
51
|
return () => {
|
|
15
52
|
offPlay();
|
|
16
53
|
offMusic();
|
|
17
54
|
offResume();
|
|
55
|
+
offLoopStart();
|
|
56
|
+
offLoopSet();
|
|
57
|
+
offLoopStop();
|
|
58
|
+
for (const { handle } of loops.values())
|
|
59
|
+
handle.stop();
|
|
60
|
+
loops.clear();
|
|
18
61
|
};
|
|
19
62
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure clamps for retained-loop live control (#1051). Kept dependency-free so the
|
|
3
|
+
* playback-rate/gain windows are one testable source of truth the audio engine reads.
|
|
4
|
+
*/
|
|
5
|
+
/** Lowest live playback-rate multiplier (two octaves below authored pitch). */
|
|
6
|
+
export declare const MIN_LOOP_RATE = 0.25;
|
|
7
|
+
/** Highest live playback-rate multiplier (two octaves above authored pitch). */
|
|
8
|
+
export declare const MAX_LOOP_RATE = 4;
|
|
9
|
+
/**
|
|
10
|
+
* Clamp a loop playback-rate multiplier into the safe pitch window `MIN_LOOP_RATE`–`MAX_LOOP_RATE`
|
|
11
|
+
* (1 = authored pitch). Non-finite input (NaN/±Infinity) falls back to the authored `1`.
|
|
12
|
+
* @internal — the audio engine applies this; game code sets rate through `ctx.game.audio.setLoop`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function clampLoopRate(rate: number): number;
|
|
15
|
+
/**
|
|
16
|
+
* Clamp a loop gain multiplier into `0`–`1`. Non-finite input falls back to `0` (silence) so a
|
|
17
|
+
* stray NaN never blasts a loop to full volume.
|
|
18
|
+
* @internal — the audio engine applies this; game code sets gain through `ctx.game.audio.setLoop`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function clampLoopGain(gain: number): number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure clamps for retained-loop live control (#1051). Kept dependency-free so the
|
|
3
|
+
* playback-rate/gain windows are one testable source of truth the audio engine reads.
|
|
4
|
+
*/
|
|
5
|
+
/** Lowest live playback-rate multiplier (two octaves below authored pitch). */
|
|
6
|
+
export const MIN_LOOP_RATE = 0.25;
|
|
7
|
+
/** Highest live playback-rate multiplier (two octaves above authored pitch). */
|
|
8
|
+
export const MAX_LOOP_RATE = 4;
|
|
9
|
+
/**
|
|
10
|
+
* Clamp a loop playback-rate multiplier into the safe pitch window `MIN_LOOP_RATE`–`MAX_LOOP_RATE`
|
|
11
|
+
* (1 = authored pitch). Non-finite input (NaN/±Infinity) falls back to the authored `1`.
|
|
12
|
+
* @internal — the audio engine applies this; game code sets rate through `ctx.game.audio.setLoop`.
|
|
13
|
+
*/
|
|
14
|
+
export function clampLoopRate(rate) {
|
|
15
|
+
if (!Number.isFinite(rate))
|
|
16
|
+
return 1;
|
|
17
|
+
return Math.min(MAX_LOOP_RATE, Math.max(MIN_LOOP_RATE, rate));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Clamp a loop gain multiplier into `0`–`1`. Non-finite input falls back to `0` (silence) so a
|
|
21
|
+
* stray NaN never blasts a loop to full volume.
|
|
22
|
+
* @internal — the audio engine applies this; game code sets gain through `ctx.game.audio.setLoop`.
|
|
23
|
+
*/
|
|
24
|
+
export function clampLoopGain(gain) {
|
|
25
|
+
if (!Number.isFinite(gain))
|
|
26
|
+
return 0;
|
|
27
|
+
return Math.min(1, Math.max(0, gain));
|
|
28
|
+
}
|
|
@@ -22,7 +22,12 @@ export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerC
|
|
|
22
22
|
const followEntityId = directed.followEntityId;
|
|
23
23
|
const rig = (() => {
|
|
24
24
|
if (directed.cinematic !== undefined) {
|
|
25
|
-
|
|
25
|
+
// A director-set cinematic auto-clears when its keyframe path completes, so a
|
|
26
|
+
// one-shot flyover can never strand the camera if a game-side clear timer drifts
|
|
27
|
+
// (the rig plays in real time; sim-clock timers do not). Static config cinematics
|
|
28
|
+
// hold their final frame as before.
|
|
29
|
+
const owner = director !== undefined && director.cinematic() !== null ? director : undefined;
|
|
30
|
+
return (_jsx(CinematicRig, { yawRef: yawRef, pitchRef: pitchRef, config: { ...config, cinematic: directed.cinematic }, followEntityId: followEntityId, absoluteFov: true, ...(owner === undefined ? {} : { onComplete: () => owner.setCinematic(null) }) }));
|
|
26
31
|
}
|
|
27
32
|
const kind = resolveRigKind(config);
|
|
28
33
|
switch (kind) {
|
|
@@ -26,4 +26,6 @@ export declare function ChaseRig(props: RigProps): null;
|
|
|
26
26
|
* kill-cam rig. Distinct from every other rig, which drives from mouse/keys.
|
|
27
27
|
*/
|
|
28
28
|
export declare function ObserverRig(props: RigProps): null;
|
|
29
|
-
export declare function CinematicRig(props: RigProps
|
|
29
|
+
export declare function CinematicRig(props: RigProps & {
|
|
30
|
+
onComplete?: () => void;
|
|
31
|
+
}): null;
|
|
@@ -4,7 +4,7 @@ import { PerspectiveCamera, Quaternion, Vector3 } from "three";
|
|
|
4
4
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
5
5
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
6
|
import { ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
7
|
-
import { bankRollStep, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, leadFollowPoint, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, seatPose, shoulderPose, sideScrollFollowBlend, smoothYaw, speedToFov, springArmStep, topDownPose, } from "./rigMath.js";
|
|
7
|
+
import { bankRollStep, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, leadFollowPoint, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, seatPose, shoulderPose, sideScrollFollowBlend, smoothYaw, speedToFov, springArmStep, topDownPose, velocityYawTarget, } from "./rigMath.js";
|
|
8
8
|
import { usePlayerFov } from "./PlayerFov.js";
|
|
9
9
|
import { useCameraShake } from "./shakeChannel.js";
|
|
10
10
|
import { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
@@ -424,6 +424,7 @@ export function ChaseRig(props) {
|
|
|
424
424
|
const posRef = useRef(null);
|
|
425
425
|
const lastFollowRef = useRef(null);
|
|
426
426
|
const lastYawRef = useRef(null);
|
|
427
|
+
const anchorYawRef = useRef(null);
|
|
427
428
|
const rollRef = useRef(0);
|
|
428
429
|
const view = config?.view ?? "chase";
|
|
429
430
|
useEffect(beginTransition, [view]);
|
|
@@ -454,15 +455,24 @@ export function ChaseRig(props) {
|
|
|
454
455
|
commit(pose, dt);
|
|
455
456
|
return;
|
|
456
457
|
}
|
|
458
|
+
let anchorYaw = yaw;
|
|
459
|
+
if (resolved.velocityYawBlend > 0) {
|
|
460
|
+
const velocity = dt > 0
|
|
461
|
+
? { x: (follow.x - last.x) / dt, y: 0, z: (follow.z - last.z) / dt }
|
|
462
|
+
: { x: 0, y: 0, z: 0 };
|
|
463
|
+
const targetYaw = velocityYawTarget(yaw, velocity, resolved);
|
|
464
|
+
anchorYaw = smoothYaw(anchorYawRef.current ?? targetYaw, targetYaw, resolved.velocityYawResponse, dt);
|
|
465
|
+
anchorYawRef.current = anchorYaw;
|
|
466
|
+
}
|
|
457
467
|
const led = leadFollowPoint(follow, last, dt, resolved);
|
|
458
|
-
const desired = chaseDesiredPosition(led,
|
|
468
|
+
const desired = chaseDesiredPosition(led, anchorYaw, resolved);
|
|
459
469
|
const prev = posRef.current ?? desired;
|
|
460
470
|
const smoothed = springArmStep(prev, desired, resolved.springDamping, dt);
|
|
461
471
|
posRef.current = smoothed;
|
|
462
472
|
if (resolved.shakePerSpeed > 0 && speed > 0) {
|
|
463
473
|
shake.shake(Math.min(resolved.shakePerSpeed * speed * dt, 0.1));
|
|
464
474
|
}
|
|
465
|
-
const pose = { position: smoothed, lookAt: chaseLookAt(led,
|
|
475
|
+
const pose = { position: smoothed, lookAt: chaseLookAt(led, anchorYaw, resolved), fov };
|
|
466
476
|
commit(pose, dt);
|
|
467
477
|
if (rollRef.current !== 0)
|
|
468
478
|
camera.rotateZ(rollRef.current);
|
|
@@ -510,8 +520,12 @@ export function CinematicRig(props) {
|
|
|
510
520
|
const cinematic = props.config?.cinematic;
|
|
511
521
|
const { camera, commit, beginTransition } = useCameraCommit(props, followId);
|
|
512
522
|
const elapsedRef = useRef(0);
|
|
523
|
+
const completedRef = useRef(false);
|
|
524
|
+
const onCompleteRef = useRef(props.onComplete);
|
|
525
|
+
onCompleteRef.current = props.onComplete;
|
|
513
526
|
useEffect(() => {
|
|
514
527
|
elapsedRef.current = 0;
|
|
528
|
+
completedRef.current = false;
|
|
515
529
|
beginTransition();
|
|
516
530
|
}, [cinematic]);
|
|
517
531
|
useFrame((_, dt) => {
|
|
@@ -520,6 +534,10 @@ export function CinematicRig(props) {
|
|
|
520
534
|
elapsedRef.current += dt;
|
|
521
535
|
const sample = cinematicSample(cinematic.keyframes, elapsedRef.current, cinematic.loop ?? false, currentFov(camera));
|
|
522
536
|
commit(sample.pose, dt);
|
|
537
|
+
if (sample.done && !completedRef.current) {
|
|
538
|
+
completedRef.current = true;
|
|
539
|
+
onCompleteRef.current?.();
|
|
540
|
+
}
|
|
523
541
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
524
542
|
return null;
|
|
525
543
|
}
|
package/dist/camera/rigMath.d.ts
CHANGED
|
@@ -121,6 +121,12 @@ export interface ResolvedChase {
|
|
|
121
121
|
bankPerYawRate: number;
|
|
122
122
|
bankMax: number;
|
|
123
123
|
bankDamping: number;
|
|
124
|
+
/** Max fraction of the slip angle the anchor yaw follows toward velocity; 0 = drift-lag off (#1051). */
|
|
125
|
+
velocityYawBlend: number;
|
|
126
|
+
/** Below this planar speed the drift-lag feature is fully off. */
|
|
127
|
+
velocityYawMinSpeed: number;
|
|
128
|
+
/** Exponential smoothing rate toward the blended anchor yaw (1-exp(-response*dt)). */
|
|
129
|
+
velocityYawResponse: number;
|
|
124
130
|
}
|
|
125
131
|
/** @internal */
|
|
126
132
|
export declare function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase;
|
|
@@ -152,6 +158,20 @@ export declare function chaseDesiredPosition(follow: Vec3, yaw: number, resolved
|
|
|
152
158
|
* @internal
|
|
153
159
|
*/
|
|
154
160
|
export declare function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3;
|
|
161
|
+
/**
|
|
162
|
+
* Drift-lag (#1051): the yaw the chase anchor should aim before temporal
|
|
163
|
+
* smoothing. When the target slides, its planar `velocity` diverges from its
|
|
164
|
+
* `heading`; this rotates the anchor from heading partway toward the velocity
|
|
165
|
+
* direction so the camera reveals the car's side while travel stays framed. The
|
|
166
|
+
* traversed fraction is `velocityYawBlend` scaled by how far the slip angle has
|
|
167
|
+
* opened (0 at straight-line travel, full at a 90° slide), so ordinary driving
|
|
168
|
+
* is untouched. Returns the raw heading — no swing — when the feature is off,
|
|
169
|
+
* when planar speed is below `velocityYawMinSpeed`, or when the target is
|
|
170
|
+
* reversing (velocity opposed to heading forward). Blending is shortest-arc, so
|
|
171
|
+
* it stays correct across the ±PI wrap seam.
|
|
172
|
+
* @internal
|
|
173
|
+
*/
|
|
174
|
+
export declare function velocityYawTarget(heading: number, velocity: Vec3, resolved: ResolvedChase): number;
|
|
155
175
|
/** World-space seat pose (cockpit/hood/rear) rigidly attached to the vehicle.
|
|
156
176
|
* @internal
|
|
157
177
|
*/
|
package/dist/camera/rigMath.js
CHANGED
|
@@ -242,6 +242,9 @@ export function resolveChase(config) {
|
|
|
242
242
|
bankPerYawRate: config?.bank?.perYawRate ?? 0,
|
|
243
243
|
bankMax: config?.bank?.max ?? 0.35,
|
|
244
244
|
bankDamping: config?.bank?.damping ?? 8,
|
|
245
|
+
velocityYawBlend: config?.velocityYaw === undefined ? 0 : config.velocityYaw.blend ?? 0.65,
|
|
246
|
+
velocityYawMinSpeed: config?.velocityYaw?.minSpeed ?? 4,
|
|
247
|
+
velocityYawResponse: config?.velocityYaw?.response ?? 6,
|
|
245
248
|
};
|
|
246
249
|
}
|
|
247
250
|
/** Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`.
|
|
@@ -323,6 +326,32 @@ export function chaseLookAt(follow, yaw, resolved) {
|
|
|
323
326
|
z: follow.z + forward.z * 2,
|
|
324
327
|
};
|
|
325
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Drift-lag (#1051): the yaw the chase anchor should aim before temporal
|
|
331
|
+
* smoothing. When the target slides, its planar `velocity` diverges from its
|
|
332
|
+
* `heading`; this rotates the anchor from heading partway toward the velocity
|
|
333
|
+
* direction so the camera reveals the car's side while travel stays framed. The
|
|
334
|
+
* traversed fraction is `velocityYawBlend` scaled by how far the slip angle has
|
|
335
|
+
* opened (0 at straight-line travel, full at a 90° slide), so ordinary driving
|
|
336
|
+
* is untouched. Returns the raw heading — no swing — when the feature is off,
|
|
337
|
+
* when planar speed is below `velocityYawMinSpeed`, or when the target is
|
|
338
|
+
* reversing (velocity opposed to heading forward). Blending is shortest-arc, so
|
|
339
|
+
* it stays correct across the ±PI wrap seam.
|
|
340
|
+
* @internal
|
|
341
|
+
*/
|
|
342
|
+
export function velocityYawTarget(heading, velocity, resolved) {
|
|
343
|
+
if (resolved.velocityYawBlend <= 0)
|
|
344
|
+
return heading;
|
|
345
|
+
const speed = Math.hypot(velocity.x, velocity.z);
|
|
346
|
+
if (speed < resolved.velocityYawMinSpeed)
|
|
347
|
+
return heading;
|
|
348
|
+
const forward = forwardVector(heading);
|
|
349
|
+
if (forward.x * velocity.x + forward.z * velocity.z < 0)
|
|
350
|
+
return heading;
|
|
351
|
+
const slip = angleDelta(heading, Math.atan2(velocity.x, velocity.z));
|
|
352
|
+
const slipFraction = clamp(Math.abs(slip) / (Math.PI / 2), 0, 1);
|
|
353
|
+
return heading + slip * resolved.velocityYawBlend * slipFraction;
|
|
354
|
+
}
|
|
326
355
|
/** World-space seat pose (cockpit/hood/rear) rigidly attached to the vehicle.
|
|
327
356
|
* @internal
|
|
328
357
|
*/
|
package/dist/defineGame.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentType } from "react";
|
|
2
2
|
import { type GameDefinitionConfig, type GameLoop } from "@jgengine/core/game/defineGame";
|
|
3
3
|
import type { GameContext, GameContextContent } from "@jgengine/core/runtime/gameContext";
|
|
4
4
|
import type { ModelAssetRef } from "@jgengine/core/scene/assetCatalog";
|
|
5
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
5
6
|
import type { PlayableGame } from "./registry.js";
|
|
6
7
|
type EngineFields<TAssetRef extends ModelAssetRef> = Omit<GameDefinitionConfig<TAssetRef>, "loop" | "ui" | "multiplayer"> & {
|
|
7
8
|
multiplayer?: GameDefinitionConfig<TAssetRef>["multiplayer"];
|
|
@@ -10,7 +11,20 @@ type PresentationFields = Omit<PlayableGame, "game" | "content" | "loop" | "Game
|
|
|
10
11
|
content?: GameContextContent;
|
|
11
12
|
loop?: Partial<GameLoop<GameContext>>;
|
|
12
13
|
GameUI?: ComponentType;
|
|
14
|
+
/** Tunes how the auto-mounted `AuthoredScene` places the document's catalog-id markers into the object store. Default `true`; pass `false` when the game spawns its placed content as entities itself (`placeAuthoredObjects`) to avoid a double render. */
|
|
15
|
+
scenePlacement?: boolean | {
|
|
16
|
+
verticalOffset?: number;
|
|
17
|
+
};
|
|
18
|
+
/** GLB models for the auto-mounted scene's scatter palette items, keyed by palette item id; string ids resolve through the game's asset catalog. Unmatched items keep the built-in proxy meshes. */
|
|
19
|
+
sceneScatterModels?: Record<string, string | ModelConfig>;
|
|
13
20
|
};
|
|
14
21
|
export type GameConfig<TAssetRef extends ModelAssetRef = ModelAssetRef> = EngineFields<TAssetRef> & PresentationFields;
|
|
22
|
+
/**
|
|
23
|
+
* The one public authoring entry point: compose engine fields (systems, world, physics, input) and
|
|
24
|
+
* presentation fields (camera, HUD, audio, authored scene) into a `PlayableGame` ready for `GameHost`.
|
|
25
|
+
* Defaults to solo/offline multiplayer; `editorLayers` auto-mounts the authored scene document.
|
|
26
|
+
*
|
|
27
|
+
* @capability define-game single public game-authoring path — compose systems, world, presentation, and authored scene in one definition
|
|
28
|
+
*/
|
|
15
29
|
export declare function defineGame<TAssetRef extends ModelAssetRef = ModelAssetRef>(config: GameConfig<TAssetRef>): PlayableGame;
|
|
16
30
|
export {};
|
package/dist/defineGame.js
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { defineGameDefinition as defineEngineGame, } from "@jgengine/core/game/defineGame";
|
|
3
4
|
import { syncLifecyclePhase } from "@jgengine/core/game/gamePhase";
|
|
4
5
|
import { offline } from "@jgengine/core/runtime/adapter";
|
|
5
6
|
import { EnvironmentScene } from "./environment.js";
|
|
7
|
+
import { terrainGroundColorSampler } from "./environment/terrainGroundColor.js";
|
|
8
|
+
import { AuthoredScene } from "./scene/AuthoredScene.js";
|
|
6
9
|
const noop = () => { };
|
|
7
10
|
function worldBackdrop(feature) {
|
|
8
11
|
return function WorldBackdrop() {
|
|
9
12
|
return _jsx(EnvironmentScene, { feature: feature });
|
|
10
13
|
};
|
|
11
14
|
}
|
|
15
|
+
function authoredSceneOverlay(document, placement, scatterModels, assets, world, Vfx) {
|
|
16
|
+
const terrain = world !== undefined && world.kind === "environment" ? world.terrain : undefined;
|
|
17
|
+
return function AuthoredSceneOverlay(props) {
|
|
18
|
+
const groundColorAt = useMemo(() => terrainGroundColorSampler(terrain, props.ctx.world.ground), [props.ctx.world.ground]);
|
|
19
|
+
return (_jsxs(_Fragment, { children: [_jsx(AuthoredScene, { document: document, field: props.ctx.world.ground, placeObjects: placement, ...(scatterModels === undefined ? {} : { scatterModels, assets }), ...(groundColorAt === undefined ? {} : { groundColorAt }) }), Vfx === undefined ? null : _jsx(Vfx, { ...props })] }));
|
|
20
|
+
};
|
|
21
|
+
}
|
|
12
22
|
const emptyUi = () => null;
|
|
23
|
+
/**
|
|
24
|
+
* The one public authoring entry point: compose engine fields (systems, world, physics, input) and
|
|
25
|
+
* presentation fields (camera, HUD, audio, authored scene) into a `PlayableGame` ready for `GameHost`.
|
|
26
|
+
* Defaults to solo/offline multiplayer; `editorLayers` auto-mounts the authored scene document.
|
|
27
|
+
*
|
|
28
|
+
* @capability define-game single public game-authoring path — compose systems, world, presentation, and authored scene in one definition
|
|
29
|
+
*/
|
|
13
30
|
export function defineGame(config) {
|
|
14
|
-
const { content, loop, GameUI, environment, camera, multiplayer, WorldOverlay, viewmodel, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, orientation, worldHealthBars, nameplates, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting, backdrop, postProcessing, shadows, presentation, objectStyles, devtools, capture, platforms, hudFit, ...engineFields } = config;
|
|
31
|
+
const { content, loop, GameUI, environment, camera, multiplayer, editorLayers, editorCatalogs, scenePlacement, sceneScatterModels, WorldOverlay, viewmodel, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, orientation, worldHealthBars, nameplates, presentationEffects, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting, backdrop, postProcessing, shadows, presentation, objectStyles, devtools, capture, platforms, hudFit, ...engineFields } = config;
|
|
15
32
|
const game = defineEngineGame({
|
|
16
33
|
...engineFields,
|
|
17
34
|
multiplayer: multiplayer ?? offline(),
|
|
@@ -39,7 +56,11 @@ export function defineGame(config) {
|
|
|
39
56
|
environment: environment ??
|
|
40
57
|
(game.world?.kind === "environment" ? worldBackdrop(game.world) : undefined),
|
|
41
58
|
camera: camera ?? { perspective: "third" },
|
|
42
|
-
|
|
59
|
+
editorLayers,
|
|
60
|
+
editorCatalogs,
|
|
61
|
+
WorldOverlay: editorLayers === undefined
|
|
62
|
+
? WorldOverlay
|
|
63
|
+
: authoredSceneOverlay(editorLayers, scenePlacement ?? true, sceneScatterModels, game.assets, game.world, WorldOverlay),
|
|
43
64
|
viewmodel,
|
|
44
65
|
renderEntity,
|
|
45
66
|
renderObject,
|
|
@@ -53,6 +74,7 @@ export function defineGame(config) {
|
|
|
53
74
|
orientation,
|
|
54
75
|
worldHealthBars,
|
|
55
76
|
nameplates,
|
|
77
|
+
presentationEffects,
|
|
56
78
|
audio,
|
|
57
79
|
entitySounds,
|
|
58
80
|
objectSounds,
|
|
@@ -30,8 +30,8 @@ export declare function buildLeanReport(playable: PlayableGame): {
|
|
|
30
30
|
pctOfSim: number;
|
|
31
31
|
}[];
|
|
32
32
|
} | null;
|
|
33
|
-
render: import("@jgengine/core/devtools/
|
|
34
|
-
latency: import("@jgengine/core/devtools/
|
|
33
|
+
render: import("@jgengine/core/devtools/types").RenderSample | null;
|
|
34
|
+
latency: import("@jgengine/core/devtools/types").LatencyStats | null;
|
|
35
35
|
longFrameSummary: {
|
|
36
36
|
count: number;
|
|
37
37
|
culprits: {
|
|
@@ -54,10 +54,10 @@ export declare function buildLeanReport(playable: PlayableGame): {
|
|
|
54
54
|
ms: number;
|
|
55
55
|
}[];
|
|
56
56
|
probes: Record<string, unknown>;
|
|
57
|
-
render: import("@jgengine/core/devtools/
|
|
57
|
+
render: import("@jgengine/core/devtools/types").RenderSample | null;
|
|
58
58
|
}[];
|
|
59
59
|
probes: Record<string, unknown>;
|
|
60
|
-
logs: import("@jgengine/core/devtools/
|
|
60
|
+
logs: import("@jgengine/core/devtools/types").DevtoolsLogEntry[] | undefined;
|
|
61
61
|
controls: {
|
|
62
62
|
name: string;
|
|
63
63
|
value: unknown;
|