@jgengine/shell 0.7.0 → 0.8.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 +47 -0
- package/README.md +26 -1
- package/dist/GameHost.d.ts +12 -0
- package/dist/GameHost.js +33 -0
- package/dist/GamePlayer.d.ts +11 -0
- package/dist/GamePlayer.js +24 -0
- package/dist/GamePlayerShell.d.ts +33 -1
- package/dist/GamePlayerShell.js +439 -28
- package/dist/behaviour.d.ts +31 -0
- package/dist/behaviour.js +63 -0
- package/dist/camera/GameCameraRig.d.ts +8 -3
- package/dist/camera/GameCameraRig.js +28 -14
- package/dist/camera/GameFirstPersonCamera.js +2 -0
- package/dist/camera/GameInspectionCamera.d.ts +6 -0
- package/dist/camera/GameInspectionCamera.js +24 -0
- package/dist/camera/cameraRigs.d.ts +5 -1
- package/dist/camera/cameraRigs.js +43 -6
- package/dist/camera/index.d.ts +4 -2
- package/dist/camera/index.js +4 -2
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +40 -1
- package/dist/camera/rigMath.js +55 -0
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -0
- package/dist/defineGame.d.ts +16 -0
- package/dist/defineGame.js +51 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
- package/dist/devtools/DevtoolsOverlay.js +334 -0
- package/dist/environment/Daylight.d.ts +50 -0
- package/dist/environment/Daylight.js +100 -0
- package/dist/environment/EnvironmentScene.js +18 -26
- package/dist/environment/GroundPad.d.ts +7 -0
- package/dist/environment/GroundPad.js +12 -0
- package/dist/environment/daylightCycle.d.ts +26 -0
- package/dist/environment/daylightCycle.js +116 -0
- package/dist/environment/groundPadMath.d.ts +13 -0
- package/dist/environment/groundPadMath.js +10 -0
- package/dist/environment/index.d.ts +2 -0
- package/dist/environment/index.js +2 -0
- package/dist/materialOverride.d.ts +8 -0
- package/dist/materialOverride.js +32 -0
- package/dist/multiplayer.d.ts +16 -9
- package/dist/multiplayer.js +62 -12
- package/dist/pointer/PointerProbe.js +14 -1
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +45 -24
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +3 -1
- package/dist/render/modelRender.d.ts +14 -0
- package/dist/render/modelRender.js +79 -0
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/terrainMath.d.ts +1 -0
- package/dist/terrain/terrainMath.js +8 -2
- package/dist/touch/TouchControlsOverlay.d.ts +18 -0
- package/dist/touch/TouchControlsOverlay.js +173 -0
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- package/dist/world/DataObjects.d.ts +44 -0
- package/dist/world/DataObjects.js +75 -0
- package/dist/world/GridWorldScene.d.ts +10 -0
- package/dist/world/GridWorldScene.js +54 -0
- package/dist/world/WorldHud.d.ts +5 -1
- package/dist/world/WorldHud.js +6 -2
- package/dist/world/WorldItems.js +21 -2
- package/llms.txt +2382 -0
- package/package.json +6 -5
- package/dist/demo/builderDemo.d.ts +0 -2
- package/dist/demo/builderDemo.js +0 -189
- package/dist/demo/demoGame.d.ts +0 -2
- package/dist/demo/demoGame.js +0 -208
- package/dist/demo/environmentShowcase.d.ts +0 -2
- package/dist/demo/environmentShowcase.js +0 -15
- package/dist/demo/mapDemo.d.ts +0 -2
- package/dist/demo/mapDemo.js +0 -206
- package/dist/demo/pointerDemo.d.ts +0 -2
- package/dist/demo/pointerDemo.js +0 -151
- package/dist/demo/sensorShowcase.d.ts +0 -2
- package/dist/demo/sensorShowcase.js +0 -131
- package/dist/demo/survivalDemo.d.ts +0 -2
- package/dist/demo/survivalDemo.js +0 -291
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least
|
|
3
|
+
* specific: an explicit `rig` field always wins; then `perspective: "first"`
|
|
4
|
+
* (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence
|
|
5
|
+
* of a rig's own config block selects that rig, checked in the fixed order below
|
|
6
|
+
* (#207.8) so a config carrying more than one block resolves deterministically
|
|
7
|
+
* instead of depending on object key order. Set `rig` explicitly to break a tie.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveRigKind(config) {
|
|
10
|
+
if (config?.rig !== undefined)
|
|
11
|
+
return config.rig;
|
|
12
|
+
if (config?.perspective === "first")
|
|
13
|
+
return "first";
|
|
14
|
+
if (config?.topDown !== undefined)
|
|
15
|
+
return "topDown";
|
|
16
|
+
if (config?.rts !== undefined)
|
|
17
|
+
return "rts";
|
|
18
|
+
if (config?.shoulder !== undefined)
|
|
19
|
+
return "shoulder";
|
|
20
|
+
if (config?.lockOn !== undefined)
|
|
21
|
+
return "lockOn";
|
|
22
|
+
if (config?.chase !== undefined)
|
|
23
|
+
return "chase";
|
|
24
|
+
if (config?.observer !== undefined)
|
|
25
|
+
return "observer";
|
|
26
|
+
if (config?.turntable !== undefined)
|
|
27
|
+
return "turntable";
|
|
28
|
+
if (config?.sideScroll !== undefined)
|
|
29
|
+
return "sideScroll";
|
|
30
|
+
if (config?.inspection !== undefined)
|
|
31
|
+
return "inspection";
|
|
32
|
+
return "orbit";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The turntable rig is a flat facade over the observer's point-orbit mode: map
|
|
36
|
+
* its `target`/`distance`/… onto an observer block so ObserverRig runs unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export function turntableAsObserver(config) {
|
|
39
|
+
const t = config?.turntable;
|
|
40
|
+
if (t === undefined)
|
|
41
|
+
return config ?? {};
|
|
42
|
+
const observer = {
|
|
43
|
+
...(t.target !== undefined ? { bind: { kind: "point", position: t.target } } : {}),
|
|
44
|
+
...(t.distance !== undefined ? { distance: t.distance } : {}),
|
|
45
|
+
...(t.height !== undefined ? { height: t.height } : {}),
|
|
46
|
+
...(t.lookHeight !== undefined ? { lookHeight: t.lookHeight } : {}),
|
|
47
|
+
...(t.orbitSpeed !== undefined ? { orbitSpeed: t.orbitSpeed } : {}),
|
|
48
|
+
...(t.startAngle !== undefined ? { startAngle: t.startAngle } : {}),
|
|
49
|
+
...(t.fov !== undefined ? { fov: t.fov } : {}),
|
|
50
|
+
};
|
|
51
|
+
return { ...config, observer };
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import { type GameDefinitionConfig, type GameLoop } from "@jgengine/core/game/defineGame";
|
|
3
|
+
import type { GameContext, GameContextContent } from "@jgengine/core/runtime/gameContext";
|
|
4
|
+
import type { ModelAssetRef } from "@jgengine/core/scene/assetCatalog";
|
|
5
|
+
import type { PlayableGame } from "./registry.js";
|
|
6
|
+
type EngineFields<TAssetRef extends ModelAssetRef> = Omit<GameDefinitionConfig<TAssetRef>, "loop" | "ui" | "multiplayer"> & {
|
|
7
|
+
multiplayer?: GameDefinitionConfig<TAssetRef>["multiplayer"];
|
|
8
|
+
};
|
|
9
|
+
type PresentationFields = Omit<PlayableGame, "game" | "content" | "loop" | "GameUI"> & {
|
|
10
|
+
content?: GameContextContent;
|
|
11
|
+
loop?: Partial<GameLoop<GameContext>>;
|
|
12
|
+
GameUI?: ComponentType;
|
|
13
|
+
};
|
|
14
|
+
export type GameConfig<TAssetRef extends ModelAssetRef = ModelAssetRef> = EngineFields<TAssetRef> & PresentationFields;
|
|
15
|
+
export declare function defineGame<TAssetRef extends ModelAssetRef = ModelAssetRef>(config: GameConfig<TAssetRef>): PlayableGame;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { defineGame as defineEngineGame, } from "@jgengine/core/game/defineGame";
|
|
3
|
+
import { offline } from "@jgengine/core/runtime/adapter";
|
|
4
|
+
import { EnvironmentScene } from "./environment.js";
|
|
5
|
+
const noop = () => { };
|
|
6
|
+
function worldBackdrop(feature) {
|
|
7
|
+
return function WorldBackdrop() {
|
|
8
|
+
return _jsx(EnvironmentScene, { feature: feature });
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const emptyUi = () => null;
|
|
12
|
+
export function defineGame(config) {
|
|
13
|
+
const { content, loop, GameUI, environment, camera, multiplayer, WorldOverlay, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, worldHealthBars, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting, backdrop, shadows, presentation, objectStyles, devtools, ...engineFields } = config;
|
|
14
|
+
const game = defineEngineGame({ ...engineFields, multiplayer: multiplayer ?? offline() });
|
|
15
|
+
return {
|
|
16
|
+
game,
|
|
17
|
+
content: content ?? {},
|
|
18
|
+
loop: {
|
|
19
|
+
onInit: loop?.onInit ?? noop,
|
|
20
|
+
onNewPlayer: loop?.onNewPlayer ?? noop,
|
|
21
|
+
onTick: loop?.onTick ?? noop,
|
|
22
|
+
},
|
|
23
|
+
GameUI: GameUI ?? emptyUi,
|
|
24
|
+
environment: environment ??
|
|
25
|
+
(game.world?.kind === "environment" ? worldBackdrop(game.world) : undefined),
|
|
26
|
+
camera: camera ?? { perspective: "third" },
|
|
27
|
+
WorldOverlay,
|
|
28
|
+
renderEntity,
|
|
29
|
+
renderObject,
|
|
30
|
+
entitySprites,
|
|
31
|
+
entityModels,
|
|
32
|
+
objectModels,
|
|
33
|
+
hotbarSelection,
|
|
34
|
+
prompts,
|
|
35
|
+
pointer,
|
|
36
|
+
touch,
|
|
37
|
+
worldHealthBars,
|
|
38
|
+
audio,
|
|
39
|
+
entitySounds,
|
|
40
|
+
objectSounds,
|
|
41
|
+
worldItem,
|
|
42
|
+
collision,
|
|
43
|
+
movement,
|
|
44
|
+
lighting,
|
|
45
|
+
backdrop,
|
|
46
|
+
shadows,
|
|
47
|
+
presentation,
|
|
48
|
+
objectStyles,
|
|
49
|
+
devtools,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DevtoolsOverrides } from "@jgengine/core/devtools/devtools";
|
|
2
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
|
+
import type { ShellMultiplayer } from "../multiplayer.js";
|
|
4
|
+
import type { PlayableGame } from "../registry.js";
|
|
5
|
+
export declare function persistDevtoolsOverrides(gameName: string): DevtoolsOverrides;
|
|
6
|
+
export declare function applyStoredDevtoolsOverrides(gameName: string): void;
|
|
7
|
+
export declare function withDevtoolsLatency(multiplayer: ShellMultiplayer): ShellMultiplayer;
|
|
8
|
+
export declare function DevtoolsRendererProbe(): null;
|
|
9
|
+
export declare function DevtoolsOverlay({ open, ctx, playable, multiplayer, }: {
|
|
10
|
+
open: boolean;
|
|
11
|
+
ctx: GameContext;
|
|
12
|
+
playable: PlayableGame;
|
|
13
|
+
multiplayer: ShellMultiplayer | null;
|
|
14
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { devtools, instrumentLatency, } from "@jgengine/core/devtools/devtools";
|
|
5
|
+
import { bindingLabel } from "@jgengine/core/input/actionBindings";
|
|
6
|
+
const REFRESH_MS = 250;
|
|
7
|
+
const LONG_FRAME_MS = 33.4;
|
|
8
|
+
function overridesStorageKey(gameName) {
|
|
9
|
+
return `jg-devtools:${gameName}`;
|
|
10
|
+
}
|
|
11
|
+
function readStoredOverrides(gameName) {
|
|
12
|
+
try {
|
|
13
|
+
const raw = localStorage.getItem(overridesStorageKey(gameName));
|
|
14
|
+
if (raw === null)
|
|
15
|
+
return null;
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
return Array.isArray(parsed.enabled) && typeof parsed.values === "object" ? parsed : null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function persistDevtoolsOverrides(gameName) {
|
|
24
|
+
const overrides = devtools.overrides.export();
|
|
25
|
+
try {
|
|
26
|
+
localStorage.setItem(overridesStorageKey(gameName), JSON.stringify(overrides));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return overrides;
|
|
30
|
+
}
|
|
31
|
+
return overrides;
|
|
32
|
+
}
|
|
33
|
+
export function applyStoredDevtoolsOverrides(gameName) {
|
|
34
|
+
const stored = readStoredOverrides(gameName);
|
|
35
|
+
if (stored === null)
|
|
36
|
+
return;
|
|
37
|
+
const applied = stored.enabled.length + Object.keys(stored.values).length;
|
|
38
|
+
if (applied === 0)
|
|
39
|
+
return;
|
|
40
|
+
devtools.overrides.apply(stored);
|
|
41
|
+
console.info(`[jgengine:devtools] applied ${applied} stored override(s) for ${gameName}`);
|
|
42
|
+
}
|
|
43
|
+
export function withDevtoolsLatency(multiplayer) {
|
|
44
|
+
return {
|
|
45
|
+
...multiplayer,
|
|
46
|
+
backend: {
|
|
47
|
+
...instrumentLatency(multiplayer.backend, ["pushFeedEntry"]),
|
|
48
|
+
transport: instrumentLatency(multiplayer.backend.transport, ["joinServer", "leaveServer", "runCommand"]),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function DevtoolsRendererProbe() {
|
|
53
|
+
const frameCounter = useRef(0);
|
|
54
|
+
useFrame((state) => {
|
|
55
|
+
frameCounter.current += 1;
|
|
56
|
+
if (frameCounter.current % 30 !== 0)
|
|
57
|
+
return;
|
|
58
|
+
const info = state.gl.info;
|
|
59
|
+
devtools.render.record({
|
|
60
|
+
drawCalls: info.render.calls,
|
|
61
|
+
triangles: info.render.triangles,
|
|
62
|
+
geometries: info.memory.geometries,
|
|
63
|
+
textures: info.memory.textures,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function keybindRows(input) {
|
|
69
|
+
const rows = [];
|
|
70
|
+
for (const [action, codes] of Object.entries(input ?? {})) {
|
|
71
|
+
const entries = flattenCodes(codes);
|
|
72
|
+
for (const entry of entries)
|
|
73
|
+
rows.push({ action, ...entry });
|
|
74
|
+
}
|
|
75
|
+
return rows;
|
|
76
|
+
}
|
|
77
|
+
function flattenCodes(codes) {
|
|
78
|
+
if (Array.isArray(codes)) {
|
|
79
|
+
return [{ codes: codes.map(bindingLabel).join(" / "), mode: "press" }];
|
|
80
|
+
}
|
|
81
|
+
const modes = codes;
|
|
82
|
+
const result = [];
|
|
83
|
+
if (modes.hold !== undefined && modes.hold.length > 0) {
|
|
84
|
+
result.push({ codes: modes.hold.map(bindingLabel).join(" / "), mode: "hold" });
|
|
85
|
+
}
|
|
86
|
+
if (modes.toggle !== undefined && modes.toggle.length > 0) {
|
|
87
|
+
result.push({ codes: modes.toggle.map(bindingLabel).join(" / "), mode: "toggle" });
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
const TABS = [
|
|
92
|
+
{ id: "perf", label: "Perf" },
|
|
93
|
+
{ id: "tune", label: "Tune" },
|
|
94
|
+
{ id: "logs", label: "Logs" },
|
|
95
|
+
{ id: "net", label: "Net" },
|
|
96
|
+
{ id: "keys", label: "Keys" },
|
|
97
|
+
];
|
|
98
|
+
function ms(value) {
|
|
99
|
+
return `${value.toFixed(1)}ms`;
|
|
100
|
+
}
|
|
101
|
+
function StatRow({ name, value, alert }) {
|
|
102
|
+
return (_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsx("span", { className: "text-neutral-400", children: name }), _jsx("span", { className: `font-mono ${alert === true ? "text-red-400" : "text-neutral-100"}`, children: value })] }));
|
|
103
|
+
}
|
|
104
|
+
function FrameBars({ frames }) {
|
|
105
|
+
return (_jsx("div", { className: "flex h-10 items-end gap-px", children: frames.map((frameMs, index) => (_jsx("div", { className: `w-1 rounded-sm ${frameMs > LONG_FRAME_MS ? "bg-red-500" : "bg-emerald-500/80"}`, style: { height: `${Math.min(100, (frameMs / (LONG_FRAME_MS * 2)) * 100)}%` } }, index))) }));
|
|
106
|
+
}
|
|
107
|
+
function PerfPanel({ ctx }) {
|
|
108
|
+
const rateRef = useRef({ version: 0, at: 0, perSecond: 0 });
|
|
109
|
+
const frame = devtools.frame.stats();
|
|
110
|
+
const render = devtools.render.latest();
|
|
111
|
+
const now = performance.now();
|
|
112
|
+
const rate = rateRef.current;
|
|
113
|
+
if (rate.at === 0) {
|
|
114
|
+
rateRef.current = { version: ctx.version(), at: now, perSecond: 0 };
|
|
115
|
+
}
|
|
116
|
+
else if (now - rate.at >= 900) {
|
|
117
|
+
const perSecond = ((ctx.version() - rate.version) / (now - rate.at)) * 1000;
|
|
118
|
+
rateRef.current = { version: ctx.version(), at: now, perSecond };
|
|
119
|
+
}
|
|
120
|
+
return (_jsxs("div", { className: "space-y-2", children: [frame === null ? (_jsx("div", { className: "text-neutral-400", children: "Waiting for frames\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(FrameBars, { frames: frame.recentFrameMs }), _jsx(StatRow, { name: "fps", value: frame.fps.toFixed(0), alert: frame.fps < 50 }), _jsx(StatRow, { name: "frame avg / p95 / max", value: `${ms(frame.avgFrameMs)} / ${ms(frame.p95FrameMs)} / ${ms(frame.maxFrameMs)}`, alert: frame.p95FrameMs > LONG_FRAME_MS }), _jsx(StatRow, { name: "sim avg / max", value: `${ms(frame.avgSimMs)} / ${ms(frame.maxSimMs)}`, alert: frame.avgSimMs > 8 }), _jsx(StatRow, { name: `long frames (of ${frame.samples})`, value: String(frame.longFrames), alert: frame.longFrames > 5 })] })), render !== null ? (_jsxs(_Fragment, { children: [_jsx(StatRow, { name: "draw calls", value: String(render.drawCalls), alert: render.drawCalls > 1000 }), _jsx(StatRow, { name: "triangles", value: render.triangles.toLocaleString() }), _jsx(StatRow, { name: "geometries / textures", value: `${render.geometries} / ${render.textures}` })] })) : null, _jsx(StatRow, { name: "state notifies /s", value: rateRef.current.perSecond.toFixed(0), alert: rateRef.current.perSecond > 90 }), Object.entries(devtools.probes.read()).map(([name, value]) => (_jsx(StatRow, { name: name, value: String(value) }, name)))] }));
|
|
121
|
+
}
|
|
122
|
+
function LogsPanel() {
|
|
123
|
+
const entries = devtools.logs.list().slice(-60);
|
|
124
|
+
const colors = {
|
|
125
|
+
error: "text-red-400",
|
|
126
|
+
warn: "text-amber-300",
|
|
127
|
+
info: "text-sky-300",
|
|
128
|
+
log: "text-neutral-300",
|
|
129
|
+
};
|
|
130
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx("button", { type: "button", className: "rounded border border-neutral-600 px-2 py-0.5 text-neutral-300 hover:bg-neutral-800", onClick: () => devtools.logs.clear(), children: "Clear" }), _jsxs("div", { className: "jg-devtools-scroll max-h-64 space-y-0.5 overflow-auto font-mono text-[10px]", children: [entries.length === 0 ? _jsx("div", { className: "text-neutral-400", children: "No logs captured yet." }) : null, entries.map((entry, index) => (_jsxs("div", { className: colors[entry.level], children: [_jsxs("span", { className: "text-neutral-500", children: [new Date(entry.at).toLocaleTimeString(), " "] }), entry.message] }, index)))] })] }));
|
|
131
|
+
}
|
|
132
|
+
function NetPanel({ multiplayer }) {
|
|
133
|
+
const stats = devtools.latency.stats();
|
|
134
|
+
if (multiplayer === null) {
|
|
135
|
+
return _jsx("div", { className: "text-neutral-400", children: "Offline \u2014 no multiplayer backend attached." });
|
|
136
|
+
}
|
|
137
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(StatRow, { name: "game / user", value: `${multiplayer.gameId} / ${multiplayer.userId}` }), stats === null ? (_jsx("div", { className: "text-neutral-400", children: "No round trips observed yet \u2014 latency is sampled from real backend calls." })) : (_jsxs(_Fragment, { children: [_jsx(StatRow, { name: "last round trip", value: ms(stats.lastMs), alert: stats.lastMs > 250 }), _jsx(StatRow, { name: "avg / min / max", value: `${ms(stats.avgMs)} / ${ms(stats.minMs)} / ${ms(stats.maxMs)}` }), _jsx(StatRow, { name: "samples", value: String(stats.samples) })] }))] }));
|
|
138
|
+
}
|
|
139
|
+
function KeysPanel({ input }) {
|
|
140
|
+
const rows = keybindRows(input);
|
|
141
|
+
if (rows.length === 0)
|
|
142
|
+
return _jsx("div", { className: "text-neutral-400", children: "This game declares no keybinds." });
|
|
143
|
+
return (_jsx("div", { className: "jg-devtools-scroll max-h-64 space-y-0.5 overflow-auto", children: rows.map((row, index) => (_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsx("span", { className: "text-neutral-300", children: row.action }), _jsxs("span", { className: "font-mono text-neutral-100", children: [row.codes, row.mode !== "press" ? _jsx("span", { className: "ml-1 text-[9px] uppercase text-neutral-500", children: row.mode }) : null] })] }, index))) }));
|
|
144
|
+
}
|
|
145
|
+
function ControlInput({ control, onWrite }) {
|
|
146
|
+
const value = control.read();
|
|
147
|
+
const write = (next) => {
|
|
148
|
+
control.write(next);
|
|
149
|
+
onWrite();
|
|
150
|
+
};
|
|
151
|
+
if (control.kind === "slider") {
|
|
152
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "range", className: "h-1 w-28 accent-emerald-400", min: control.min, max: control.max, step: control.step, value: Number(value), onChange: (event) => write(Number(event.target.value)) }), _jsx("input", { type: "number", className: "w-16 rounded border border-neutral-600 bg-neutral-900 px-1 py-0.5 font-mono text-neutral-100", step: control.step, value: Number(value), onChange: (event) => {
|
|
153
|
+
const next = Number(event.target.value);
|
|
154
|
+
if (!Number.isNaN(next))
|
|
155
|
+
write(next);
|
|
156
|
+
} })] }));
|
|
157
|
+
}
|
|
158
|
+
if (control.kind === "toggle") {
|
|
159
|
+
return (_jsx("input", { type: "checkbox", className: "h-4 w-4 accent-emerald-400", checked: Boolean(value), onChange: (event) => write(event.target.checked) }));
|
|
160
|
+
}
|
|
161
|
+
if (control.kind === "color") {
|
|
162
|
+
return (_jsx("input", { type: "color", className: "h-6 w-10 cursor-pointer rounded border border-neutral-600 bg-transparent", value: String(value), onChange: (event) => write(event.target.value) }));
|
|
163
|
+
}
|
|
164
|
+
if (control.kind === "select") {
|
|
165
|
+
return (_jsx("select", { className: "rounded border border-neutral-600 bg-neutral-900 px-1 py-0.5 text-neutral-100", value: String(value), onChange: (event) => {
|
|
166
|
+
const match = control.options?.find((option) => String(option) === event.target.value);
|
|
167
|
+
write(match ?? event.target.value);
|
|
168
|
+
}, children: control.options?.map((option) => (_jsx("option", { value: String(option), children: String(option) }, String(option)))) }));
|
|
169
|
+
}
|
|
170
|
+
return (_jsx("input", { type: "text", className: "w-32 rounded border border-neutral-600 bg-neutral-900 px-1 py-0.5 font-mono text-neutral-100", value: String(value), onChange: (event) => write(event.target.value) }));
|
|
171
|
+
}
|
|
172
|
+
function formatPreview(value) {
|
|
173
|
+
return typeof value === "number" ? String(Math.round(value * 1000) / 1000) : String(value);
|
|
174
|
+
}
|
|
175
|
+
function deltaSnippet(discovered) {
|
|
176
|
+
const overrides = devtools.overrides.export();
|
|
177
|
+
if (Object.keys(overrides.values).length === 0)
|
|
178
|
+
return null;
|
|
179
|
+
const discoveredById = new Map(discovered.map((entry) => [entry.id, entry]));
|
|
180
|
+
const tables = new Map();
|
|
181
|
+
const flat = {};
|
|
182
|
+
for (const [name, value] of Object.entries(overrides.values)) {
|
|
183
|
+
const entry = discoveredById.get(name);
|
|
184
|
+
if (entry !== undefined) {
|
|
185
|
+
const values = tables.get(entry.table) ?? {};
|
|
186
|
+
values[entry.key] = value;
|
|
187
|
+
tables.set(entry.table, values);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
flat[name] = value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const parts = [];
|
|
194
|
+
for (const [table, values] of tables)
|
|
195
|
+
parts.push(`${table}: ${JSON.stringify(values, null, 2)}`);
|
|
196
|
+
if (Object.keys(flat).length > 0)
|
|
197
|
+
parts.push(`controls: ${JSON.stringify(flat, null, 2)}`);
|
|
198
|
+
return parts.join("\n\n");
|
|
199
|
+
}
|
|
200
|
+
function TunePanel({ gameName }) {
|
|
201
|
+
const [deltasCopied, setDeltasCopied] = useState(false);
|
|
202
|
+
const controls = devtools.controls.list();
|
|
203
|
+
const discovered = devtools.discover.list();
|
|
204
|
+
const persist = () => persistDevtoolsOverrides(gameName);
|
|
205
|
+
const discoveredIds = new Set(discovered.map((entry) => entry.id));
|
|
206
|
+
const explicit = controls.filter((control) => !discoveredIds.has(control.name));
|
|
207
|
+
const controlByName = new Map(controls.map((control) => [control.name, control]));
|
|
208
|
+
const snippet = deltaSnippet(discovered);
|
|
209
|
+
const copyDeltas = () => {
|
|
210
|
+
if (snippet === null)
|
|
211
|
+
return;
|
|
212
|
+
const clipboard = navigator.clipboard;
|
|
213
|
+
if (clipboard !== undefined) {
|
|
214
|
+
void clipboard.writeText(snippet).catch(() => console.log(snippet));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
console.log(snippet);
|
|
218
|
+
}
|
|
219
|
+
setDeltasCopied(true);
|
|
220
|
+
setTimeout(() => setDeltasCopied(false), 1500);
|
|
221
|
+
};
|
|
222
|
+
if (explicit.length === 0 && discovered.length === 0) {
|
|
223
|
+
return (_jsxs("div", { className: "space-y-2 text-neutral-400", children: [_jsx("div", { children: "Nothing discovered." }), _jsxs("div", { className: "font-mono text-[10px] text-neutral-500", children: ["export const TUNING = { gravity: -22, skyColor: \"#87ceeb\" };", _jsx("br", {}), "Exported tables of numbers, booleans, and colors appear here", _jsx("br", {}), "automatically — check one to control it live."] })] }));
|
|
224
|
+
}
|
|
225
|
+
const tables = new Map();
|
|
226
|
+
for (const entry of discovered) {
|
|
227
|
+
const list = tables.get(entry.table) ?? [];
|
|
228
|
+
list.push(entry);
|
|
229
|
+
tables.set(entry.table, list);
|
|
230
|
+
}
|
|
231
|
+
return (_jsxs("div", { className: "jg-devtools-scroll max-h-72 space-y-3 overflow-auto", children: [_jsxs("div", { className: "flex gap-1.5", children: [_jsx("button", { type: "button", className: "rounded border border-neutral-600 px-2 py-0.5 text-neutral-300 hover:bg-neutral-800", onClick: () => {
|
|
232
|
+
devtools.controls.resetAll();
|
|
233
|
+
persist();
|
|
234
|
+
}, children: "Reset all" }), _jsx("button", { type: "button", disabled: snippet === null, className: "rounded border border-neutral-600 px-2 py-0.5 text-neutral-300 hover:bg-neutral-800 disabled:opacity-40 disabled:hover:bg-transparent", onClick: copyDeltas, title: "Copy changed values as source snippets to paste upstream", children: deltasCopied ? "Copied" : "Copy deltas" })] }), explicit.length > 0 ? (_jsxs("div", { className: "space-y-1.5", children: [_jsx("div", { className: "text-[9px] uppercase tracking-wide text-neutral-500", children: "registered" }), explicit.map((control) => (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "text-neutral-300", title: control.name, children: control.label }), _jsx(ControlInput, { control: control, onWrite: persist })] }, control.name)))] })) : null, [...tables.entries()].map(([table, entries]) => (_jsxs("div", { className: "space-y-1.5", children: [_jsx("div", { className: "text-[9px] uppercase tracking-wide text-neutral-500", children: table }), entries.map((entry) => {
|
|
235
|
+
const control = entry.enabled ? controlByName.get(entry.id) : undefined;
|
|
236
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("label", { className: "flex min-w-0 items-center gap-1.5 text-neutral-300", title: entry.id, children: [_jsx("input", { type: "checkbox", className: "h-3 w-3 shrink-0 accent-emerald-400", checked: entry.enabled, onChange: (event) => {
|
|
237
|
+
if (event.target.checked)
|
|
238
|
+
devtools.discover.enable(entry.id);
|
|
239
|
+
else
|
|
240
|
+
devtools.discover.disable(entry.id);
|
|
241
|
+
persist();
|
|
242
|
+
} }), _jsx("span", { className: "truncate", children: entry.key })] }), control !== undefined ? (_jsx(ControlInput, { control: control, onWrite: persist })) : (_jsx("span", { className: "font-mono text-neutral-500", children: formatPreview(entry.read()) }))] }, entry.id));
|
|
243
|
+
})] }, table)))] }));
|
|
244
|
+
}
|
|
245
|
+
function buildReport(playable) {
|
|
246
|
+
return { game: playable.game.name, ...devtools.snapshot() };
|
|
247
|
+
}
|
|
248
|
+
export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
|
|
249
|
+
const [tab, setTab] = useState("perf");
|
|
250
|
+
const [, setTick] = useState(0);
|
|
251
|
+
const [copied, setCopied] = useState(false);
|
|
252
|
+
useSyncExternalStore(devtools.signal.subscribe, devtools.signal.version, devtools.signal.version);
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
devtools.logs.captureConsole();
|
|
255
|
+
globalThis.__JG_DEVTOOLS = {
|
|
256
|
+
snapshot: () => buildReport(playable),
|
|
257
|
+
controls: devtools.controls,
|
|
258
|
+
discover: devtools.discover,
|
|
259
|
+
};
|
|
260
|
+
}, [playable]);
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
const stored = readStoredOverrides(playable.game.name);
|
|
263
|
+
if (stored === null)
|
|
264
|
+
return;
|
|
265
|
+
const appliedEnables = new Set();
|
|
266
|
+
const appliedValues = new Set();
|
|
267
|
+
const applyLateRegistrations = () => {
|
|
268
|
+
const discovered = new Map(devtools.discover.list().map((entry) => [entry.id, entry]));
|
|
269
|
+
const needEnable = stored.enabled.filter((id) => !appliedEnables.has(id) && discovered.get(id)?.enabled === false);
|
|
270
|
+
const needValue = Object.entries(stored.values).filter(([name]) => !appliedValues.has(name) && devtools.controls.get(name) !== null);
|
|
271
|
+
if (needEnable.length === 0 && needValue.length === 0)
|
|
272
|
+
return;
|
|
273
|
+
for (const id of needEnable)
|
|
274
|
+
appliedEnables.add(id);
|
|
275
|
+
for (const [name] of needValue)
|
|
276
|
+
appliedValues.add(name);
|
|
277
|
+
devtools.overrides.apply({ enabled: needEnable, values: Object.fromEntries(needValue) });
|
|
278
|
+
};
|
|
279
|
+
applyLateRegistrations();
|
|
280
|
+
return devtools.signal.subscribe(applyLateRegistrations);
|
|
281
|
+
}, [playable]);
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
const disposers = [
|
|
284
|
+
devtools.probes.register("entities", () => ctx.scene.entity.list().length),
|
|
285
|
+
devtools.probes.register("objects", () => ctx.scene.object.list().length),
|
|
286
|
+
];
|
|
287
|
+
return () => {
|
|
288
|
+
for (const dispose of disposers)
|
|
289
|
+
dispose();
|
|
290
|
+
};
|
|
291
|
+
}, [ctx]);
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!open)
|
|
294
|
+
return;
|
|
295
|
+
const interval = setInterval(() => setTick((current) => current + 1), REFRESH_MS);
|
|
296
|
+
return () => clearInterval(interval);
|
|
297
|
+
}, [open]);
|
|
298
|
+
if (!open)
|
|
299
|
+
return null;
|
|
300
|
+
const copyReport = () => {
|
|
301
|
+
const report = JSON.stringify(buildReport(playable), null, 2);
|
|
302
|
+
const clipboard = navigator.clipboard;
|
|
303
|
+
if (clipboard !== undefined) {
|
|
304
|
+
void clipboard.writeText(report).catch(() => console.log(report));
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
console.log(report);
|
|
308
|
+
}
|
|
309
|
+
setCopied(true);
|
|
310
|
+
setTimeout(() => setCopied(false), 1500);
|
|
311
|
+
};
|
|
312
|
+
return (_jsxs("div", { className: "pointer-events-auto absolute left-4 top-4 z-50 w-80 rounded border border-neutral-700 bg-neutral-950/95 p-3 text-xs text-neutral-100 shadow-2xl", children: [_jsx("style", { children: `
|
|
313
|
+
.jg-devtools-scroll {
|
|
314
|
+
scrollbar-width: thin;
|
|
315
|
+
scrollbar-color: #52525b #18181b;
|
|
316
|
+
}
|
|
317
|
+
.jg-devtools-scroll::-webkit-scrollbar {
|
|
318
|
+
width: 8px;
|
|
319
|
+
height: 8px;
|
|
320
|
+
}
|
|
321
|
+
.jg-devtools-scroll::-webkit-scrollbar-track {
|
|
322
|
+
background: #18181b;
|
|
323
|
+
border-radius: 9999px;
|
|
324
|
+
}
|
|
325
|
+
.jg-devtools-scroll::-webkit-scrollbar-thumb {
|
|
326
|
+
background-color: #52525b;
|
|
327
|
+
border-radius: 9999px;
|
|
328
|
+
border: 2px solid #18181b;
|
|
329
|
+
}
|
|
330
|
+
.jg-devtools-scroll::-webkit-scrollbar-thumb:hover {
|
|
331
|
+
background-color: #71717a;
|
|
332
|
+
}
|
|
333
|
+
` }), _jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "font-semibold uppercase tracking-wide text-neutral-300", children: [playable.game.name, " devtools"] }), _jsx("button", { type: "button", className: "rounded border border-neutral-600 px-2 py-0.5 text-neutral-300 hover:bg-neutral-800", onClick: copyReport, children: copied ? "Copied" : "Copy report" })] }), _jsx("div", { className: "mb-2 flex gap-1", children: TABS.map((entry) => (_jsx("button", { type: "button", className: `rounded px-2 py-0.5 ${tab === entry.id ? "bg-neutral-100 text-neutral-950" : "text-neutral-400 hover:bg-neutral-800"}`, onClick: () => setTab(entry.id), children: entry.label }, entry.id))) }), tab === "perf" ? _jsx(PerfPanel, { ctx: ctx }) : null, tab === "logs" ? _jsx(LogsPanel, {}) : null, tab === "net" ? _jsx(NetPanel, { multiplayer: multiplayer }) : null, tab === "keys" ? _jsx(KeysPanel, { input: playable.game.input }) : null, tab === "tune" ? _jsx(TunePanel, { gameName: playable.game.name }) : null, _jsx("div", { className: "mt-2 border-t border-neutral-800 pt-1.5 text-[9px] text-neutral-500", children: "F2 toggles \u00B7 agents: window.__JG_DEVTOOLS.snapshot()" })] }));
|
|
334
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type MutableRefObject } from "react";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import type { SkyEnvironmentDescriptor } from "@jgengine/core/world/features";
|
|
4
|
+
export interface SkyDomeProps {
|
|
5
|
+
topColor?: string;
|
|
6
|
+
horizonColor?: string;
|
|
7
|
+
radius?: number;
|
|
8
|
+
offset?: number;
|
|
9
|
+
exponent?: number;
|
|
10
|
+
/** Exposes the created shader material so a time-of-day driver can mutate its uniforms per frame without recreating it. */
|
|
11
|
+
materialRef?: MutableRefObject<THREE.ShaderMaterial | null>;
|
|
12
|
+
}
|
|
13
|
+
export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
|
|
14
|
+
export interface DaylightProps {
|
|
15
|
+
sky?: SkyDomeProps | false;
|
|
16
|
+
fog?: {
|
|
17
|
+
color?: string;
|
|
18
|
+
near?: number;
|
|
19
|
+
far?: number;
|
|
20
|
+
} | false;
|
|
21
|
+
sun?: {
|
|
22
|
+
position?: readonly [number, number, number];
|
|
23
|
+
intensity?: number;
|
|
24
|
+
color?: string;
|
|
25
|
+
};
|
|
26
|
+
ambient?: {
|
|
27
|
+
skyColor?: string;
|
|
28
|
+
groundColor?: string;
|
|
29
|
+
intensity?: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export declare function Daylight({ sky, fog, sun, ambient }?: DaylightProps): import("react").JSX.Element;
|
|
33
|
+
/** 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. */
|
|
34
|
+
export declare function SkyDaylight({ sky }: {
|
|
35
|
+
sky: SkyEnvironmentDescriptor;
|
|
36
|
+
}): import("react").JSX.Element;
|
|
37
|
+
export interface TimeOfDayDaylightProps {
|
|
38
|
+
sky: SkyEnvironmentDescriptor;
|
|
39
|
+
/** The world's `SimClock` (or a stub exposing `calendar().dayFraction`). Absent means static rendering. */
|
|
40
|
+
clock?: {
|
|
41
|
+
calendar(): {
|
|
42
|
+
dayFraction: number;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Drives `Daylight`'s sun/sky/fog from the world clock when `sky.timeOfDay` and `clock` are both present,
|
|
48
|
+
* sampling `daylightStateAt` every frame; otherwise renders the static preset look via `SkyDaylight`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function TimeOfDayDaylight({ sky, clock }: TimeOfDayDaylightProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { daylightStateAt, SKY_PRESET_DAY_FRACTION } from "./daylightCycle.js";
|
|
6
|
+
const SKY_TOP = "#3fa4f2";
|
|
7
|
+
const SKY_HORIZON = "#e3f4ff";
|
|
8
|
+
const FOG_COLOR = "#e9f6ff";
|
|
9
|
+
const SUN_COLOR = "#fff1c9";
|
|
10
|
+
const HEMI_SKY = "#bfe3ff";
|
|
11
|
+
const HEMI_GROUND = "#4c6b34";
|
|
12
|
+
export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, } = {}) {
|
|
13
|
+
const material = useMemo(() => {
|
|
14
|
+
return new THREE.ShaderMaterial({
|
|
15
|
+
uniforms: {
|
|
16
|
+
topColor: { value: new THREE.Color(topColor) },
|
|
17
|
+
bottomColor: { value: new THREE.Color(horizonColor) },
|
|
18
|
+
offset: { value: offset },
|
|
19
|
+
exponent: { value: exponent },
|
|
20
|
+
},
|
|
21
|
+
vertexShader: `
|
|
22
|
+
varying vec3 vWorldPosition;
|
|
23
|
+
void main() {
|
|
24
|
+
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
|
25
|
+
vWorldPosition = worldPosition.xyz;
|
|
26
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
27
|
+
}
|
|
28
|
+
`,
|
|
29
|
+
fragmentShader: `
|
|
30
|
+
uniform vec3 topColor;
|
|
31
|
+
uniform vec3 bottomColor;
|
|
32
|
+
uniform float offset;
|
|
33
|
+
uniform float exponent;
|
|
34
|
+
varying vec3 vWorldPosition;
|
|
35
|
+
void main() {
|
|
36
|
+
float h = normalize(vWorldPosition + vec3(0.0, offset, 0.0)).y;
|
|
37
|
+
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
|
|
38
|
+
}
|
|
39
|
+
`,
|
|
40
|
+
side: THREE.BackSide,
|
|
41
|
+
depthWrite: false,
|
|
42
|
+
fog: false,
|
|
43
|
+
});
|
|
44
|
+
}, [topColor, horizonColor, offset, exponent]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (materialRef !== undefined)
|
|
47
|
+
materialRef.current = material;
|
|
48
|
+
return () => {
|
|
49
|
+
material.dispose();
|
|
50
|
+
if (materialRef !== undefined)
|
|
51
|
+
materialRef.current = null;
|
|
52
|
+
};
|
|
53
|
+
}, [material, materialRef]);
|
|
54
|
+
return (_jsx("mesh", { material: material, renderOrder: -1, children: _jsx("sphereGeometry", { args: [radius, 32, 16] }) }));
|
|
55
|
+
}
|
|
56
|
+
export function Daylight({ sky, fog, sun, ambient } = {}) {
|
|
57
|
+
const sunPosition = sun?.position ?? [120, 160, 70];
|
|
58
|
+
return (_jsxs(_Fragment, { children: [sky === false ? null : _jsx(SkyDome, { ...(sky ?? {}) }), fog === false ? null : (_jsx("fog", { attach: "fog", args: [fog?.color ?? FOG_COLOR, fog?.near ?? 70, fog?.far ?? 260] })), _jsx("hemisphereLight", { args: [ambient?.skyColor ?? HEMI_SKY, ambient?.groundColor ?? HEMI_GROUND, ambient?.intensity ?? 0.55] }), _jsx("directionalLight", { position: [sunPosition[0], sunPosition[1], sunPosition[2]], intensity: sun?.intensity ?? 0.85, color: sun?.color ?? SUN_COLOR, castShadow: true })] }));
|
|
59
|
+
}
|
|
60
|
+
/** 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. */
|
|
61
|
+
export function SkyDaylight({ sky }) {
|
|
62
|
+
const state = useMemo(() => daylightStateAt(SKY_PRESET_DAY_FRACTION[sky.preset], sky), [sky]);
|
|
63
|
+
return (_jsx(Daylight, { sky: { topColor: state.skyTop, horizonColor: state.skyBottom }, fog: { color: sky.fog?.color ?? state.background, near: sky.fog?.near, far: sky.fog?.far }, sun: { position: state.sunPosition, intensity: state.sunIntensity }, ambient: { intensity: state.ambientIntensity } }));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Drives `Daylight`'s sun/sky/fog from the world clock when `sky.timeOfDay` and `clock` are both present,
|
|
67
|
+
* sampling `daylightStateAt` every frame; otherwise renders the static preset look via `SkyDaylight`.
|
|
68
|
+
*/
|
|
69
|
+
export function TimeOfDayDaylight({ sky, clock }) {
|
|
70
|
+
if (!sky.timeOfDay || clock === undefined)
|
|
71
|
+
return _jsx(SkyDaylight, { sky: sky });
|
|
72
|
+
return _jsx(DrivenDaylight, { sky: sky, clock: clock });
|
|
73
|
+
}
|
|
74
|
+
function DrivenDaylight({ sky, clock, }) {
|
|
75
|
+
const initial = useMemo(() => daylightStateAt(clock.calendar().dayFraction, sky), [clock, sky]);
|
|
76
|
+
const sunRef = useRef(null);
|
|
77
|
+
const hemiRef = useRef(null);
|
|
78
|
+
const fogRef = useRef(null);
|
|
79
|
+
const skyMaterialRef = useRef(null);
|
|
80
|
+
useFrame(() => {
|
|
81
|
+
const state = daylightStateAt(clock.calendar().dayFraction, sky);
|
|
82
|
+
const sun = sunRef.current;
|
|
83
|
+
if (sun !== null) {
|
|
84
|
+
sun.position.set(state.sunPosition[0], state.sunPosition[1], state.sunPosition[2]);
|
|
85
|
+
sun.intensity = state.sunIntensity;
|
|
86
|
+
}
|
|
87
|
+
const hemi = hemiRef.current;
|
|
88
|
+
if (hemi !== null)
|
|
89
|
+
hemi.intensity = state.ambientIntensity;
|
|
90
|
+
const fog = fogRef.current;
|
|
91
|
+
if (fog !== null)
|
|
92
|
+
fog.color.set(sky.fog?.color ?? state.background);
|
|
93
|
+
const skyMaterial = skyMaterialRef.current;
|
|
94
|
+
if (skyMaterial !== null) {
|
|
95
|
+
skyMaterial.uniforms.topColor.value.set(state.skyTop);
|
|
96
|
+
skyMaterial.uniforms.bottomColor.value.set(state.skyBottom);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, materialRef: skyMaterialRef }), _jsx("fog", { attach: "fog", ref: fogRef, args: [sky.fog?.color ?? initial.background, sky.fog?.near ?? 70, sky.fog?.far ?? 260] }), _jsx("hemisphereLight", { ref: hemiRef, args: [HEMI_SKY, HEMI_GROUND, initial.ambientIntensity] }), _jsx("directionalLight", { ref: sunRef, position: initial.sunPosition, intensity: initial.sunIntensity, color: SUN_COLOR, castShadow: true })] }));
|
|
100
|
+
}
|