@vgai/engine 0.2.0 → 0.4.0-canary.20260715.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/README.md +3 -1
- package/package.json +24 -4
- package/schemas/engine-api.json +124 -0
- package/schemas/engine-api.md +53 -0
- package/schemas/engine-capabilities.json +124 -0
- package/schemas/inputmap.schema.json +314 -0
- package/schemas/mat.schema.json +286 -0
- package/schemas/prefab.schema.json +10148 -0
- package/schemas/scn2d.schema.json +475 -0
- package/schemas/vgai-game.schema.json +383 -0
- package/schemas/vscn.schema.json +11007 -0
- package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
- package/src/adapter/authoring.ts +77 -0
- package/src/adapter/first-party-systems.ts +23 -34
- package/src/adapter/game-adapter.ts +8 -8
- package/src/adapter/host-context.ts +2 -4
- package/src/adapter/index.ts +4 -4
- package/src/adapter/system-adapter.ts +88 -22
- package/src/adapter/vgai-scene-game-adapter.ts +244 -194
- package/src/animation/anim-graph-types.ts +12 -43
- package/src/animation/animation-clock.ts +479 -0
- package/src/animation/camera-ownership.ts +467 -0
- package/src/animation/cinematic-cues.ts +451 -0
- package/src/animation/clip-map.ts +41 -0
- package/src/animation/gsap-registration.ts +184 -0
- package/src/animation/theatre-clock-binding.ts +111 -0
- package/src/animation/theatre-director.ts +347 -0
- package/src/animation/theatre-object-binding.ts +661 -0
- package/src/animation/xstate-animation-binding.ts +436 -0
- package/src/animation/xstate-animation-meta.ts +319 -0
- package/src/audio/index.ts +39 -7
- package/src/audio/tone-clock-binding.ts +98 -0
- package/src/audio/tone-context.ts +129 -0
- package/src/audio/tone-offline-render.ts +167 -0
- package/src/audio/wav-encode.ts +119 -0
- package/src/character/cloth-sim.ts +533 -0
- package/src/character/spring-chain.ts +307 -0
- package/src/core/game-loop.ts +57 -2
- package/src/core/seeded-random.ts +161 -0
- package/src/core/system-runner.ts +20 -3
- package/src/core/types.ts +50 -0
- package/src/data/data-asset.ts +167 -0
- package/src/data/data-check-core.ts +242 -0
- package/src/data/data-ref.ts +145 -0
- package/src/data/vite-plugin-data.ts +290 -0
- package/src/dev/performance-profiler.ts +213 -0
- package/src/dev/webgl-gpu-timer.ts +53 -0
- package/src/ecs/component-manager.ts +45 -12
- package/src/ecs/game-component.ts +95 -11
- package/src/humanoid/bake.operation.ts +326 -0
- package/src/humanoid/body.ts +663 -0
- package/src/humanoid/clips.ts +149 -0
- package/src/humanoid/compose.ts +209 -0
- package/src/humanoid/generate.ts +189 -0
- package/src/humanoid/index.ts +36 -0
- package/src/humanoid/schema.ts +108 -0
- package/src/humanoid/skeleton.ts +345 -0
- package/src/index.ts +48 -0
- package/src/input/input-manager.ts +1886 -33
- package/src/input/input-types.ts +158 -3
- package/src/input/prompt-labels.ts +122 -0
- package/src/input/rebind-controller.ts +105 -0
- package/src/input/schema.ts +206 -52
- package/src/manifest/index.ts +5 -5
- package/src/manifest/load.ts +125 -72
- package/src/manifest/schema.ts +362 -255
- package/src/react/game-state.tsx +135 -32
- package/src/react/root-adapter.tsx +49 -0
- package/src/react/unmanaged-root-detector.ts +66 -0
- package/src/react/use-data.ts +124 -0
- package/src/react/use-selection.tsx +135 -0
- package/src/runtime/create-runtime.ts +112 -273
- package/src/runtime/debug-bridge.ts +483 -0
- package/src/runtime/debug-registry.ts +856 -0
- package/src/runtime/game.ts +342 -93
- package/src/runtime/gameplay-rng-trap.ts +134 -0
- package/src/runtime/input-router.ts +7 -7
- package/src/runtime/mount-game.ts +40 -38
- package/src/runtime/mount-manifest.ts +169 -37
- package/src/runtime/render-audio-control.ts +168 -0
- package/src/runtime/render-control.ts +522 -0
- package/src/runtime/render-seed.ts +79 -0
- package/src/runtime/state-bridge.ts +24 -10
- package/src/runtime/types.ts +110 -33
- package/src/scene/asset-loaders.ts +10 -36
- package/src/scene/asset-paths.ts +0 -2
- package/src/scene/asset-ref-check.ts +248 -0
- package/src/scene/asset-registry.ts +22 -0
- package/src/scene/component-registry.ts +14 -3
- package/src/scene/defaults.ts +1 -0
- package/src/scene/light-camera-factory.ts +11 -3
- package/src/scene/parse.ts +133 -0
- package/src/scene/scene-apply.ts +55 -4
- package/src/scene/scene-loader.ts +91 -123
- package/src/scene/scene-types.ts +0 -1
- package/src/scene/schema/animation.ts +30 -79
- package/src/scene/schema/entity.ts +20 -0
- package/src/scene/schema/index.ts +2 -46
- package/src/scene/schema/light.ts +16 -1
- package/src/scene/schema/material.ts +96 -91
- package/src/scene/schema/scene-file.ts +1 -7
- package/src/scene/user-data.ts +22 -10
- package/src/setup/setup-renderer.ts +10 -3
- package/src/tools/define-tool.ts +191 -0
- package/src/world2d/authoring-2d.ts +17 -1
- package/src/world2d/collision-2d.ts +1 -1
- package/src/world2d/pixi-game-adapter.ts +19 -17
- package/src/world2d/scene2d-loader.ts +1 -0
- package/src/world2d/types.ts +8 -2
- package/src/animation/anim-graph.ts +0 -406
- package/src/animation/anim-system.ts +0 -28
- package/src/animation/property-track.ts +0 -178
- package/src/animation/schema.ts +0 -204
- package/src/audio/ambient.ts +0 -300
- package/src/audio/impacts.ts +0 -212
- package/src/audio/movement.ts +0 -140
- package/src/audio/musical.ts +0 -200
- package/src/audio/ui-sounds.ts +0 -171
- package/src/audio/vehicle.ts +0 -235
- package/src/audio/weapons.ts +0 -152
- package/src/runtime/scene-ui-bridge.ts +0 -86
- package/src/runtime/scene-ui-data.ts +0 -119
- package/src/scene/schema/ui.ts +0 -602
package/src/audio/vehicle.ts
DELETED
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vehicle sounds — continuous sounds with real-time parameter control.
|
|
3
|
-
*
|
|
4
|
-
* Each returns play/stop/setX/dispose. Parameters use setTargetAtTime for smooth ramping.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
function createNoiseBuffer(ctx: AudioContext, duration = 2): AudioBuffer {
|
|
8
|
-
const length = ctx.sampleRate * duration;
|
|
9
|
-
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
10
|
-
const data = buffer.getChannelData(0);
|
|
11
|
-
for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
|
|
12
|
-
return buffer;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Engine drone — sawtooth oscillator with throttle-driven pitch and volume. */
|
|
16
|
-
export function engineDrone(
|
|
17
|
-
ctx: AudioContext,
|
|
18
|
-
dest: AudioNode,
|
|
19
|
-
options?: { baseFreq?: number; volume?: number },
|
|
20
|
-
) {
|
|
21
|
-
const baseFreq = options?.baseFreq ?? 60;
|
|
22
|
-
const maxVol = options?.volume ?? 0.4;
|
|
23
|
-
|
|
24
|
-
const out = ctx.createGain();
|
|
25
|
-
out.gain.value = 0;
|
|
26
|
-
out.connect(dest);
|
|
27
|
-
|
|
28
|
-
// Subtle waveshaping for grit
|
|
29
|
-
const shaper = ctx.createWaveShaper();
|
|
30
|
-
const curve = new Float32Array(256);
|
|
31
|
-
for (let i = 0; i < 256; i++) {
|
|
32
|
-
const x = (i / 128 - 1) * 1.5;
|
|
33
|
-
curve[i] = Math.tanh(x);
|
|
34
|
-
}
|
|
35
|
-
shaper.curve = curve;
|
|
36
|
-
shaper.connect(out);
|
|
37
|
-
|
|
38
|
-
let osc: OscillatorNode | null = null;
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
play() {
|
|
42
|
-
if (osc) return;
|
|
43
|
-
osc = ctx.createOscillator();
|
|
44
|
-
osc.type = 'sawtooth';
|
|
45
|
-
osc.frequency.value = baseFreq;
|
|
46
|
-
osc.connect(shaper);
|
|
47
|
-
osc.start();
|
|
48
|
-
out.gain.setTargetAtTime(0.1, ctx.currentTime, 0.05);
|
|
49
|
-
},
|
|
50
|
-
stop() {
|
|
51
|
-
if (!osc) return;
|
|
52
|
-
out.gain.setTargetAtTime(0, ctx.currentTime, 0.05);
|
|
53
|
-
const ref = osc;
|
|
54
|
-
osc = null;
|
|
55
|
-
setTimeout(() => {
|
|
56
|
-
try {
|
|
57
|
-
ref.stop();
|
|
58
|
-
} catch {
|
|
59
|
-
/* already stopped */
|
|
60
|
-
}
|
|
61
|
-
ref.disconnect();
|
|
62
|
-
}, 200);
|
|
63
|
-
},
|
|
64
|
-
/** Set throttle 0–1. Adjusts pitch (1x–3x base) and volume. */
|
|
65
|
-
setThrottle(t: number) {
|
|
66
|
-
if (!osc) return;
|
|
67
|
-
const clamped = Math.max(0, Math.min(1, t));
|
|
68
|
-
osc.frequency.setTargetAtTime(baseFreq + clamped * baseFreq * 2, ctx.currentTime, 0.05);
|
|
69
|
-
out.gain.setTargetAtTime(0.1 + clamped * maxVol, ctx.currentTime, 0.05);
|
|
70
|
-
},
|
|
71
|
-
dispose() {
|
|
72
|
-
if (osc) {
|
|
73
|
-
try {
|
|
74
|
-
osc.stop();
|
|
75
|
-
} catch {
|
|
76
|
-
/* already stopped */
|
|
77
|
-
}
|
|
78
|
-
osc.disconnect();
|
|
79
|
-
osc = null;
|
|
80
|
-
}
|
|
81
|
-
shaper.disconnect();
|
|
82
|
-
out.disconnect();
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Brake squeal — high-frequency oscillator with LFO tremolo. */
|
|
88
|
-
export function brakeSqueal(
|
|
89
|
-
ctx: AudioContext,
|
|
90
|
-
dest: AudioNode,
|
|
91
|
-
options?: { pitch?: number; volume?: number },
|
|
92
|
-
) {
|
|
93
|
-
const pitch = options?.pitch ?? 2000;
|
|
94
|
-
const maxVol = options?.volume ?? 0.3;
|
|
95
|
-
|
|
96
|
-
const out = ctx.createGain();
|
|
97
|
-
out.gain.value = 0;
|
|
98
|
-
out.connect(dest);
|
|
99
|
-
|
|
100
|
-
let osc: OscillatorNode | null = null;
|
|
101
|
-
let lfo: OscillatorNode | null = null;
|
|
102
|
-
let lfoGain: GainNode | null = null;
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
play() {
|
|
106
|
-
if (osc) return;
|
|
107
|
-
osc = ctx.createOscillator();
|
|
108
|
-
osc.type = 'sine';
|
|
109
|
-
osc.frequency.value = pitch;
|
|
110
|
-
|
|
111
|
-
// LFO for tremolo effect
|
|
112
|
-
lfo = ctx.createOscillator();
|
|
113
|
-
lfo.type = 'sine';
|
|
114
|
-
lfo.frequency.value = 30;
|
|
115
|
-
lfoGain = ctx.createGain();
|
|
116
|
-
lfoGain.gain.value = 0.3;
|
|
117
|
-
lfo.connect(lfoGain);
|
|
118
|
-
lfoGain.connect(out.gain);
|
|
119
|
-
|
|
120
|
-
osc.connect(out);
|
|
121
|
-
osc.start();
|
|
122
|
-
lfo.start();
|
|
123
|
-
},
|
|
124
|
-
stop() {
|
|
125
|
-
if (!osc) return;
|
|
126
|
-
out.gain.setTargetAtTime(0, ctx.currentTime, 0.02);
|
|
127
|
-
const refs = { osc, lfo, lfoGain };
|
|
128
|
-
osc = null;
|
|
129
|
-
lfo = null;
|
|
130
|
-
lfoGain = null;
|
|
131
|
-
setTimeout(() => {
|
|
132
|
-
try {
|
|
133
|
-
refs.osc.stop();
|
|
134
|
-
} catch {
|
|
135
|
-
/* already stopped */
|
|
136
|
-
}
|
|
137
|
-
refs.osc.disconnect();
|
|
138
|
-
try {
|
|
139
|
-
refs.lfo?.stop();
|
|
140
|
-
} catch {
|
|
141
|
-
/* already stopped */
|
|
142
|
-
}
|
|
143
|
-
refs.lfo?.disconnect();
|
|
144
|
-
refs.lfoGain?.disconnect();
|
|
145
|
-
}, 100);
|
|
146
|
-
},
|
|
147
|
-
/** Set brake intensity 0–1. */
|
|
148
|
-
setIntensity(i: number) {
|
|
149
|
-
out.gain.setTargetAtTime(Math.max(0, Math.min(1, i)) * maxVol, ctx.currentTime, 0.02);
|
|
150
|
-
},
|
|
151
|
-
dispose() {
|
|
152
|
-
if (osc) {
|
|
153
|
-
try {
|
|
154
|
-
osc.stop();
|
|
155
|
-
} catch {
|
|
156
|
-
/* already stopped */
|
|
157
|
-
}
|
|
158
|
-
osc.disconnect();
|
|
159
|
-
}
|
|
160
|
-
if (lfo) {
|
|
161
|
-
try {
|
|
162
|
-
lfo.stop();
|
|
163
|
-
} catch {
|
|
164
|
-
/* already stopped */
|
|
165
|
-
}
|
|
166
|
-
lfo.disconnect();
|
|
167
|
-
}
|
|
168
|
-
lfoGain?.disconnect();
|
|
169
|
-
osc = null;
|
|
170
|
-
lfo = null;
|
|
171
|
-
lfoGain = null;
|
|
172
|
-
out.disconnect();
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Tire skid — filtered looping noise with intensity control. */
|
|
178
|
-
export function tireSkid(ctx: AudioContext, dest: AudioNode, options?: { volume?: number }) {
|
|
179
|
-
const maxVol = options?.volume ?? 0.3;
|
|
180
|
-
const noiseBuffer = createNoiseBuffer(ctx, 2);
|
|
181
|
-
|
|
182
|
-
const out = ctx.createGain();
|
|
183
|
-
out.gain.value = 0;
|
|
184
|
-
out.connect(dest);
|
|
185
|
-
|
|
186
|
-
const filter = ctx.createBiquadFilter();
|
|
187
|
-
filter.type = 'bandpass';
|
|
188
|
-
filter.frequency.value = 800;
|
|
189
|
-
filter.Q.value = 1.5;
|
|
190
|
-
filter.connect(out);
|
|
191
|
-
|
|
192
|
-
let src: AudioBufferSourceNode | null = null;
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
play() {
|
|
196
|
-
if (src) return;
|
|
197
|
-
src = ctx.createBufferSource();
|
|
198
|
-
src.buffer = noiseBuffer;
|
|
199
|
-
src.loop = true;
|
|
200
|
-
src.connect(filter);
|
|
201
|
-
src.start();
|
|
202
|
-
},
|
|
203
|
-
stop() {
|
|
204
|
-
if (!src) return;
|
|
205
|
-
out.gain.setTargetAtTime(0, ctx.currentTime, 0.02);
|
|
206
|
-
const ref = src;
|
|
207
|
-
src = null;
|
|
208
|
-
setTimeout(() => {
|
|
209
|
-
try {
|
|
210
|
-
ref.stop();
|
|
211
|
-
} catch {
|
|
212
|
-
/* already stopped */
|
|
213
|
-
}
|
|
214
|
-
ref.disconnect();
|
|
215
|
-
}, 100);
|
|
216
|
-
},
|
|
217
|
-
/** Set skid intensity 0–1. */
|
|
218
|
-
setIntensity(i: number) {
|
|
219
|
-
out.gain.setTargetAtTime(Math.max(0, Math.min(1, i)) * maxVol, ctx.currentTime, 0.02);
|
|
220
|
-
},
|
|
221
|
-
dispose() {
|
|
222
|
-
if (src) {
|
|
223
|
-
try {
|
|
224
|
-
src.stop();
|
|
225
|
-
} catch {
|
|
226
|
-
/* already stopped */
|
|
227
|
-
}
|
|
228
|
-
src.disconnect();
|
|
229
|
-
}
|
|
230
|
-
src = null;
|
|
231
|
-
filter.disconnect();
|
|
232
|
-
out.disconnect();
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
|
-
}
|
package/src/audio/weapons.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Weapon sounds — procedural combat audio.
|
|
3
|
-
*
|
|
4
|
-
* gunshot and laser are one-shot. chargeUp is continuous (play/stop).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
function createNoiseBuffer(ctx: AudioContext, duration = 1): AudioBuffer {
|
|
8
|
-
const length = ctx.sampleRate * duration;
|
|
9
|
-
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
10
|
-
const data = buffer.getChannelData(0);
|
|
11
|
-
for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
|
|
12
|
-
return buffer;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Sharp gunshot — highpass-filtered noise click. */
|
|
16
|
-
export function gunshot(
|
|
17
|
-
ctx: AudioContext,
|
|
18
|
-
dest: AudioNode,
|
|
19
|
-
options?: { pitch?: number; volume?: number; duration?: number },
|
|
20
|
-
) {
|
|
21
|
-
const pitch = options?.pitch ?? 1200;
|
|
22
|
-
const volume = options?.volume ?? 0.6;
|
|
23
|
-
const duration = options?.duration ?? 0.08;
|
|
24
|
-
const noiseBuffer = createNoiseBuffer(ctx, 0.3);
|
|
25
|
-
|
|
26
|
-
const out = ctx.createGain();
|
|
27
|
-
out.gain.value = volume;
|
|
28
|
-
out.connect(dest);
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
play() {
|
|
32
|
-
const now = ctx.currentTime;
|
|
33
|
-
const src = ctx.createBufferSource();
|
|
34
|
-
src.buffer = noiseBuffer;
|
|
35
|
-
|
|
36
|
-
const hp = ctx.createBiquadFilter();
|
|
37
|
-
hp.type = 'highpass';
|
|
38
|
-
hp.frequency.value = pitch;
|
|
39
|
-
|
|
40
|
-
const env = ctx.createGain();
|
|
41
|
-
env.gain.setValueAtTime(1, now);
|
|
42
|
-
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
|
43
|
-
|
|
44
|
-
src.connect(hp);
|
|
45
|
-
hp.connect(env);
|
|
46
|
-
env.connect(out);
|
|
47
|
-
src.start(now);
|
|
48
|
-
src.stop(now + duration + 0.01);
|
|
49
|
-
},
|
|
50
|
-
dispose() {
|
|
51
|
-
out.disconnect();
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Laser — descending sawtooth frequency sweep. */
|
|
57
|
-
export function laser(
|
|
58
|
-
ctx: AudioContext,
|
|
59
|
-
dest: AudioNode,
|
|
60
|
-
options?: { startFreq?: number; endFreq?: number; volume?: number; duration?: number },
|
|
61
|
-
) {
|
|
62
|
-
const startFreq = options?.startFreq ?? 1200;
|
|
63
|
-
const endFreq = options?.endFreq ?? 200;
|
|
64
|
-
const volume = options?.volume ?? 0.4;
|
|
65
|
-
const duration = options?.duration ?? 0.2;
|
|
66
|
-
|
|
67
|
-
const out = ctx.createGain();
|
|
68
|
-
out.gain.value = volume;
|
|
69
|
-
out.connect(dest);
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
play() {
|
|
73
|
-
const now = ctx.currentTime;
|
|
74
|
-
const osc = ctx.createOscillator();
|
|
75
|
-
osc.type = 'sawtooth';
|
|
76
|
-
osc.frequency.setValueAtTime(startFreq, now);
|
|
77
|
-
osc.frequency.exponentialRampToValueAtTime(endFreq, now + duration);
|
|
78
|
-
|
|
79
|
-
const env = ctx.createGain();
|
|
80
|
-
env.gain.setValueAtTime(1, now);
|
|
81
|
-
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
|
82
|
-
|
|
83
|
-
osc.connect(env);
|
|
84
|
-
env.connect(out);
|
|
85
|
-
osc.start(now);
|
|
86
|
-
osc.stop(now + duration + 0.01);
|
|
87
|
-
},
|
|
88
|
-
dispose() {
|
|
89
|
-
out.disconnect();
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Charge-up tone — ascending pitch, call play() to start and stop() to end. */
|
|
95
|
-
export function chargeUp(
|
|
96
|
-
ctx: AudioContext,
|
|
97
|
-
dest: AudioNode,
|
|
98
|
-
options?: { startFreq?: number; endFreq?: number; volume?: number; duration?: number },
|
|
99
|
-
) {
|
|
100
|
-
const startFreq = options?.startFreq ?? 100;
|
|
101
|
-
const endFreq = options?.endFreq ?? 800;
|
|
102
|
-
const volume = options?.volume ?? 0.3;
|
|
103
|
-
const duration = options?.duration ?? 2.0;
|
|
104
|
-
|
|
105
|
-
const out = ctx.createGain();
|
|
106
|
-
out.gain.value = 0;
|
|
107
|
-
out.connect(dest);
|
|
108
|
-
|
|
109
|
-
let osc: OscillatorNode | null = null;
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
play() {
|
|
113
|
-
if (osc) return;
|
|
114
|
-
const now = ctx.currentTime;
|
|
115
|
-
osc = ctx.createOscillator();
|
|
116
|
-
osc.type = 'sawtooth';
|
|
117
|
-
osc.frequency.setValueAtTime(startFreq, now);
|
|
118
|
-
osc.frequency.exponentialRampToValueAtTime(endFreq, now + duration);
|
|
119
|
-
osc.connect(out);
|
|
120
|
-
osc.start(now);
|
|
121
|
-
out.gain.setValueAtTime(0.001, now);
|
|
122
|
-
out.gain.exponentialRampToValueAtTime(volume, now + 0.05);
|
|
123
|
-
},
|
|
124
|
-
stop() {
|
|
125
|
-
if (!osc) return;
|
|
126
|
-
const now = ctx.currentTime;
|
|
127
|
-
out.gain.setTargetAtTime(0, now, 0.02);
|
|
128
|
-
const ref = osc;
|
|
129
|
-
osc = null;
|
|
130
|
-
setTimeout(() => {
|
|
131
|
-
try {
|
|
132
|
-
ref.stop();
|
|
133
|
-
} catch {
|
|
134
|
-
/* already stopped */
|
|
135
|
-
}
|
|
136
|
-
ref.disconnect();
|
|
137
|
-
}, 100);
|
|
138
|
-
},
|
|
139
|
-
dispose() {
|
|
140
|
-
if (osc) {
|
|
141
|
-
try {
|
|
142
|
-
osc.stop();
|
|
143
|
-
} catch {
|
|
144
|
-
/* already stopped */
|
|
145
|
-
}
|
|
146
|
-
osc.disconnect();
|
|
147
|
-
osc = null;
|
|
148
|
-
}
|
|
149
|
-
out.disconnect();
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scene-UI render bridge (B1, widened design/24-scene-ui.md D3) — the injection
|
|
3
|
-
* seam that keeps the engine React-free.
|
|
4
|
-
*
|
|
5
|
-
* The engine knows the scene carries a `ui` tree (data), but it does NOT know how to
|
|
6
|
-
* render React. An editor-side module registers a renderer here (a thin wrapper over
|
|
7
|
-
* the React `mountSceneUI`); the first-party scene adapter calls it at game-time to
|
|
8
|
-
* mount the scene's UI into `host.ui` and disposes it on teardown. Fully decoupled:
|
|
9
|
-
* the engine treats `sceneUI` and the handle as opaque. The optional third `services`
|
|
10
|
-
* argument carries game-sourced data/components/events/projector (D2-D7) — still
|
|
11
|
-
* opaque to the engine, still zero React import here.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Structural match for `@vgai/scene-ui`'s `UIDataSource` — engine stays React-free
|
|
16
|
-
* (this interface is duck-typed against the real one, not imported from it).
|
|
17
|
-
*/
|
|
18
|
-
export interface SceneUIDataSourceLike {
|
|
19
|
-
get(path: string): unknown;
|
|
20
|
-
set(path: string, value: unknown): void;
|
|
21
|
-
subscribe(listener: () => void): () => void;
|
|
22
|
-
version(): number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Game-time services fed into a scene's UI mount (D2's "dynamic" layer). Every
|
|
27
|
-
* field is optional: a renderer falls back to its own registration-time registry
|
|
28
|
-
* (D2 merge rule) or built-in default when a field is absent. `components`/
|
|
29
|
-
* `transforms`/`strings` stay `unknown`-valued here — they're React things the
|
|
30
|
-
* scene-ui renderer casts to its real `UIComponentRegistry`/`TransformRegistry`/
|
|
31
|
-
* `StringTables`.
|
|
32
|
-
*/
|
|
33
|
-
export interface SceneUIGameServices {
|
|
34
|
-
/** Reactive game state the UI binds against (D4's `gameStateDataSource`, or a
|
|
35
|
-
* game's own bus). Absent -> the renderer falls back to an empty static store. */
|
|
36
|
-
data?: SceneUIDataSourceLike;
|
|
37
|
-
/** `kind:"component"` leaf implementations, keyed by the name authored in the scene. */
|
|
38
|
-
components?: Record<string, unknown>;
|
|
39
|
-
/** Named binding transforms (`$bind.transform`). */
|
|
40
|
-
transforms?: Record<string, unknown>;
|
|
41
|
-
/** Localized string tables, keyed by locale then textKey. */
|
|
42
|
-
strings?: Record<string, Record<string, string>>;
|
|
43
|
-
locale?: string;
|
|
44
|
-
/** Routes a `kind:"button"` (etc.) `events.onClick` handler id to the game.
|
|
45
|
-
* Absent -> a loud console.warn per event (D6 — no magic engine event bus). */
|
|
46
|
-
onEvent?: (handlerId: string, payload?: unknown) => void;
|
|
47
|
-
/** Projects an entity id to a screen-space point for world-tracked canvases
|
|
48
|
-
* (D5). Returns null when the entity is unknown or behind the camera. */
|
|
49
|
-
worldProjector?: (entityId: string) => { x: number; y: number } | null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface SceneUIHandle {
|
|
53
|
-
dispose(): void;
|
|
54
|
-
/** Re-render with new services without a remount (a late `setSceneUIServices`
|
|
55
|
-
* call after the initial mount, D7). Optional — a renderer that can't update
|
|
56
|
-
* in place may omit it; callers dispose+remount instead. */
|
|
57
|
-
update?(services: SceneUIGameServices): void;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Renderer signature — `sceneUI` is the scene's `ui` array (opaque to the engine).
|
|
62
|
-
* `services` is optional so existing 2-arg renderer functions keep working
|
|
63
|
-
* unchanged (back-compat, D3/D5 §5).
|
|
64
|
-
*/
|
|
65
|
-
export type SceneUIRenderer = (
|
|
66
|
-
container: HTMLElement,
|
|
67
|
-
sceneUI: unknown,
|
|
68
|
-
services?: SceneUIGameServices,
|
|
69
|
-
) => SceneUIHandle;
|
|
70
|
-
|
|
71
|
-
let _renderer: SceneUIRenderer | null = null;
|
|
72
|
-
|
|
73
|
-
/** Register the React renderer (called once by the editor/runtime host). */
|
|
74
|
-
export function setSceneUIRenderer(renderer: SceneUIRenderer | null): void {
|
|
75
|
-
_renderer = renderer;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Render a scene's UI into `container` if a renderer is registered, else no-op. */
|
|
79
|
-
export function renderSceneUI(
|
|
80
|
-
container: HTMLElement,
|
|
81
|
-
sceneUI: unknown,
|
|
82
|
-
services?: SceneUIGameServices,
|
|
83
|
-
): SceneUIHandle | null {
|
|
84
|
-
if (!_renderer || sceneUI == null) return null;
|
|
85
|
-
return _renderer(container, sceneUI, services);
|
|
86
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* gameStateDataSource (design/24-scene-ui.md D4) — the default scene-UI data
|
|
3
|
-
* source: binds a scene's authored UI directly against live `GameComponent`
|
|
4
|
-
* instance fields, with zero game-side wiring.
|
|
5
|
-
*
|
|
6
|
-
* Path grammar: `"<ComponentName>.<field.path>"` — the first segment resolves
|
|
7
|
-
* through the SAME `ComponentRegistry` that resolves a scene's `components:`
|
|
8
|
-
* entries (name -> class) to `game.queryByComponent(Class)[0]`, the rest walks
|
|
9
|
-
* the live instance's own fields (components mutate `this` freely — the
|
|
10
|
-
* state-bridge contract, `runtime/state-bridge.ts`). A text node bound to
|
|
11
|
-
* `S1Session.wallet.money` just works.
|
|
12
|
-
*
|
|
13
|
-
* React-free, plain TS — `SceneUIDataSourceLike` is a structural (duck-typed)
|
|
14
|
-
* match for `@vgai/scene-ui`'s real `UIDataSource`, so the engine never
|
|
15
|
-
* imports the runtime that consumes this.
|
|
16
|
-
*/
|
|
17
|
-
import type { GameComponent, GameComponentClass } from '../ecs/game-component';
|
|
18
|
-
import type { ComponentRegistry } from '../scene/component-registry';
|
|
19
|
-
import type { Game } from './game';
|
|
20
|
-
import type { SceneUIDataSourceLike } from './scene-ui-bridge';
|
|
21
|
-
|
|
22
|
-
function splitRoot(path: string): [root: string, rest: string] {
|
|
23
|
-
const dot = path.indexOf('.');
|
|
24
|
-
return dot === -1 ? [path, ''] : [path.slice(0, dot), path.slice(dot + 1)];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function readRest(obj: unknown, rest: string): unknown {
|
|
28
|
-
if (!rest) return obj;
|
|
29
|
-
let cur: unknown = obj;
|
|
30
|
-
for (const key of rest.split('.')) {
|
|
31
|
-
if (cur == null || typeof cur !== 'object') return undefined;
|
|
32
|
-
cur = (cur as Record<string, unknown>)[key];
|
|
33
|
-
}
|
|
34
|
-
return cur;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Build the default `SceneUIDataSourceLike` over a game's live components.
|
|
39
|
-
*
|
|
40
|
-
* `subscribe`/`version` are driven by `game.state` (bumps once per completed
|
|
41
|
-
* frame, after every world's phases — see `state-bridge.ts`) PLUS a local
|
|
42
|
-
* bump on `set()`, so a two-way control's own write echoes immediately
|
|
43
|
-
* instead of waiting for the next frame.
|
|
44
|
-
*/
|
|
45
|
-
export function gameStateDataSource(
|
|
46
|
-
game: Game,
|
|
47
|
-
registry: ComponentRegistry,
|
|
48
|
-
): SceneUIDataSourceLike {
|
|
49
|
-
let version = 0;
|
|
50
|
-
const listeners = new Set<() => void>();
|
|
51
|
-
// Warn-once-per-root-segment (D4/§9 review checklist): a scene author
|
|
52
|
-
// typo'ing a component name in a binding should be loud exactly once, not
|
|
53
|
-
// on every `get()` (a bound node re-reads its path every render).
|
|
54
|
-
const warnedRoots = new Set<string>();
|
|
55
|
-
|
|
56
|
-
const notify = (): void => {
|
|
57
|
-
for (const listener of listeners) listener();
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** Resolves a root segment to its live component instance, or undefined. */
|
|
61
|
-
const resolveInstance = (root: string): GameComponent | undefined => {
|
|
62
|
-
const Klass: GameComponentClass | undefined = registry[root];
|
|
63
|
-
if (!Klass) {
|
|
64
|
-
if (!warnedRoots.has(root)) {
|
|
65
|
-
warnedRoots.add(root);
|
|
66
|
-
console.warn(
|
|
67
|
-
`scene-ui: data binding root "${root}" is not a registered component name — ` +
|
|
68
|
-
'check the binding source against src/scripts/registry.ts.',
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
// Registered but zero live instances (e.g. not spawned yet this scene) is
|
|
74
|
-
// an ordinary, expected transient state — stays undefined, no warning.
|
|
75
|
-
return game.queryByComponent(Klass)[0];
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// ONE subscription to the frame-versioned state bridge for this data
|
|
79
|
-
// source's whole lifetime, fanning out to however many external
|
|
80
|
-
// subscribers this scene-UI mount has.
|
|
81
|
-
game.state.subscribe(() => {
|
|
82
|
-
version++;
|
|
83
|
-
notify();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
get(path: string): unknown {
|
|
88
|
-
const [root, rest] = splitRoot(path);
|
|
89
|
-
const inst = resolveInstance(root);
|
|
90
|
-
return inst === undefined ? undefined : readRest(inst, rest);
|
|
91
|
-
},
|
|
92
|
-
set(path: string, value: unknown): void {
|
|
93
|
-
const [root, rest] = splitRoot(path);
|
|
94
|
-
const inst = resolveInstance(root);
|
|
95
|
-
if (inst === undefined || !rest) return;
|
|
96
|
-
const keys = rest.split('.');
|
|
97
|
-
let cur: Record<string, unknown> = inst as unknown as Record<string, unknown>;
|
|
98
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
99
|
-
const key = keys[i] as string;
|
|
100
|
-
const next = cur[key];
|
|
101
|
-
if (next == null || typeof next !== 'object') return; // don't invent nested fields on a live instance
|
|
102
|
-
cur = next as Record<string, unknown>;
|
|
103
|
-
}
|
|
104
|
-
cur[keys[keys.length - 1] as string] = value;
|
|
105
|
-
// Two-way writes echo immediately, not on the next frame (D4).
|
|
106
|
-
version++;
|
|
107
|
-
notify();
|
|
108
|
-
},
|
|
109
|
-
subscribe(listener: () => void): () => void {
|
|
110
|
-
listeners.add(listener);
|
|
111
|
-
return () => {
|
|
112
|
-
listeners.delete(listener);
|
|
113
|
-
};
|
|
114
|
-
},
|
|
115
|
-
version(): number {
|
|
116
|
-
return version;
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
}
|