@jgengine/shell 0.24.0 → 0.26.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/Shell3dPresentation.js +29 -11
  3. package/dist/ShellChrome.js +1 -1
  4. package/dist/ShellNotices.d.ts +4 -4
  5. package/dist/ShellNotices.js +9 -6
  6. package/dist/devtools/FrameTelemetry.d.ts +12 -0
  7. package/dist/devtools/FrameTelemetry.js +68 -0
  8. package/dist/devtools/agentBridge.js +2 -0
  9. package/dist/devtools/devtoolsReports.d.ts +2 -0
  10. package/dist/devtools/devtoolsReports.js +3 -0
  11. package/dist/devtools/frameSampler.d.ts +127 -0
  12. package/dist/devtools/frameSampler.js +345 -0
  13. package/dist/environment/AuthoredDaylight.d.ts +28 -0
  14. package/dist/environment/AuthoredDaylight.js +42 -0
  15. package/dist/environment/Daylight.d.ts +55 -7
  16. package/dist/environment/Daylight.js +126 -29
  17. package/dist/environment/daylightCycle.d.ts +26 -4
  18. package/dist/environment/daylightCycle.js +36 -5
  19. package/dist/postfx/PostProcessing.d.ts +7 -1
  20. package/dist/postfx/PostProcessing.js +28 -2
  21. package/dist/postfx/autoExposure.d.ts +49 -0
  22. package/dist/postfx/autoExposure.js +189 -0
  23. package/dist/postfx/gradeShader.js +43 -4
  24. package/dist/presentationRecovery3d.js +2 -1
  25. package/dist/render/CascadedShadows.d.ts +3 -1
  26. package/dist/render/CascadedShadows.js +9 -1
  27. package/dist/render/EnvironmentLighting.d.ts +10 -1
  28. package/dist/render/EnvironmentLighting.js +11 -3
  29. package/dist/render/IrradianceVolumeLighting.d.ts +18 -0
  30. package/dist/render/IrradianceVolumeLighting.js +35 -0
  31. package/dist/render/SceneLighting.d.ts +14 -1
  32. package/dist/render/SceneLighting.js +29 -8
  33. package/dist/render/authoredGrade.d.ts +11 -0
  34. package/dist/render/authoredGrade.js +26 -0
  35. package/dist/render/presentationLook.d.ts +15 -0
  36. package/dist/render/presentationLook.js +18 -0
  37. package/dist/render/stableTextureSet.d.ts +9 -0
  38. package/dist/render/stableTextureSet.js +24 -0
  39. package/dist/render/triplanarMaterial.d.ts +52 -0
  40. package/dist/render/triplanarMaterial.js +169 -0
  41. package/dist/settings/appliedSettings.js +16 -13
  42. package/package.json +4 -4
  43. package/skills/jgengine/api.md +59 -6
  44. package/skills/jgengine/capabilities.md +4 -0
  45. package/skills/jgengine-ui/SKILL.md +3 -3
  46. package/skills/jgengine-ui/api.md +98 -31
  47. package/skills/jgengine-ui/capabilities.md +15 -1
  48. package/skills/jgengine-ui/reference-primitives.md +5 -3
  49. package/skills/jgengine-world/SKILL.md +3 -1
  50. package/skills/jgengine-world/api.md +107 -3
  51. package/skills/jgengine-world/capabilities.md +22 -2
  52. package/skills/jgengine-world/recipes/npc-senses.md +8 -2
@@ -0,0 +1,345 @@
1
+ import { devtools } from "@jgengine/core/devtools/devtools";
2
+ import { roundReport, toReportLogs } from "@jgengine/core/devtools/report";
3
+ import { REPORT_ENDPOINT_PATH } from "@jgengine/core/devtools/reportEndpoint";
4
+ import { readUrlParam } from "@jgengine/core/devtools/urlFlags";
5
+ /**
6
+ * In-page frame telemetry: the game reports on itself instead of needing a second
7
+ * process to attach a profiler. Frame timings come from the devtools frame ring,
8
+ * renderer counts from the live renderer, GPU-object creation from wrapped GL calls,
9
+ * and per-system numbers from `devtools.probes` — all read on demand, so nothing
10
+ * here runs on the hot path.
11
+ */
12
+ /** Query flag that turns the sampler off for an A/B frame-time comparison. */
13
+ export const TELEMETRY_FLAG = "telemetry";
14
+ /** How long after the sampler starts the GL counters get their post-warmup baseline. */
15
+ export const TELEMETRY_WARMUP_MS = 5_000;
16
+ /** Cadence of the push to the dev server, in milliseconds. */
17
+ export const REPORT_PUSH_MS = 1_000;
18
+ const WATCHED_GL_CALLS = [
19
+ ["linkProgram", "programsLinked"],
20
+ ["createFramebuffer", "framebuffers"],
21
+ ["createBuffer", "buffers"],
22
+ ["createTexture", "textures"],
23
+ ["createVertexArray", "vertexArrays"],
24
+ ];
25
+ function emptyGlTotals() {
26
+ return { programsLinked: 0, framebuffers: 0, buffers: 0, textures: 0, vertexArrays: 0 };
27
+ }
28
+ /**
29
+ * Wrap the live context's creation calls so a leak shows up as a non-zero counter.
30
+ * Only object creation is wrapped — never a draw call — so a steady-state frame
31
+ * pays nothing. Returns whether any call was actually wrapped, since a zero is only
32
+ * evidence when it was.
33
+ * @internal
34
+ */
35
+ export function attachGlCounters(context) {
36
+ const totals = emptyGlTotals();
37
+ const target = context;
38
+ let attached = false;
39
+ for (const [method, key] of WATCHED_GL_CALLS) {
40
+ const original = target[method];
41
+ if (typeof original !== "function")
42
+ continue;
43
+ attached = true;
44
+ target[method] = (...args) => {
45
+ totals[key] += 1;
46
+ return original.apply(context, args);
47
+ };
48
+ }
49
+ return { totals, attached };
50
+ }
51
+ /**
52
+ * Count live three.js nodes from the scene the game is drawing; `null` when it cannot be walked.
53
+ * @internal
54
+ */
55
+ export function countSceneNodes(scene) {
56
+ const candidate = scene;
57
+ if (candidate === null || candidate === undefined || typeof candidate.traverse !== "function") {
58
+ return { nodes: null, scenes: 0 };
59
+ }
60
+ try {
61
+ let nodes = 0;
62
+ candidate.traverse(() => {
63
+ nodes += 1;
64
+ });
65
+ return { nodes, scenes: 1 };
66
+ }
67
+ catch {
68
+ return { nodes: null, scenes: 0 };
69
+ }
70
+ }
71
+ function glReport(inputs) {
72
+ const since = inputs.since;
73
+ return {
74
+ programsLinked: since.programsLinked,
75
+ framebuffers: since.framebuffers,
76
+ buffers: since.buffers,
77
+ textures: since.textures,
78
+ vertexArrays: since.vertexArrays,
79
+ allocations: since.programsLinked + since.framebuffers + since.buffers + since.textures + since.vertexArrays,
80
+ residentPrograms: inputs.residentPrograms,
81
+ total: { ...inputs.totals },
82
+ attached: inputs.attached,
83
+ };
84
+ }
85
+ /**
86
+ * Assemble the shared report shape from live sources. Pure: every number arrives as
87
+ * an argument, so a test can assert the shape without a renderer or a browser.
88
+ * @internal
89
+ */
90
+ export function buildFrameReport(inputs) {
91
+ const frame = inputs.frame;
92
+ const scaled = frame === null
93
+ ? { fps: 0, avgMs: 0, p95Ms: 0, maxMs: 0, samples: 0, longFrames: 0 }
94
+ : {
95
+ fps: roundReport(frame.fps, 1),
96
+ avgMs: roundReport(frame.avgFrameMs),
97
+ p95Ms: roundReport(frame.p95FrameMs),
98
+ maxMs: roundReport(frame.maxFrameMs),
99
+ samples: frame.samples,
100
+ longFrames: frame.longFrames,
101
+ };
102
+ const seconds = scaled.fps > 0 ? roundReport(scaled.samples / scaled.fps, 1) : 0;
103
+ return {
104
+ game: inputs.game,
105
+ gpu: inputs.gpu,
106
+ quality: inputs.quality,
107
+ viewport: inputs.viewport,
108
+ seconds,
109
+ at: inputs.at,
110
+ frame: scaled,
111
+ render: {
112
+ draws: inputs.render?.drawCalls ?? null,
113
+ triangles: inputs.render?.triangles ?? null,
114
+ geometries: inputs.render?.geometries ?? null,
115
+ textures: inputs.render?.textures ?? null,
116
+ nodes: inputs.scene.nodes,
117
+ nodesSource: inputs.scene.nodes === null ? "unavailable" : "scene-graph",
118
+ scenes: inputs.scene.scenes,
119
+ },
120
+ gl: glReport(inputs.gl),
121
+ sim: frame === null ? null : { avgMs: roundReport(frame.avgSimMs), outsideMs: roundReport(frame.avgOutsideMs) },
122
+ profile: null,
123
+ systems: inputs.systems,
124
+ logs: inputs.logs,
125
+ telemetry: {
126
+ source: "in-page",
127
+ warmedUp: inputs.warmedUp,
128
+ ageMs: Math.round(inputs.ageMs),
129
+ buildMs: roundReport(inputs.buildMs),
130
+ heapBytes: inputs.heapBytes,
131
+ },
132
+ };
133
+ }
134
+ function browserNow() {
135
+ const perf = globalThis.performance;
136
+ return perf !== undefined ? perf.now() : Date.now();
137
+ }
138
+ /**
139
+ * `Harbor Heat` -> `harbor-heat`, unless the dev runner's `?game=` says otherwise. The
140
+ * report is keyed by this, so it matches `bun run perf` and the budget file.
141
+ * @internal
142
+ */
143
+ export function reportGameId(displayName) {
144
+ const fromRunner = readUrlParam("game");
145
+ if (fromRunner !== null && fromRunner.length > 0)
146
+ return fromRunner;
147
+ return displayName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
148
+ }
149
+ /**
150
+ * Ask the dev server whether its report route exists. This is the sampler's only
151
+ * production guard and it is behaviour, not a build flag: the route is mounted by the
152
+ * dev Vite configs only, so a shipped game answers this one request and installs
153
+ * nothing — no timers, no GL wrapping, no `window.__JG`. The website dev server serves
154
+ * the /play runner as a production build, which is why a build-time `PROD` check would
155
+ * wrongly disable the report there.
156
+ * @internal
157
+ */
158
+ export async function probeReportRoute(endpoint = REPORT_ENDPOINT_PATH, game = "", fetchImpl = typeof fetch === "function" ? fetch : null, timeoutMs = 1_500) {
159
+ if (fetchImpl === null)
160
+ return false;
161
+ try {
162
+ const response = await fetchImpl(`${endpoint}?game=${encodeURIComponent(game)}`, {
163
+ signal: AbortSignal.timeout(timeoutMs),
164
+ cache: "no-store",
165
+ });
166
+ const type = response.headers.get("content-type") ?? "";
167
+ if (!type.includes("application/json"))
168
+ return false;
169
+ const body = (await response.json());
170
+ return typeof body?.error === "string" || body?.frame !== undefined;
171
+ }
172
+ catch {
173
+ return false;
174
+ }
175
+ }
176
+ function heapUsedBytes() {
177
+ const memory = globalThis.performance?.memory;
178
+ return typeof memory?.usedJSHeapSize === "number" ? memory.usedJSHeapSize : null;
179
+ }
180
+ function subtract(totals, baseline) {
181
+ const since = emptyGlTotals();
182
+ for (const key of Object.keys(since)) {
183
+ since[key] = Math.max(0, totals[key] - (baseline?.[key] ?? 0));
184
+ }
185
+ return since;
186
+ }
187
+ let installed = null;
188
+ /**
189
+ * Reset the sampler's warmup baseline, so a fresh measurement starts from zero.
190
+ * @internal
191
+ */
192
+ export function resetFrameTelemetry() {
193
+ installed?.resetWindow();
194
+ }
195
+ /**
196
+ * Install the sampler for the page: publishes `window.__JG.report()` and pushes the
197
+ * same record to the dev server's report route every second. Returns `null` when
198
+ * `?telemetry=off` asks for the comparison run; callers gate installation on
199
+ * {@link probeReportRoute}, so a shipped game never starts the sampler.
200
+ * @internal
201
+ */
202
+ export function installFrameTelemetry(sources) {
203
+ if ((readUrlParam(TELEMETRY_FLAG) ?? "").toLowerCase() === "off")
204
+ return null;
205
+ if (typeof window === "undefined")
206
+ return null;
207
+ const startedAt = browserNow();
208
+ let baseline = null;
209
+ let disposed = false;
210
+ let pushFailures = 0;
211
+ let scheduled = null;
212
+ const gameId = reportGameId(sources.game);
213
+ const endpoint = sources.endpoint ?? REPORT_ENDPOINT_PATH;
214
+ const shouldPush = sources.push !== false && typeof fetch === "function";
215
+ const markWarmup = () => {
216
+ const ageMs = browserNow() - startedAt;
217
+ if (baseline === null && ageMs >= TELEMETRY_WARMUP_MS)
218
+ baseline = { ...sources.gl().totals };
219
+ return baseline !== null;
220
+ };
221
+ const buildReport = () => {
222
+ const started = browserNow();
223
+ const warmedUp = markWarmup();
224
+ const gl = sources.gl();
225
+ const report = buildFrameReport({
226
+ game: gameId,
227
+ gpu: sources.gpu(),
228
+ quality: sources.quality(),
229
+ viewport: sources.viewport(),
230
+ at: Date.now(),
231
+ frame: devtools.frame.stats(),
232
+ render: sources.render(),
233
+ scene: countSceneNodes(sources.scene()),
234
+ gl: {
235
+ totals: gl.totals,
236
+ since: subtract(gl.totals, baseline),
237
+ residentPrograms: sources.residentPrograms(),
238
+ attached: gl.attached,
239
+ },
240
+ systems: devtools.probes.systems(),
241
+ logs: toReportLogs(devtools.logs.snapshot()),
242
+ heapBytes: sources.heapBytes?.() ?? heapUsedBytes(),
243
+ ageMs: browserNow() - startedAt,
244
+ warmedUp,
245
+ buildMs: browserNow() - started,
246
+ });
247
+ return report;
248
+ };
249
+ const telemetry = {
250
+ report: buildReport,
251
+ resetWindow() {
252
+ baseline = null;
253
+ devtools.frame.reset();
254
+ },
255
+ dispose() {
256
+ if (disposed)
257
+ return;
258
+ disposed = true;
259
+ clearInterval(timer);
260
+ if (scheduled !== null)
261
+ cancelIdle(scheduled);
262
+ window.removeEventListener("error", onError);
263
+ window.removeEventListener("unhandledrejection", onRejection);
264
+ document.removeEventListener("webglcontextlost", onContextLost, true);
265
+ const host = window;
266
+ if (host.__JG?.report === buildReport)
267
+ delete host.__JG;
268
+ if (installed === telemetry)
269
+ installed = null;
270
+ },
271
+ };
272
+ const pushOnce = async () => {
273
+ if (disposed)
274
+ return;
275
+ try {
276
+ const response = await fetch(endpoint, {
277
+ method: "POST",
278
+ headers: { "content-type": "application/json" },
279
+ body: JSON.stringify({ gameId, report: buildReport() }),
280
+ });
281
+ if (!response.ok)
282
+ throw new Error(`report route answered ${response.status}`);
283
+ pushFailures = 0;
284
+ }
285
+ catch {
286
+ pushFailures += 1;
287
+ if (pushFailures === 3) {
288
+ // A missing route (production build, or a host without the dev plugin) is not worth retrying forever.
289
+ clearInterval(timer);
290
+ }
291
+ }
292
+ };
293
+ // Reports ride an idle callback so a build and its POST never land in a busy frame.
294
+ const run = () => {
295
+ scheduled = null;
296
+ void pushOnce();
297
+ };
298
+ const schedulePush = () => {
299
+ if (scheduled !== null || disposed)
300
+ return;
301
+ scheduled = scheduleIdle(run);
302
+ };
303
+ const timer = setInterval(schedulePush, REPORT_PUSH_MS);
304
+ const logError = (system, message) => {
305
+ devtools.logs.write("error", system, message);
306
+ // An error is the one line that must not wait for the next tick: a crash can tear
307
+ // the presentation down (or trigger a reload) before an idle callback ever runs.
308
+ if (shouldPush)
309
+ void pushOnce();
310
+ };
311
+ const onError = (event) => {
312
+ logError("runtime", `${event.message} (${event.filename ?? "?"}:${event.lineno ?? 0})`);
313
+ };
314
+ const onRejection = (event) => {
315
+ const reason = event.reason;
316
+ const message = reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason);
317
+ logError("runtime", `unhandled rejection: ${message}`);
318
+ };
319
+ const onContextLost = (event) => {
320
+ const target = event.target;
321
+ const where = target?.id !== undefined && target.id.length > 0 ? `#${target.id}` : "canvas";
322
+ logError("render", `webgl context lost on ${where}`);
323
+ };
324
+ window.addEventListener("error", onError);
325
+ window.addEventListener("unhandledrejection", onRejection);
326
+ document.addEventListener("webglcontextlost", onContextLost, true);
327
+ window.__JG = { report: buildReport };
328
+ installed = telemetry;
329
+ if (!shouldPush)
330
+ clearInterval(timer);
331
+ return telemetry;
332
+ }
333
+ function scheduleIdle(run) {
334
+ const idle = window;
335
+ if (typeof idle.requestIdleCallback === "function")
336
+ return idle.requestIdleCallback(run, { timeout: REPORT_PUSH_MS });
337
+ return window.setTimeout(run, 0);
338
+ }
339
+ function cancelIdle(handle) {
340
+ const idle = window;
341
+ if (typeof idle.cancelIdleCallback === "function")
342
+ idle.cancelIdleCallback(handle);
343
+ else
344
+ window.clearTimeout(handle);
345
+ }
@@ -0,0 +1,28 @@
1
+ import type { SkyEnvironmentDescriptor } from "@jgengine/core/world/features";
2
+ import type { DaylightModulation } from "../render/SceneLighting.js";
3
+ /** Props for {@link AuthoredDaylight}: the authored sky whose values are the noon keyframe, the world clock, and the holder configured lights read. */
4
+ export interface AuthoredDaylightProps {
5
+ /** The authored sky; its `sunIntensity`/`ambientIntensity` are the noon peak the cycle scales from. */
6
+ sky: SkyEnvironmentDescriptor;
7
+ /** The world's `SimClock` (or a stub exposing `calendar().dayFraction`). */
8
+ clock: {
9
+ calendar(): {
10
+ dayFraction: number;
11
+ };
12
+ };
13
+ /** Mutable holder the shell's configured lights read every frame. */
14
+ modulation: DaylightModulation;
15
+ }
16
+ /**
17
+ * Drives authored `PlayableGame.lighting` from the day cycle: it scales the first directional (the
18
+ * sun) and the ambient fill between the authored noon peak and night, retints the sun, and mounts
19
+ * the moon. Without it a time-of-day sky only repaints the dome, so the ground keeps its midday key
20
+ * light after dark — a black sky over a sunlit city.
21
+ *
22
+ * The authored values stay the noon keyframe, mirroring how `horizonColor`/`zenithColor` already
23
+ * work, so art direction still owns how bright noon is.
24
+ *
25
+ * @capability day-night-sky let an authored sun set and a moon rise instead of a fixed midday key light
26
+ * @internal
27
+ */
28
+ export declare function AuthoredDaylight({ sky, clock, modulation }: AuthoredDaylightProps): import("react").JSX.Element | null;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useMemo, useRef } from "react";
4
+ import * as THREE from "three";
5
+ import { MoonLight, useMoonAboveHorizon } from "./Daylight.js";
6
+ import { daylightStateAt, DEFAULT_DAY_AMBIENT_INTENSITY, DEFAULT_DAY_SUN_INTENSITY } from "./daylightCycle.js";
7
+ /**
8
+ * Drives authored `PlayableGame.lighting` from the day cycle: it scales the first directional (the
9
+ * sun) and the ambient fill between the authored noon peak and night, retints the sun, and mounts
10
+ * the moon. Without it a time-of-day sky only repaints the dome, so the ground keeps its midday key
11
+ * light after dark — a black sky over a sunlit city.
12
+ *
13
+ * The authored values stay the noon keyframe, mirroring how `horizonColor`/`zenithColor` already
14
+ * work, so art direction still owns how bright noon is.
15
+ *
16
+ * @capability day-night-sky let an authored sun set and a moon rise instead of a fixed midday key light
17
+ * @internal
18
+ */
19
+ export function AuthoredDaylight({ sky, clock, modulation }) {
20
+ const initial = useMemo(() => daylightStateAt(clock.calendar().dayFraction, sky), [sky, clock]);
21
+ const noonSun = sky.sunIntensity ?? DEFAULT_DAY_SUN_INTENSITY;
22
+ const noonAmbient = sky.ambientIntensity ?? DEFAULT_DAY_AMBIENT_INTENSITY;
23
+ const moonRef = useRef(null);
24
+ const moonDirectionRef = useRef(initial.moonPosition);
25
+ const [moonUp, syncMoon] = useMoonAboveHorizon(initial.moonIntensity > 0);
26
+ useFrame(() => {
27
+ const state = daylightStateAt(clock.calendar().dayFraction, sky);
28
+ modulation.sunFactor = state.sunIntensity / noonSun;
29
+ modulation.sunColor = state.sunColor;
30
+ modulation.ambientFactor = state.ambientIntensity / noonAmbient;
31
+ moonDirectionRef.current = state.moonPosition;
32
+ const moon = moonRef.current;
33
+ if (moon !== null) {
34
+ moon.intensity = state.moonIntensity;
35
+ moon.color.set(state.moonColor);
36
+ }
37
+ syncMoon(state.moonIntensity);
38
+ });
39
+ if (!moonUp)
40
+ return null;
41
+ return (_jsx(MoonLight, { direction: initial.moonPosition, directionRef: moonDirectionRef, lightRef: moonRef, intensity: initial.moonIntensity, color: initial.moonColor }));
42
+ }
@@ -14,6 +14,14 @@ export interface SkyDomeProps {
14
14
  sunColor?: string;
15
15
  /** Sun disc/glow intensity multiplier. Default 1. */
16
16
  sunIntensity?: number;
17
+ /** Direction toward the moon; a cool disc + halo is drawn there at night. Omit to skip the moon. */
18
+ moonDirection?: readonly [number, number, number];
19
+ /** Moon disc/halo colour. Default cool moonlight. */
20
+ moonColor?: string;
21
+ /** Moon disc/halo intensity multiplier. Default 0 (no moon). */
22
+ moonIntensity?: number;
23
+ /** Star-field opacity: 0 is a day sky, 1 is a clear night. Default 0. */
24
+ starOpacity?: number;
17
25
  /** Horizon haze-band strength: 0 removes the dusty band, ~1 makes it heavy. Default 0.5. */
18
26
  hazeStrength?: number;
19
27
  /** Procedural cloud-band opacity: 0 is a clear sky, 1 is heavy overcast. Default 0.35. */
@@ -23,13 +31,29 @@ export interface SkyDomeProps {
23
31
  /** Exposes the created shader material so a time-of-day driver can mutate its uniforms per frame without recreating it. */
24
32
  materialRef?: MutableRefObject<THREE.ShaderMaterial | null>;
25
33
  }
34
+ /** Aim the dome's moon disc and dial its brightness; called per frame by the time-of-day drivers. @internal */
35
+ export declare function setSkyMoon(material: THREE.ShaderMaterial, direction: readonly [number, number, number], color: THREE.ColorRepresentation, intensity: number, starOpacity: number): void;
26
36
  /**
27
37
  * Shadow-casting sun whose shadow box leads the camera each frame ({@link sunShadowFocus}), so
28
38
  * grounded shadows stay crisp and soft-edged under the player anywhere in a large world. Time-of-day
29
39
  * drivers aim it through `directionRef` and retint it through `lightRef`.
30
40
  * @internal
31
41
  */
32
- export declare function SunLight({ direction, directionRef, lightRef, intensity, color, }: {
42
+ export declare function SunLight({ direction, directionRef, lightRef, intensity, color, castShadow, }: {
43
+ direction: readonly [number, number, number];
44
+ directionRef?: MutableRefObject<readonly [number, number, number]>;
45
+ lightRef?: RefObject<THREE.DirectionalLight | null>;
46
+ intensity: number;
47
+ color: string;
48
+ /** Shadow-casting key light. Off for the moon: its light is too weak to read a shadow and a second shadow pass is not free. */
49
+ castShadow?: boolean;
50
+ }): import("react").JSX.Element;
51
+ /**
52
+ * The moon as a dim, shadowless key light opposite the sun, so a night scene keeps a lit and a
53
+ * shaded side instead of collapsing into flat ambient. Same camera-following aim as the sun.
54
+ * @internal
55
+ */
56
+ export declare function MoonLight({ direction, directionRef, lightRef, intensity, color }: {
33
57
  direction: readonly [number, number, number];
34
58
  directionRef?: MutableRefObject<readonly [number, number, number]>;
35
59
  lightRef?: RefObject<THREE.DirectionalLight | null>;
@@ -58,7 +82,7 @@ export declare function SkyFog({ color, near, far, fogRef }: {
58
82
  /** Sets the display color a {@link SkyFog} converts each frame. @internal */
59
83
  export declare function setSkyFogColor(fog: THREE.Fog, color: THREE.ColorRepresentation): void;
60
84
  /** @internal */
61
- export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, hazeStrength, sunGlowStrength, cloudiness, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
85
+ export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, moonDirection, moonColor, moonIntensity, starOpacity, hazeStrength, sunGlowStrength, cloudiness, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
62
86
  export interface DaylightProps {
63
87
  sky?: SkyDomeProps | false;
64
88
  fog?: {
@@ -71,6 +95,12 @@ export interface DaylightProps {
71
95
  intensity?: number;
72
96
  color?: string;
73
97
  };
98
+ moon?: {
99
+ position?: readonly [number, number, number];
100
+ intensity?: number;
101
+ color?: string;
102
+ stars?: number;
103
+ };
74
104
  ambient?: {
75
105
  skyColor?: string;
76
106
  groundColor?: string;
@@ -82,17 +112,29 @@ export interface DaylightProps {
82
112
  clouds?: VolumetricCloudsConfig;
83
113
  }
84
114
  /** @internal */
85
- export declare function Daylight({ sky, fog, sun, ambient, lights, clouds }?: DaylightProps): import("react").JSX.Element;
115
+ export declare function Daylight({ sky, fog, sun, ambient, moon, lights, clouds }?: DaylightProps): import("react").JSX.Element;
86
116
  export interface SkyDaylightProps {
87
117
  sky: SkyEnvironmentDescriptor;
88
118
  lights?: boolean;
89
- /** Terrain `biomeBands`; when any carries `fog`/`sky`, fog and dome cross-fade per camera z. */
119
+ /** Terrain `biomeBands`; when any carries `fog`/`sky`, fog and cross-fade per camera z. */
90
120
  bands?: readonly BiomeBand[];
121
+ /** Fog range authored on the game's `backdrop.fog`, used when the sky descriptor sets none. */
122
+ fogRange?: {
123
+ near?: number;
124
+ far?: number;
125
+ };
91
126
  }
127
+ /**
128
+ * Mount state for the moon: it re-renders only when the moon crosses the horizon. A zero-intensity
129
+ * directional light still costs a per-fragment loop in every lit material, so a day sky must not
130
+ * carry it.
131
+ * @internal
132
+ */
133
+ export declare function useMoonAboveHorizon(initialUp: boolean): [boolean, (intensity: number) => void];
92
134
  /** Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
93
135
  * @internal
94
136
  */
95
- export declare function SkyDaylight({ sky, lights, bands }: SkyDaylightProps): import("react").JSX.Element;
137
+ export declare function SkyDaylight({ sky, lights, bands, fogRange }: SkyDaylightProps): import("react").JSX.Element;
96
138
  export interface TimeOfDayDaylightProps {
97
139
  sky: SkyEnvironmentDescriptor;
98
140
  /** The world's `SimClock` (or a stub exposing `calendar().dayFraction`). Absent means static rendering. */
@@ -104,11 +146,17 @@ export interface TimeOfDayDaylightProps {
104
146
  lights?: boolean;
105
147
  /** Terrain `biomeBands`; when any carries `fog`/`sky`, fog and dome cross-fade per camera z. */
106
148
  bands?: readonly BiomeBand[];
149
+ /** Fog range authored on the game's `backdrop.fog`, used when the sky descriptor sets none. */
150
+ fogRange?: {
151
+ near?: number;
152
+ far?: number;
153
+ };
107
154
  }
108
155
  /**
109
156
  * Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock`
110
157
  * are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so
111
- * only dome colors and fog track the day fraction.
158
+ * only dome colors and fog track the day fraction; `AuthoredDaylight` scales those authored lights
159
+ * from the same state.
112
160
  * @internal
113
161
  */
114
- export declare function TimeOfDayDaylight({ sky, clock, lights, bands }: TimeOfDayDaylightProps): import("react").JSX.Element;
162
+ export declare function TimeOfDayDaylight({ sky, clock, lights, bands, fogRange }: TimeOfDayDaylightProps): import("react").JSX.Element;