lecodes-sdk 2.3.0 → 2.4.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/package.json CHANGED
@@ -43,7 +43,7 @@
43
43
  "marcidb-embedded": "^0.15.0",
44
44
  "sharp": "^0.35.2"
45
45
  },
46
- "version": "2.3.0",
46
+ "version": "2.4.0",
47
47
  "files": [
48
48
  "src",
49
49
  "dist",
@@ -1,175 +1,175 @@
1
- // `Timeline` (docs/timeline-plan.md §2.4): choreography over the keyframe core. Tracks on any
2
- // number of targets (UI elements, 3D / 2D nodes, lights, cameras) at absolute positions, JS calls
3
- // at times, labels, holds — one clock, one handle. Also the `animateTo` / `animateFrom` bag
4
- // builder every target class delegates to (`tweenBag`).
5
-
6
- import { TweenAnimation, type Animation } from "./Animation"
7
- import { makeValueTrack, type AnimateOptions, type AnimateValue } from "./animateValue"
8
- import { CLOCK_GAME, CLOCK_UI, bagProps, iterationsOf, makeTrack, type Track, type TweenMeta, type TweenSpec, type TweenTarget } from "./spec"
9
-
10
- /** Vector-valued props: an array value is ONE keyframe unless it is an array of vectors. */
11
- const VECTOR_PROPS = new Set(["position", "scale", "quaternion", "eulerAngles", "color"])
12
-
13
- const clockOf = (clock: TweenMeta["clock"], fallback: number): number =>
14
- clock === "game" ? CLOCK_GAME : clock === "ui" ? CLOCK_UI : fallback
15
-
16
- /** @internal Build the tracks of one options bag on one target. `fromCurrent` = animateTo shape. */
17
- export const bagTracks = (target: TweenTarget, bag: Record<string, unknown>, atMs: number, fromCurrent: boolean): Track[] => {
18
- const meta = bag as TweenMeta
19
- const tracks: Track[] = []
20
- for (const [prop, value] of bagProps(bag)) {
21
- const track = makeTrack(target, prop, value, meta, atMs, fromCurrent, VECTOR_PROPS.has(prop))
22
- if (track) tracks.push(track)
23
- }
24
- return tracks
25
- }
26
-
27
- /** @internal The `animateTo` / `animateFrom` implementation: one bag → one animation, played now. */
28
- export const tweenBag = (target: TweenTarget, bag: Record<string, unknown>, fromCurrent: boolean): Animation => {
29
- const meta = bag as TweenMeta
30
- const tracks = bagTracks(target, bag, 0, fromCurrent)
31
- let durationMs = 0
32
- for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
33
- const spec: TweenSpec = {
34
- clock: clockOf(meta.clock, target._tweenClock),
35
- durationMs, delayMs: 0,
36
- iterations: iterationsOf(meta.loop),
37
- pingPong: meta.loopMode !== "restart",
38
- rate: 1, tracks, calls: [],
39
- }
40
- return new TweenAnimation(spec).play()
41
- }
42
-
43
- export type TimelineOptions = {
44
- /** `'ui'` (default) = wall time; `'game'` follows `Time.scale` and pauses with the game. */
45
- clock?: "ui" | "game",
46
- /** Repeat the whole timeline: `true` = forever, a number = cycles. */
47
- loop?: boolean | number,
48
- loopMode?: "restart" | "ping-pong",
49
- /** Wait before the first play, ms. */
50
- delay?: number,
51
- }
52
-
53
- /** Where a track goes: ms, a label, or `[label, offsetMs]`. Default = the current end (sequencing). */
54
- export type TimelinePosition = number | string | [string, number]
55
-
56
- export type TimelineAddOptions = {
57
- at?: TimelinePosition,
58
- /** With several targets: each starts this many ms after the previous one. */
59
- stagger?: number,
60
- }
61
-
62
- /** A choreography: tracks on many targets at absolute times, JS calls, labels and holds, driven by
63
- * one clock. Build it once, `play()` on every entrance — `play()` restarts from t = 0 and the pose
64
- * at any time is fully defined by the tracks (the first keyframe of a track holds before it
65
- * starts, the last one after it ends). */
66
- export class TimelineImpl extends TweenAnimation {
67
- private _labels = new Map<string, number>()
68
- private _end = 0
69
-
70
- constructor(options: TimelineOptions = {}) {
71
- super({
72
- clock: clockOf(options.clock, CLOCK_UI),
73
- durationMs: 0,
74
- delayMs: options.delay ?? 0,
75
- iterations: iterationsOf(options.loop),
76
- pingPong: options.loopMode !== "restart",
77
- rate: 1, tracks: [], calls: [],
78
- })
79
- }
80
-
81
- private _at(at: TimelinePosition | undefined): number {
82
- if (at === undefined) return this._end
83
- if (typeof at === "number") return at
84
- const name = Array.isArray(at) ? at[0] : at
85
- const offset = Array.isArray(at) ? at[1] : 0
86
- const base = this._labels.get(name)
87
- if (base === undefined) throw new Error(`Timeline: unknown label "${name}"`)
88
- return base + offset
89
- }
90
-
91
- private _grow(endMs: number): void {
92
- if (endMs > this._end) this._end = endMs
93
- this._spec.durationMs = this._end
94
- }
95
-
96
- /** Animate `props` on one target or several (staggered), like `animateTo` — arrays are keyframes,
97
- * `duration` / `easing` / `times` / `commit` apply. The bag's `delay` shifts the track after `at`. */
98
- add(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
99
- const at = this._at(options.at)
100
- const targets = Array.isArray(target) ? target : [target]
101
- const stagger = options.stagger ?? 0
102
- targets.forEach((t, i) => {
103
- const tracks = bagTracks(t, props, at + i * stagger, true)
104
- for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
105
- })
106
- return this
107
- }
108
-
109
- /** Like `add` with the `animateFrom` shape: the target's own state is the implicit last keyframe. */
110
- addFrom(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
111
- const at = this._at(options.at)
112
- const targets = Array.isArray(target) ? target : [target]
113
- const stagger = options.stagger ?? 0
114
- targets.forEach((t, i) => {
115
- const tracks = bagTracks(t, props, at + i * stagger, false)
116
- for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
117
- })
118
- return this
119
- }
120
-
121
- /** A free-value track (see the `animate()` global): the host evaluates it and calls `onUpdate`
122
- * every frame with the value. */
123
- animate<T extends AnimateValue>(options: AnimateOptions<T>, at?: TimelinePosition): this {
124
- const { track, deliver, slot } = makeValueTrack(options, this._at(at))
125
- this._values.set(slot, deliver)
126
- this._spec.tracks.push(track)
127
- this._grow(track.atMs + track.durMs)
128
- return this
129
- }
130
-
131
- /** Call `fn` when playback crosses `at` (forward or backward), in time order, always before
132
- * finish, never from a previous run. */
133
- call(at: TimelinePosition, fn: () => void): this {
134
- const t = this._at(at)
135
- this._spec.calls.push(t)
136
- this._calls.push(fn)
137
- this._grow(t)
138
- return this
139
- }
140
-
141
- /** Name a time — for `at`, `seek` and callers (`tl.labels.landed`). Default = the current end. */
142
- label(name: string, at?: TimelinePosition): this {
143
- this._labels.set(name, this._at(at))
144
- return this
145
- }
146
-
147
- /** Named times, ms. */
148
- get labels(): Record<string, number> {
149
- const o: Record<string, number> = {}
150
- for (const [k, v] of this._labels) o[k] = v
151
- return o
152
- }
153
-
154
- /** Extend the timeline with stillness after its current end. */
155
- hold(ms: number): this {
156
- this._grow(this._end + Math.max(0, ms))
157
- return this
158
- }
159
-
160
- /** The current end, ms (tracks, calls and holds). */
161
- get end(): number { return this._end }
162
-
163
- /** `seek` also accepts a label. */
164
- override seek(at: number | string): this {
165
- return super.seek(typeof at === "string" ? this._at(at) : at)
166
- }
167
- }
168
-
169
- /** The timeline handle type (see {@link TimelineImpl} for the members). */
170
- export type Timeline = TimelineImpl
171
-
172
- /** Create a timeline — `Timeline()` / `Timeline({ clock: 'game', loop: true })`. */
173
- export function Timeline(options?: TimelineOptions): Timeline {
174
- return new TimelineImpl(options)
175
- }
1
+ // `Timeline` (docs/timeline-plan.md §2.4): choreography over the keyframe core. Tracks on any
2
+ // number of targets (UI elements, 3D / 2D nodes, lights, cameras) at absolute positions, JS calls
3
+ // at times, labels, holds — one clock, one handle. Also the `animateTo` / `animateFrom` bag
4
+ // builder every target class delegates to (`tweenBag`).
5
+
6
+ import { TweenAnimation, type Animation } from "./Animation"
7
+ import { makeValueTrack, type AnimateOptions, type AnimateValue } from "./animateValue"
8
+ import { CLOCK_GAME, CLOCK_UI, bagProps, iterationsOf, makeTrack, type Track, type TweenMeta, type TweenSpec, type TweenTarget } from "./spec"
9
+
10
+ /** Vector-valued props: an array value is ONE keyframe unless it is an array of vectors. */
11
+ const VECTOR_PROPS = new Set(["position", "scale", "quaternion", "eulerAngles", "color"])
12
+
13
+ const clockOf = (clock: TweenMeta["clock"], fallback: number): number =>
14
+ clock === "game" ? CLOCK_GAME : clock === "ui" ? CLOCK_UI : fallback
15
+
16
+ /** @internal Build the tracks of one options bag on one target. `fromCurrent` = animateTo shape. */
17
+ export const bagTracks = (target: TweenTarget, bag: Record<string, unknown>, atMs: number, fromCurrent: boolean): Track[] => {
18
+ const meta = bag as TweenMeta
19
+ const tracks: Track[] = []
20
+ for (const [prop, value] of bagProps(bag)) {
21
+ const track = makeTrack(target, prop, value, meta, atMs, fromCurrent, VECTOR_PROPS.has(prop))
22
+ if (track) tracks.push(track)
23
+ }
24
+ return tracks
25
+ }
26
+
27
+ /** @internal The `animateTo` / `animateFrom` implementation: one bag → one animation, played now. */
28
+ export const tweenBag = (target: TweenTarget, bag: Record<string, unknown>, fromCurrent: boolean): Animation => {
29
+ const meta = bag as TweenMeta
30
+ const tracks = bagTracks(target, bag, 0, fromCurrent)
31
+ let durationMs = 0
32
+ for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
33
+ const spec: TweenSpec = {
34
+ clock: clockOf(meta.clock, target._tweenClock),
35
+ durationMs, delayMs: 0,
36
+ iterations: iterationsOf(meta.loop),
37
+ pingPong: meta.loopMode !== "restart",
38
+ rate: 1, tracks, calls: [],
39
+ }
40
+ return new TweenAnimation(spec).play()
41
+ }
42
+
43
+ export type TimelineOptions = {
44
+ /** `'ui'` (default) = wall time; `'game'` follows `Time.scale` and pauses with the game. */
45
+ clock?: "ui" | "game",
46
+ /** Repeat the whole timeline: `true` = forever, a number = cycles. */
47
+ loop?: boolean | number,
48
+ loopMode?: "restart" | "ping-pong",
49
+ /** Wait before the first play, ms. */
50
+ delay?: number,
51
+ }
52
+
53
+ /** Where a track goes: ms, a label, or `[label, offsetMs]`. Default = the current end (sequencing). */
54
+ export type TimelinePosition = number | string | [string, number]
55
+
56
+ export type TimelineAddOptions = {
57
+ at?: TimelinePosition,
58
+ /** With several targets: each starts this many ms after the previous one. */
59
+ stagger?: number,
60
+ }
61
+
62
+ /** A choreography: tracks on many targets at absolute times, JS calls, labels and holds, driven by
63
+ * one clock. Build it once, `play()` on every entrance — `play()` restarts from t = 0 and the pose
64
+ * at any time is fully defined by the tracks (the first keyframe of a track holds before it
65
+ * starts, the last one after it ends). */
66
+ export class TimelineImpl extends TweenAnimation {
67
+ private _labels = new Map<string, number>()
68
+ private _end = 0
69
+
70
+ constructor(options: TimelineOptions = {}) {
71
+ super({
72
+ clock: clockOf(options.clock, CLOCK_UI),
73
+ durationMs: 0,
74
+ delayMs: options.delay ?? 0,
75
+ iterations: iterationsOf(options.loop),
76
+ pingPong: options.loopMode !== "restart",
77
+ rate: 1, tracks: [], calls: [],
78
+ })
79
+ }
80
+
81
+ private _at(at: TimelinePosition | undefined): number {
82
+ if (at === undefined) return this._end
83
+ if (typeof at === "number") return at
84
+ const name = Array.isArray(at) ? at[0] : at
85
+ const offset = Array.isArray(at) ? at[1] : 0
86
+ const base = this._labels.get(name)
87
+ if (base === undefined) throw new Error(`Timeline: unknown label "${name}"`)
88
+ return base + offset
89
+ }
90
+
91
+ private _grow(endMs: number): void {
92
+ if (endMs > this._end) this._end = endMs
93
+ this._spec.durationMs = this._end
94
+ }
95
+
96
+ /** Animate `props` on one target or several (staggered), like `animateTo` — arrays are keyframes,
97
+ * `duration` / `easing` / `times` / `commit` apply. The bag's `delay` shifts the track after `at`. */
98
+ add(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
99
+ const at = this._at(options.at)
100
+ const targets = Array.isArray(target) ? target : [target]
101
+ const stagger = options.stagger ?? 0
102
+ targets.forEach((t, i) => {
103
+ const tracks = bagTracks(t, props, at + i * stagger, true)
104
+ for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
105
+ })
106
+ return this
107
+ }
108
+
109
+ /** Like `add` with the `animateFrom` shape: the target's own state is the implicit last keyframe. */
110
+ addFrom(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
111
+ const at = this._at(options.at)
112
+ const targets = Array.isArray(target) ? target : [target]
113
+ const stagger = options.stagger ?? 0
114
+ targets.forEach((t, i) => {
115
+ const tracks = bagTracks(t, props, at + i * stagger, false)
116
+ for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
117
+ })
118
+ return this
119
+ }
120
+
121
+ /** A free-value track (see the `animate()` global): the host evaluates it and calls `onUpdate`
122
+ * every frame with the value. */
123
+ animate<T extends AnimateValue>(options: AnimateOptions<T>, at?: TimelinePosition): this {
124
+ const { track, deliver, slot } = makeValueTrack(options, this._at(at))
125
+ this._values.set(slot, deliver)
126
+ this._spec.tracks.push(track)
127
+ this._grow(track.atMs + track.durMs)
128
+ return this
129
+ }
130
+
131
+ /** Call `fn` when playback crosses `at` (forward or backward), in time order, always before
132
+ * finish, never from a previous run. */
133
+ call(at: TimelinePosition, fn: () => void): this {
134
+ const t = this._at(at)
135
+ this._spec.calls.push(t)
136
+ this._calls.push(fn)
137
+ this._grow(t)
138
+ return this
139
+ }
140
+
141
+ /** Name a time — for `at`, `seek` and callers (`tl.labels.landed`). Default = the current end. */
142
+ label(name: string, at?: TimelinePosition): this {
143
+ this._labels.set(name, this._at(at))
144
+ return this
145
+ }
146
+
147
+ /** Named times, ms. */
148
+ get labels(): Record<string, number> {
149
+ const o: Record<string, number> = {}
150
+ for (const [k, v] of this._labels) o[k] = v
151
+ return o
152
+ }
153
+
154
+ /** Extend the timeline with stillness after its current end. */
155
+ hold(ms: number): this {
156
+ this._grow(this._end + Math.max(0, ms))
157
+ return this
158
+ }
159
+
160
+ /** The current end, ms (tracks, calls and holds). */
161
+ get end(): number { return this._end }
162
+
163
+ /** `seek` also accepts a label. */
164
+ override seek(at: number | string): this {
165
+ return super.seek(typeof at === "string" ? this._at(at) : at)
166
+ }
167
+ }
168
+
169
+ /** The timeline handle type (see {@link TimelineImpl} for the members). */
170
+ export type Timeline = TimelineImpl
171
+
172
+ /** Create a timeline — `Timeline()` / `Timeline({ clock: 'game', loop: true })`. */
173
+ export function Timeline(options?: TimelineOptions): Timeline {
174
+ return new TimelineImpl(options)
175
+ }
@@ -1,6 +1,8 @@
1
1
  // Feature gate for the game audio system (docs/audio-plan.md). `_creatorAudio` is OPTIONAL as a
2
2
  // block: a host without it (or whose engine failed to open a device) makes every Sound silent and
3
- // every Voice inert — a project stays runnable, nothing throws.
3
+ // every Voice inert — a project stays runnable, nothing throws. hasSupport() opens no device: the
4
+ // native engine is created by the first call that needs it (a load, a bus write), so reading this
5
+ // at load costs nothing; a host whose device then fails to open hands Sound.load the silent clip.
4
6
 
5
7
  export const audioSupported: boolean =
6
8
  typeof _creatorAudio !== "undefined" && !!_creatorAudio && typeof _creatorAudio.hasSupport === "function" && _creatorAudio.hasSupport()
@@ -13,27 +13,32 @@ declare global {
13
13
  // CREATOR_PKG_AUDIO (desktop first; Android / Apple pending — parity `audio-game`). The headless
14
14
  // renderer provides a RECORDER (plays are logged, nothing is mixed). Every parameter block is a
15
15
  // fixed-order Float32Array (creator-audio.h CAUD_SRC_* / the effect param orders).
16
+ // The engine (and the audio DEVICE) is created LAZILY, by the first call that needs it — the
17
+ // `@guard audioEnsureEngine` on every method that touches engine state (creator-pkg.h
18
+ // "creator-audio host integration"): an app that never makes a sound never opens the device.
19
+ // hasSupport() therefore opens nothing: it is "the bridge is here and no init has failed".
16
20
  /** @engine audio */
17
21
  var _creatorAudio: {
18
- /** @guard audioEnsureEngine @c caudHasSupport */
22
+ /** True on a host with the bridge until an engine init has FAILED (no device at all). Opens
23
+ * nothing: the engine is created by the first call that needs it. @custom */
19
24
  hasSupport(): boolean
20
25
  /** Decode the bytes behind a fetch system id (WAV / MP3 / OGG Vorbis) on the engine's
21
26
  * loader thread; mono unless `stereo`. onDone(clipId, durationSeconds, channels). @custom */
22
27
  loadClip(systemId: bytes, stereo: boolean, onDone: (clipId: number, duration: number, channels: number) => void, onReject: (message: string) => void): void
23
28
  /** @c caudClipRelease */
24
29
  releaseClip(clipId: u32): void
25
- /** Bus id by name (-1 unknown); 0 master, 1 sfx, 2 music, 3 ui, 4 voice. @c caudBusId */
30
+ /** Bus id by name (-1 unknown); 0 master, 1 sfx, 2 music, 3 ui, 4 voice. @guard audioEnsureEngine @c caudBusId */
26
31
  busId(name: string): i32
27
- /** Creates (or finds) an app-defined bus under master. -1 when the 16 slots are full. @c caudBusCreate */
32
+ /** Creates (or finds) an app-defined bus under master. -1 when the 16 slots are full. @guard audioEnsureEngine @c caudBusCreate */
28
33
  createBus(name: string): i32
29
- /** @c caudBusSetVolume */
34
+ /** @guard audioEnsureEngine @c caudBusSetVolume */
30
35
  setBusVolume(bus: i32, volume: f32): void
31
- /** @c caudBusSetMuted */
36
+ /** @guard audioEnsureEngine @c caudBusSetMuted */
32
37
  setBusMuted(bus: i32, muted: boolean): void
33
38
  /** kind 1 reverb [roomSize, damping, width, mix, preDelay] · 2 echo [delay, decay, mix] ·
34
- * 3 lowpass [cutoffHz]; an empty array turns the effect off. Changes are smoothed. @c caudBusSetEffect */
39
+ * 3 lowpass [cutoffHz]; an empty array turns the effect off. Changes are smoothed. @guard audioEnsureEngine @c caudBusSetEffect */
35
40
  setBusEffect(bus: i32, kind: i32, params: Float32Array): void
36
- /** @c caudBusStopAll */
41
+ /** @guard audioEnsureEngine @c caudBusStopAll */
37
42
  stopBus(bus: i32, fade: f32): void
38
43
  /** A 3D emitter. `attachSource` binds it to an entity the engine follows every frame;
39
44
  * `setSourcePosition` places an unbound one (playAt). @guard audioEnsureEngine @c caudSourceCreate */
@@ -41,11 +46,11 @@ declare global {
41
46
  /** @c gl:audioAttachSource */
42
47
  attachSource(sourceId: u32, entityId: u32): void
43
48
  /** [minDistance, maxDistance, rolloff (0 none 1 inverse 2 linear 3 exp), coneInnerDeg,
44
- * coneOuterDeg, coneOuterGain, doppler, spread, occlusion (0/1), bus]. @c caudSourceSetParams */
49
+ * coneOuterDeg, coneOuterGain, doppler, spread, occlusion (0/1), bus]. @guard audioEnsureEngine @c caudSourceSetParams */
45
50
  setSourceParams(sourceId: u32, params: Float32Array): void
46
- /** @c caudSourceSetPose @pack x 3 @rest nullptr */
51
+ /** @guard audioEnsureEngine @c caudSourceSetPose @pack x 3 @rest nullptr */
47
52
  setSourcePosition(sourceId: u32, x: f32, y: f32, z: f32): void
48
- /** @c caudSourceStopAll */
53
+ /** @guard audioEnsureEngine @c caudSourceStopAll */
49
54
  stopSource(sourceId: u32, fade: f32): void
50
55
  /** @c caudSourceVoices */
51
56
  sourceVoices(sourceId: u32): i32
@@ -53,22 +58,22 @@ declare global {
53
58
  destroySource(sourceId: u32): void
54
59
  /** The listener entity; 0 = the active scene camera (the default). @c gl:audioSetListener */
55
60
  setListener(entityId: u32): void
56
- /** @c caudListenerSetDoppler */
61
+ /** @guard audioEnsureEngine @c caudListenerSetDoppler */
57
62
  setListenerOptions(dopplerFactor: f32): void
58
63
  /** HRTF binaural rendering for the `maxVoices` nearest spatial voices (headphones); the rest keep panning. @guard audioEnsureEngine @c caudSetHrtf */
59
64
  setHrtf(enabled: boolean, maxVoices: i32): void
60
65
  /** Whether the engine's time scale (Time.scale) also pitches the sfx bus. Default false (pause = mute only). @guard audioEnsureEngine @c caudSetTimeScalePitch */
61
66
  setTimeScalePitch(enabled: boolean): void
62
67
  /** → a voice token (0 = nothing played: the pool refused, the clip is not ready). bus -1 = the
63
- * source's bus (sfx for 2D). pan is 2D only. @c caudPlay @pack bus CaudPlay */
68
+ * source's bus (sfx for 2D). pan is 2D only. @guard audioEnsureEngine @c caudPlay @pack bus CaudPlay */
64
69
  play(clipId: u32, sourceId: u32, bus: i32, volume: f32, pitch: f32, loop: boolean, priority: i32, fadeIn: f32, startAt: f32, pan: f32): u32
65
- /** @c caudVoiceSetVolume */
70
+ /** @guard audioEnsureEngine @c caudVoiceSetVolume */
66
71
  setVoiceVolume(token: u32, volume: f32): void
67
- /** @c caudVoiceSetPitch */
72
+ /** @guard audioEnsureEngine @c caudVoiceSetPitch */
68
73
  setVoicePitch(token: u32, pitch: f32): void
69
- /** @c caudVoiceSetPan */
74
+ /** @guard audioEnsureEngine @c caudVoiceSetPan */
70
75
  setVoicePan(token: u32, pan: f32): void
71
- /** @c caudVoiceStop */
76
+ /** @guard audioEnsureEngine @c caudVoiceStop */
72
77
  stopVoice(token: u32, fade: f32): void
73
78
  /** @c caudVoicePlaying */
74
79
  voicePlaying(token: u32): boolean
@@ -77,7 +82,7 @@ declare global {
77
82
  /** THE ended listener (single slot per JS world): called once per tick with the tokens of the
78
83
  * voices that ended since the previous tick — natural end, stop, steal. @custom */
79
84
  setOnEnded(callback: ((tokens: Float32Array) => void) | null): void
80
- /** @c caudStopAll */
85
+ /** @guard audioEnsureEngine @c caudStopAll */
81
86
  stopAll(fade: f32): void
82
87
  /** A reverb volume: shape 0 box (dims = full size) / 1 sphere (dims[0] = diameter), in the
83
88
  * entity's local units; `blend` metres of crossfade inside the border; reverb params as for
@@ -85,7 +90,7 @@ declare global {
85
90
  createZone(): u32
86
91
  /** @c gl:audioAttachZone */
87
92
  attachZone(zoneId: u32, entityId: u32): void
88
- /** @c caudZoneSet */
93
+ /** @guard audioEnsureEngine @c caudZoneSet */
89
94
  setZone(zoneId: u32, shape: i32, dims: Float32Array, blend: f32, bus: i32, reverb: Float32Array): void
90
95
  /** @c gl:audioDetachZone, caudZoneDestroy */
91
96
  destroyZone(zoneId: u32): void
@@ -38,6 +38,25 @@ declare global {
38
38
  /** Whether the overlay is on. */
39
39
  statsOverlay?(): boolean
40
40
 
41
+ // ---- The frame rate (device.setFrameRate / setVsync / displayRefreshRate) ---------------------
42
+ /** Cap the host's frame loop at `fps` frames a second (device.setFrameRate): the tick, and the
43
+ * 3D frame with it, runs at most that often. `0` = no cap: the display's own rate, or, with
44
+ * vsync off, as fast as the host renders. A request, not a state — a phone asks its panel for
45
+ * the rate and may be refused; the screen's own maximum still wins. Host state, kept through
46
+ * `_creatorApp.run()`. Optional: a host whose loop has no pacing of its own (web, headless)
47
+ * leaves it out. */
48
+ setFrameRate?(fps: f64): void
49
+ /** Present without waiting for the display's refresh (device.setVsync): off = no vsync wait,
50
+ * tearing possible, and with no cap set the loop renders as fast as it can. Optional: desktop
51
+ * only — a mobile compositor never presents outside the refresh, so the slot is absent there
52
+ * and the SDK answers `false`. */
53
+ setVsync?(enabled: boolean): void
54
+ /** The display's refresh rate in Hz (device.displayRefreshRate): the fastest the screen offers
55
+ * the app — 120 on a 120 Hz phone whatever mode the cap has put it in (an iPhone's 60 unless
56
+ * the app's Info.plist lifts the floor), the monitor's current mode on desktop. Optional: 0
57
+ * where the host cannot tell (web, Linux for now). */
58
+ displayRefreshRate?(): f64
59
+
41
60
  // ---- device.motion (orientation sensor; pull-per-frame model) --------------------------------
42
61
  /** Does this device have the motion sensors (gyro)? Surfaced as device.motion.available. */
43
62
  motionAvailable?(): boolean
@@ -286,6 +286,11 @@ declare global {
286
286
  setInstanceTransforms(entityId: u32, matrices: Float32Array, first: u32): void
287
287
  setInstancedMeshMaterial(entityId: u32, materialId: u32): void
288
288
  setInstancedMeshShadows(entityId: u32, cast: boolean, receive: boolean): void
289
+ /** GLB scatter (gl/Scatter.ts ↔ creator-gl/src/scatter.cpp): the loaded model `sourceId` drawn at `placements.length / 5`
290
+ * placements (x y z, yaw in degrees, uniform scale) under `entityId`, hardware-instanced in cells of `cell` metres
291
+ * with the source's own buffers and material instances. Returns the renderables made (0 = nothing scattered).
292
+ * Absent on a host without it: Scatter then clones the model per placement. @count count=placements/5 */
293
+ createGlbScatter?(entityId: u32, sourceId: u32, placements: Float32Array, cell: f32, castShadows: boolean): u32
289
294
  setCulling(entityId: u32, culling: boolean): void
290
295
  /** Filament RenderableManager::setPriority — the coarse draw order, 0..7 (Mesh.renderPriority).
291
296
  * Optional: an older host leaves everything at the default 4. */
@@ -780,6 +785,12 @@ declare global {
780
785
  /** Distance fade for every instance of the entity's ASSET (present and future): the cards thin out between `start`
781
786
  * and `end` metres from the camera and past `end` the LOD pass takes the instance out of the scene. end 0 = none. */
782
787
  foliageSetFade?(entityId: u32, start: f32, end: f32): void
788
+ /** THE PLANTS' CASTER DISTANCE: a foliage instance casts the sun's shadow only within `distance` metres of the camera
789
+ * (the LOD pass flips its renderables' castShadows); past it a baked ground keeps the shadow. 0 = every plant casts. */
790
+ foliageSetCast?(distance: f32): void
791
+ /** THE GROUND'S COLOUR UNDER THE PLANTS: the terrain's control map (RGBA = its four layers' weights) and each layer's
792
+ * mean colour (12 floats, linear rgb x 4) — a grass blade's base takes the colour of the soil it stands in. Level-wide. */
793
+ foliageSetGroundTint?(terrainId: u32, controlTextureId: u32, colors: Float32Array): void
783
794
  /** @ifdef CREATOR_GL_PHYSICS */
784
795
  physicsConfigure(gx: f32, gy: f32, gz: f32, maxBodies: u32): void
785
796
  /** @c setPhysicsInterpolation @ifdef CREATOR_GL_PHYSICS */
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import type { AppConfig } from "../server/auth/appConfig"
16
+ import { CODE_EXT } from "./assetMacro"
16
17
  import { bundleProjectWithMap, SERVER_INJECT, type MemoryFiles } from "./bundler"
17
18
  import type { CompileEntry } from "./compileProject"
18
19
  import { deriveEndpointParams } from "./serverTypes"
@@ -271,12 +272,18 @@ export const compileServerBundle = async (opts: CompileServerOptions): Promise<{
271
272
  // with a Figma-exported design next to app.server.ts). A stub's value is the resource's PATH: a
272
273
  // row of shared data that names a picture (`photo: "images/a.jpg"`) reaches the server as the
273
274
  // path of that picture, which a seed stores into a file field (files/db.ts). The board's `design/`
274
- // files are never server code and stay out entirely.
275
+ // files are never server code and stay out entirely. A text file that is not code follows the
276
+ // app bundle's rules (compileProject.ts entriesToFiles): a `.json` is its parsed data — a shared
277
+ // module of a price list the person gave does `import raw from "./price.json"`, and the seed
278
+ // that imports the module ran into "raw is not defined" while the text went in as code — and any
279
+ // other text (.csv, .txt, .md) is its string.
275
280
  const files: MemoryFiles = {}
276
281
  for (const e of opts.entries) {
277
282
  const path = normalizePath(e.path)
278
283
  if (/^\/design\//.test(path)) continue
279
284
  if (e.type !== "text" || path.toLowerCase().endsWith(".svg")) { files[path] = `export default ${JSON.stringify(path)}`; continue }
285
+ if (path.toLowerCase().endsWith(".json")) { files[path] = `export default ${e.text?.trim() || "null"}`; continue }
286
+ if (!CODE_EXT.test(path)) { files[path] = `export default ${JSON.stringify(e.text ?? "")}`; continue }
280
287
  files[path] = e.text ?? ""
281
288
  }
282
289
  let params = opts.params