@jgengine/shell 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -0
- package/dist/GamePlayerShell.js +48 -358
- package/dist/GameUiPreview.js +3 -0
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +111 -26
- package/dist/audio/audioWire.d.ts +22 -4
- package/dist/audio/audioWire.js +46 -3
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/camera/GameCameraRig.js +6 -1
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- package/dist/camera/rigMath.d.ts +20 -0
- package/dist/camera/rigMath.js +29 -0
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +26 -4
- package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
- package/dist/devtools/DevtoolsOverlay.js +28 -12
- package/dist/devtools/agentBridge.js +4 -0
- package/dist/devtools/perfDiagnose.js +1 -1
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
- package/dist/environment/EnvironmentScene.js +12 -3
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +11 -4
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +1 -0
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/modelLoad.js +4 -1
- package/dist/render/resolveModel.d.ts +15 -0
- package/dist/render/resolveModel.js +37 -2
- package/dist/scatter/InstancedScatter.js +5 -0
- package/dist/scatter/scatterProxies.js +24 -0
- package/dist/scene/AuthoredScene.d.ts +3 -1
- package/dist/scene/AuthoredScene.js +7 -2
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GrassFieldRenderer.js +53 -5
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.js +70 -7
- package/dist/scene/WaterRenderer.js +34 -10
- package/dist/scene/builtinSceneKindRenderers.js +4 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +2 -0
- package/dist/settings/appliedSettings.d.ts +2 -1
- package/dist/settings/appliedSettings.js +4 -2
- package/dist/structures/TransformGizmo.d.ts +11 -1
- package/dist/structures/TransformGizmo.js +9 -5
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/grassGeometry.d.ts +33 -1
- package/dist/terrain/grassGeometry.js +83 -21
- package/dist/terrain/grassMaterial.d.ts +18 -0
- package/dist/terrain/grassMaterial.js +66 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/soilPatchMaterial.d.ts +15 -2
- package/dist/terrain/soilPatchMaterial.js +46 -8
- package/dist/terrain/terrainDetailMaterial.js +15 -2
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +9 -0
- package/dist/water/OceanConfig.js +6 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/WorldScene.js +29 -2
- package/dist/worldSky.d.ts +7 -1
- package/dist/worldSky.js +11 -1
- package/package.json +4 -4
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +15 -10
- package/skills/jgengine-ui/api.md +202 -47
- package/skills/jgengine-ui/capabilities.md +71 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +20 -7
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +419 -15
- package/skills/jgengine-world/capabilities.md +108 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge/runtime.d.ts +0 -15
- package/dist/cartridge/runtime.js +0 -556
- package/dist/cartridge/spec.d.ts +0 -188
- package/dist/cartridge/spec.js +0 -19
- package/dist/cartridge/validate.d.ts +0 -2
- package/dist/cartridge/validate.js +0 -78
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useFrame } from "@react-three/fiber";
|
|
2
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { devtools, instrumentLatency, } from "@jgengine/core/devtools/devtools";
|
|
5
|
+
import { fallbackSeamsSnapshot } from "@jgengine/core/devtools/fallbackSeams";
|
|
5
6
|
import { MOVEMENT_TUNING } from "@jgengine/core/movement/movementModel";
|
|
6
7
|
import { collisionDebug } from "./collisionDebug.js";
|
|
7
8
|
import { readStoredOverrides } from "./devtoolsOverrides.js";
|
|
@@ -24,21 +25,35 @@ export function withDevtoolsLatency(multiplayer) {
|
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
}
|
|
28
|
+
const RENDER_SAMPLE_MS = 500;
|
|
29
|
+
const RENDER_PROBE_FRAME_PRIORITY = -10;
|
|
27
30
|
/** @internal */
|
|
28
31
|
export function DevtoolsRendererProbe() {
|
|
29
|
-
const
|
|
32
|
+
const gl = useThree((state) => state.gl);
|
|
33
|
+
const lastSampleAt = useRef(0);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
// three resets gl.info after every render() call, so with a multi-pass
|
|
36
|
+
// composer the counters only ever hold the last fullscreen pass. Own the
|
|
37
|
+
// reset instead: accumulate across the whole frame, reset at frame start.
|
|
38
|
+
gl.info.autoReset = false;
|
|
39
|
+
return () => {
|
|
40
|
+
gl.info.autoReset = true;
|
|
41
|
+
};
|
|
42
|
+
}, [gl]);
|
|
30
43
|
useFrame((state) => {
|
|
31
|
-
frameCounter.current += 1;
|
|
32
|
-
if (frameCounter.current % 30 !== 0)
|
|
33
|
-
return;
|
|
34
44
|
const info = state.gl.info;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
const now = performance.now();
|
|
46
|
+
if (now - lastSampleAt.current >= RENDER_SAMPLE_MS) {
|
|
47
|
+
lastSampleAt.current = now;
|
|
48
|
+
devtools.render.record({
|
|
49
|
+
drawCalls: info.render.calls,
|
|
50
|
+
triangles: info.render.triangles,
|
|
51
|
+
geometries: info.memory.geometries,
|
|
52
|
+
textures: info.memory.textures,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
info.reset();
|
|
56
|
+
}, RENDER_PROBE_FRAME_PRIORITY);
|
|
42
57
|
return null;
|
|
43
58
|
}
|
|
44
59
|
const TABS = [
|
|
@@ -194,6 +209,7 @@ export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
|
|
|
194
209
|
const disposers = [
|
|
195
210
|
devtools.probes.register("entities", () => ctx.scene.entity.list().length),
|
|
196
211
|
devtools.probes.register("objects", () => ctx.scene.object.list().length),
|
|
212
|
+
devtools.probes.register("fallbacks", () => fallbackSeamsSnapshot()),
|
|
197
213
|
];
|
|
198
214
|
return () => {
|
|
199
215
|
for (const dispose of disposers)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
1
2
|
import { getSaveEndpoint } from "@jgengine/core/devtools/saveEndpoint";
|
|
2
3
|
import { isHudAnchor, listActiveHudLayouts, } from "@jgengine/core/ui/hudLayout";
|
|
3
4
|
import { listHudPanelTypes, resolvePanelResize, resizePanelSize, } from "@jgengine/core/ui/hudDocument";
|
|
@@ -104,6 +105,9 @@ export function installAgentBridge(options) {
|
|
|
104
105
|
return { ok: true, snapshot: buildLeanReport(playable) };
|
|
105
106
|
case "debug_report":
|
|
106
107
|
return { ok: true, report: buildFullReport(playable) };
|
|
108
|
+
case "debug_perf_reset":
|
|
109
|
+
devtools.frame.reset();
|
|
110
|
+
return { ok: true };
|
|
107
111
|
case "canvas_state":
|
|
108
112
|
return { ok: true, layouts: canvasPanels() };
|
|
109
113
|
case "canvas_set_editing": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { devtools, LONG_FRAME_MS } from "@jgengine/core/devtools/devtools";
|
|
2
2
|
import { ms } from "./panelAtoms.js";
|
|
3
3
|
const CULPRIT_HINTS = {
|
|
4
|
-
"outside-sim": "Cost is outside the sim driver — three.js render, React commit, GPU, GC, or tab throttle.",
|
|
4
|
+
"outside-sim": "Cost is outside the sim driver — three.js render, React commit, GPU, GC, or tab throttle. Check the render sample: high draw calls/triangles mean scene cost (instancing, culling, terrain density); moderate counts with slow frames mean fill cost (dpr, AO/DOF passes, bloom) — try Quality low/medium.",
|
|
5
5
|
sim: "Sim is slow but no named phase stands out — wrap hot onTick work with measure(\"name\", fn).",
|
|
6
6
|
onTick: "Game loop.onTick is the hotspot — profile inside it with measure(\"physics\"|\"ai\"|…, fn).",
|
|
7
7
|
pose: "Shell movement / collision / voxel step is expensive.",
|
|
@@ -23,6 +23,13 @@ export declare class GameUiErrorBoundary extends Component<{
|
|
|
23
23
|
componentDidCatch(error: unknown, info: ErrorInfo): void;
|
|
24
24
|
render(): ReactNode;
|
|
25
25
|
}
|
|
26
|
+
/** Standalone error footer text (page, browser, engine) shared by every error surface. */
|
|
27
|
+
export declare function errorReportContext(): string;
|
|
28
|
+
/** "Copy error" + "File issue" buttons for any error surface. `report` is the full text to copy / prefill. */
|
|
29
|
+
export declare function ErrorReportActions({ report, issueTitle }: {
|
|
30
|
+
report: string;
|
|
31
|
+
issueTitle: string;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
26
33
|
export declare function DiagnosticOverlay({ diagnostics, gameName }: {
|
|
27
34
|
diagnostics: RuntimeDiagnostic[];
|
|
28
35
|
gameName: string;
|
|
@@ -32,16 +32,35 @@ function expandedReactError(message) {
|
|
|
32
32
|
? "Maximum update depth exceeded. A component is repeatedly updating state during render or after every update."
|
|
33
33
|
: null;
|
|
34
34
|
}
|
|
35
|
+
const ISSUES_NEW_URL = "https://github.com/Noisemaker111/jgengine/issues/new";
|
|
36
|
+
/** Standalone error footer text (page, browser, engine) shared by every error surface. */
|
|
37
|
+
export function errorReportContext() {
|
|
38
|
+
return [
|
|
39
|
+
`Engine: ${VERSION}`,
|
|
40
|
+
`Page: ${window.location.origin}${window.location.pathname}`,
|
|
41
|
+
`Browser: ${navigator.userAgent}`,
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
/** "Copy error" + "File issue" buttons for any error surface. `report` is the full text to copy / prefill. */
|
|
45
|
+
export function ErrorReportActions({ report, issueTitle }) {
|
|
46
|
+
const [copied, setCopied] = useState(false);
|
|
47
|
+
const issueBody = report.length > 8000 ? `${report.slice(0, 8000)}\n\n[Report truncated; use Copy error for the full report.]` : report;
|
|
48
|
+
const issueUrl = `${ISSUES_NEW_URL}?title=${encodeURIComponent(`[BUG] ${issueTitle.slice(0, 120)}`)}&body=${encodeURIComponent(issueBody)}`;
|
|
49
|
+
return (_jsxs("div", { className: "flex gap-2", children: [_jsx("button", { className: "rounded border border-red-300/50 bg-red-900 px-2 py-1 text-xs font-semibold text-red-50 hover:bg-red-800", type: "button", onClick: () => {
|
|
50
|
+
void navigator.clipboard.writeText(report).then(() => {
|
|
51
|
+
setCopied(true);
|
|
52
|
+
window.setTimeout(() => setCopied(false), 2000);
|
|
53
|
+
});
|
|
54
|
+
}, children: copied ? "Copied" : "Copy error" }), _jsx("a", { className: "rounded border border-red-300/50 bg-red-900 px-2 py-1 text-xs font-semibold text-red-50 hover:bg-red-800", href: issueUrl, rel: "noreferrer", target: "_blank", children: "File issue" })] }));
|
|
55
|
+
}
|
|
35
56
|
function diagnosticReport(diagnostic, gameName) {
|
|
36
57
|
const explanation = expandedReactError(diagnostic.message);
|
|
37
58
|
return [
|
|
38
59
|
"JGengine runtime error",
|
|
39
60
|
`Game: ${gameName}`,
|
|
40
|
-
`Engine: ${VERSION}`,
|
|
41
61
|
`Phase: ${diagnostic.phase}`,
|
|
42
62
|
`Time: ${diagnostic.capturedAt}`,
|
|
43
|
-
|
|
44
|
-
`Browser: ${navigator.userAgent}`,
|
|
63
|
+
errorReportContext(),
|
|
45
64
|
explanation === null ? null : `Explanation: ${explanation}`,
|
|
46
65
|
"",
|
|
47
66
|
`Message: ${diagnostic.message}`,
|
|
@@ -52,18 +71,10 @@ function diagnosticReport(diagnostic, gameName) {
|
|
|
52
71
|
.join("\n");
|
|
53
72
|
}
|
|
54
73
|
export function DiagnosticOverlay({ diagnostics, gameName }) {
|
|
55
|
-
const [copied, setCopied] = useState(false);
|
|
56
74
|
if (diagnostics.length === 0)
|
|
57
75
|
return null;
|
|
58
76
|
const latest = diagnostics[diagnostics.length - 1];
|
|
59
77
|
const explanation = expandedReactError(latest.message);
|
|
60
78
|
const report = diagnosticReport(latest, gameName);
|
|
61
|
-
|
|
62
|
-
const issueUrl = `https://github.com/Noisemaker111/jgengine/issues/new?title=${encodeURIComponent(`[BUG] ${latest.phase}: ${latest.message.slice(0, 100)}`)}&body=${encodeURIComponent(issueBody)}`;
|
|
63
|
-
return (_jsxs("div", { className: "pointer-events-auto absolute right-4 top-4 z-50 max-w-lg rounded border border-red-400/60 bg-red-950/95 p-3 text-xs text-red-50 shadow-2xl", children: [_jsx("div", { className: "mb-1 font-semibold uppercase tracking-wide text-red-200", children: "JG engine error" }), _jsxs("div", { className: "font-mono text-[11px] text-red-100", children: ["[", latest.phase, "] ", latest.message] }), explanation !== null ? _jsx("div", { className: "mt-2 text-red-100", children: explanation }) : null, latest.stack !== undefined ? (_jsx("pre", { className: "mt-2 max-h-36 overflow-auto whitespace-pre-wrap text-[10px] text-red-200/80", children: latest.stack })) : null, latest.componentStack !== undefined ? (_jsx("pre", { className: "mt-2 max-h-36 overflow-auto whitespace-pre-wrap text-[10px] text-red-200/80", children: latest.componentStack })) : null, _jsxs("div", { className: "mt-3 flex gap-2", children: [_jsx("button", { className: "rounded border border-red-300/50 bg-red-900 px-2 py-1 font-semibold hover:bg-red-800", type: "button", onClick: () => {
|
|
64
|
-
void navigator.clipboard.writeText(report).then(() => {
|
|
65
|
-
setCopied(true);
|
|
66
|
-
window.setTimeout(() => setCopied(false), 2000);
|
|
67
|
-
});
|
|
68
|
-
}, children: copied ? "Copied" : "Copy error" }), _jsx("a", { className: "rounded border border-red-300/50 bg-red-900 px-2 py-1 font-semibold hover:bg-red-800", href: issueUrl, rel: "noreferrer", target: "_blank", children: "File issue" })] })] }));
|
|
79
|
+
return (_jsxs("div", { className: "pointer-events-auto absolute right-4 top-4 z-50 max-w-lg select-text rounded border border-red-400/60 bg-red-950/95 p-3 text-xs text-red-50 shadow-2xl", children: [_jsx("div", { className: "mb-1 font-semibold uppercase tracking-wide text-red-200", children: "JG engine error" }), _jsxs("div", { className: "font-mono text-[11px] text-red-100", children: ["[", latest.phase, "] ", latest.message] }), explanation !== null ? _jsx("div", { className: "mt-2 text-red-100", children: explanation }) : null, latest.stack !== undefined ? (_jsx("pre", { className: "mt-2 max-h-36 overflow-auto whitespace-pre-wrap text-[10px] text-red-200/80", children: latest.stack })) : null, latest.componentStack !== undefined ? (_jsx("pre", { className: "mt-2 max-h-36 overflow-auto whitespace-pre-wrap text-[10px] text-red-200/80", children: latest.componentStack })) : null, _jsx("div", { className: "mt-3", children: _jsx(ErrorReportActions, { report: report, issueTitle: `${latest.phase}: ${latest.message.slice(0, 100)}` }) })] }));
|
|
69
80
|
}
|
|
@@ -9,6 +9,7 @@ import { resolveStructureBuildings } from "@jgengine/core/world/environmentSumma
|
|
|
9
9
|
import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainDetail, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
|
|
10
10
|
import { GroundPad } from "./GroundPad.js";
|
|
11
11
|
import { RoadRibbons } from "./RoadRibbons.js";
|
|
12
|
+
import { terrainGroundColorSampler } from "./terrainGroundColor.js";
|
|
12
13
|
import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
13
14
|
import { GrassField } from "../terrain/GrassField.js";
|
|
14
15
|
import { CarvedTerrain } from "../terrain/CarvedTerrain.js";
|
|
@@ -73,7 +74,7 @@ function TerrainGround({ terrain, field, center, }) {
|
|
|
73
74
|
function areaCenter(area) {
|
|
74
75
|
return area.position ?? [0, 0];
|
|
75
76
|
}
|
|
76
|
-
function Vegetation({ grass, field }) {
|
|
77
|
+
function Vegetation({ grass, field, groundColor, }) {
|
|
77
78
|
const [cx, cz] = areaCenter(grass.area);
|
|
78
79
|
const heightAt = useMemo(() => cx === 0 && cz === 0
|
|
79
80
|
? field.sampleHeight
|
|
@@ -82,7 +83,15 @@ function Vegetation({ grass, field }) {
|
|
|
82
83
|
const wind = useMemo(() => ({ strength: grass.windStrength }), [grass.windStrength]);
|
|
83
84
|
const colorBase = grass.colors[0];
|
|
84
85
|
const colorTip = grass.colors[grass.colors.length - 1];
|
|
85
|
-
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(GrassField, { area: area, density: grass.density, seed: grass.seed, bladeHeight: grass.bladeHeight, bladeWidth: grass.bladeWidth, heightAt: heightAt, colorBase: colorBase, colorTip: colorTip, wind: wind, frustumCulled: true }) }));
|
|
86
|
+
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(GrassField, { area: area, count: Math.max(1500, Math.min(250000, Math.ceil(grass.density * grass.area.w * grass.area.d))), density: grass.density, seed: grass.seed, bladeHeight: grass.bladeHeight, bladeWidth: grass.bladeWidth, heightAt: heightAt, colorBase: colorBase, colorTip: colorTip, colorGround: groundColor, wind: wind, edgeFeather: Math.min(8, Math.max(1.5, Math.min(grass.area.w, grass.area.d) * 0.12)), frustumCulled: true }) }));
|
|
87
|
+
}
|
|
88
|
+
/** Terrain color under a grass patch center — so blade roots blend into the ground they stand on. */
|
|
89
|
+
function vegetationGroundColor(terrain, field, grass) {
|
|
90
|
+
const sampler = terrainGroundColorSampler(terrain, field);
|
|
91
|
+
if (sampler === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
const [cx, cz] = areaCenter(grass.area);
|
|
94
|
+
return sampler(cx, cz);
|
|
86
95
|
}
|
|
87
96
|
function weatherVolume(area) {
|
|
88
97
|
return [area.w, area.h ?? 60, area.d];
|
|
@@ -145,5 +154,5 @@ export function EnvironmentScene({ feature }) {
|
|
|
145
154
|
const pads = feature.pads ?? [];
|
|
146
155
|
const islands = feature.islands ?? [];
|
|
147
156
|
const roads = feature.roads ?? [];
|
|
148
|
-
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field }, `road-${index}`))), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
|
|
157
|
+
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field }, `road-${index}`))), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field, groundColor: vegetationGroundColor(feature.terrain, field, grass) }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
|
|
149
158
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TerrainEnvironmentDescriptor } from "@jgengine/core/world/features";
|
|
2
|
+
import { type TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
/**
|
|
4
|
+
* Terrain surface color at a world XZ — the palette low/high lerped by the sampled ground height,
|
|
5
|
+
* mirroring how `CarvedTerrain` shades its vertices. The seam that lets everything planted on the
|
|
6
|
+
* ground (grass roots, soil-patch borders) blend into the exact color it stands on.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function terrainGroundColorSampler(terrain: Omit<TerrainEnvironmentDescriptor, "kind"> | undefined, field: TerrainField): ((x: number, z: number) => string) | undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { resolveTerrainPalette } from "@jgengine/core/world/terrain";
|
|
3
|
+
/**
|
|
4
|
+
* Terrain surface color at a world XZ — the palette low/high lerped by the sampled ground height,
|
|
5
|
+
* mirroring how `CarvedTerrain` shades its vertices. The seam that lets everything planted on the
|
|
6
|
+
* ground (grass roots, soil-patch borders) blend into the exact color it stands on.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export function terrainGroundColorSampler(terrain, field) {
|
|
10
|
+
if (terrain === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
const palette = resolveTerrainPalette(terrain);
|
|
13
|
+
const low = new THREE.Color(palette.low);
|
|
14
|
+
const high = new THREE.Color(palette.high);
|
|
15
|
+
const base = terrain.baseHeight ?? 0;
|
|
16
|
+
const swing = terrain.height * 1.2;
|
|
17
|
+
const scratch = new THREE.Color();
|
|
18
|
+
return (x, z) => {
|
|
19
|
+
const t = swing <= 0 ? 0.5 : THREE.MathUtils.clamp((field.sampleHeight(x, z) - (base - swing)) / (swing * 2), 0, 1);
|
|
20
|
+
return `#${scratch.copy(low).lerp(high, t).getHexString()}`;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The happy-path game kit — one import surface covering a whole simple game: authoring
|
|
3
|
+
* (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
+
* (`defineSystem`), the authored-scene document helpers, HUD primitives, and deterministic
|
|
5
|
+
* randomness. Start here; reach for deep package paths only when a game outgrows the kit.
|
|
6
|
+
*
|
|
7
|
+
* @capability game-kit one-stop happy-path import surface for authoring, mounting, state, systems, authored scenes, and HUD
|
|
8
|
+
*/
|
|
9
|
+
export { defineGame, type GameConfig } from "./defineGame.js";
|
|
10
|
+
export { GameHost, type GameHostProps, type EditorSummonModule } from "./GameHost.js";
|
|
11
|
+
export type { PlayableGame } from "./registry.js";
|
|
12
|
+
export { defineStore, type StoreHandle } from "@jgengine/core/store/defineStore";
|
|
13
|
+
export { defineKeyedStore } from "@jgengine/core/store/defineKeyedStore";
|
|
14
|
+
export { defineSystem, type SystemDefinition } from "@jgengine/core/game/defineSystem";
|
|
15
|
+
export type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
16
|
+
export type { GameLoop } from "@jgengine/core/game/defineGame";
|
|
17
|
+
export { normalizeEditorLayers } from "@jgengine/core/editor/document";
|
|
18
|
+
export type { EditorDocument, EditorCatalogDefinition } from "@jgengine/core/editor/types";
|
|
19
|
+
export { authoredSpawnPosition } from "@jgengine/core/world/authoredSpawn";
|
|
20
|
+
export { collectAuthoredTriggers } from "@jgengine/core/scene/authoredTriggers";
|
|
21
|
+
export { environmentContentFromDocument } from "@jgengine/core/editor/environment";
|
|
22
|
+
export { environment } from "@jgengine/core/world/features";
|
|
23
|
+
export type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
24
|
+
export { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react/hudLayout";
|
|
25
|
+
export { StatBar, Hotbar, Clock, Coins } from "@jgengine/react/hud";
|
|
26
|
+
export { seededRng } from "@jgengine/core/random/rng";
|
|
27
|
+
export { offline } from "@jgengine/core/runtime/adapter";
|
package/dist/gameKit.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The happy-path game kit — one import surface covering a whole simple game: authoring
|
|
3
|
+
* (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
+
* (`defineSystem`), the authored-scene document helpers, HUD primitives, and deterministic
|
|
5
|
+
* randomness. Start here; reach for deep package paths only when a game outgrows the kit.
|
|
6
|
+
*
|
|
7
|
+
* @capability game-kit one-stop happy-path import surface for authoring, mounting, state, systems, authored scenes, and HUD
|
|
8
|
+
*/
|
|
9
|
+
// Authoring + mounting — the one public path from config to a running game.
|
|
10
|
+
export { defineGame } from "./defineGame.js";
|
|
11
|
+
export { GameHost } from "./GameHost.js";
|
|
12
|
+
// Serializable state + composable behavior.
|
|
13
|
+
export { defineStore } from "@jgengine/core/store/defineStore";
|
|
14
|
+
export { defineKeyedStore } from "@jgengine/core/store/defineKeyedStore";
|
|
15
|
+
export { defineSystem } from "@jgengine/core/game/defineSystem";
|
|
16
|
+
// Authored scene document — normalize `editor.scene.json`, read spawns and trigger volumes.
|
|
17
|
+
export { normalizeEditorLayers } from "@jgengine/core/editor/document";
|
|
18
|
+
export { authoredSpawnPosition } from "@jgengine/core/world/authoredSpawn";
|
|
19
|
+
export { collectAuthoredTriggers } from "@jgengine/core/scene/authoredTriggers";
|
|
20
|
+
export { environmentContentFromDocument } from "@jgengine/core/editor/environment";
|
|
21
|
+
// World + camera.
|
|
22
|
+
export { environment } from "@jgengine/core/world/features";
|
|
23
|
+
// HUD layout + optional building blocks — games own custom chrome (not a stock face).
|
|
24
|
+
export { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react/hudLayout";
|
|
25
|
+
export { StatBar, Hotbar, Clock, Coins } from "@jgengine/react/hud";
|
|
26
|
+
// Deterministic randomness (inject seeds; never Math.random in game logic).
|
|
27
|
+
export { seededRng } from "@jgengine/core/random/rng";
|
|
28
|
+
// Multiplayer story: omit `multiplayer` entirely for solo — offline is the shell default.
|
|
29
|
+
export { offline } from "@jgengine/core/runtime/adapter";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type MarkerCollection, type MarkerKindStyle } from "@jgengine/core/world/markers";
|
|
2
2
|
export interface MapMarkerBeaconsProps {
|
|
3
|
-
|
|
3
|
+
/** Static views, an external marker source, or a native JGengine MarkerSet. */
|
|
4
|
+
markers: MarkerCollection;
|
|
4
5
|
kindStyles?: Record<string, MarkerKindStyle>;
|
|
5
6
|
height?: number;
|
|
6
7
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { PostProcessingConfig } from "@jgengine/core/render/postProcessing";
|
|
2
|
+
import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
2
3
|
/**
|
|
3
4
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
4
5
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
5
6
|
* post chain: RenderPass → GTAO → UnrealBloom → OutputPass → Grade. Rendered only
|
|
6
7
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
8
|
+
*
|
|
9
|
+
* `quality` (the player's graphics-quality setting) gates the passes whose cost
|
|
10
|
+
* scales with scene geometry or resolution beyond the dpr cap: GTAO re-renders
|
|
11
|
+
* the whole scene for depth/normals and runs a multi-sample full-screen pass,
|
|
12
|
+
* and Bokeh DOF renders scene depth again — both run on "high" only. Bloom,
|
|
13
|
+
* tone mapping, and grade stay on every tier.
|
|
7
14
|
*/
|
|
8
|
-
export declare function PostProcessing({ config }: {
|
|
15
|
+
export declare function PostProcessing({ config, quality }: {
|
|
9
16
|
config: PostProcessingConfig;
|
|
17
|
+
quality?: GraphicsQuality;
|
|
10
18
|
}): null;
|
|
@@ -36,8 +36,14 @@ function syncSize(built, width, height, pixelRatio) {
|
|
|
36
36
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
37
37
|
* post chain: RenderPass → GTAO → UnrealBloom → OutputPass → Grade. Rendered only
|
|
38
38
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
39
|
+
*
|
|
40
|
+
* `quality` (the player's graphics-quality setting) gates the passes whose cost
|
|
41
|
+
* scales with scene geometry or resolution beyond the dpr cap: GTAO re-renders
|
|
42
|
+
* the whole scene for depth/normals and runs a multi-sample full-screen pass,
|
|
43
|
+
* and Bokeh DOF renders scene depth again — both run on "high" only. Bloom,
|
|
44
|
+
* tone mapping, and grade stay on every tier.
|
|
39
45
|
*/
|
|
40
|
-
export function PostProcessing({ config }) {
|
|
46
|
+
export function PostProcessing({ config, quality = "high" }) {
|
|
41
47
|
const gl = useThree((s) => s.gl);
|
|
42
48
|
const scene = useThree((s) => s.scene);
|
|
43
49
|
const camera = useThree((s) => s.camera);
|
|
@@ -52,7 +58,8 @@ export function PostProcessing({ config }) {
|
|
|
52
58
|
});
|
|
53
59
|
const composer = new EffectComposer(gl, target);
|
|
54
60
|
composer.addPass(new RenderPass(scene, camera));
|
|
55
|
-
|
|
61
|
+
const heavyPasses = quality === "high";
|
|
62
|
+
if (heavyPasses && config.ao !== undefined && config.ao !== false) {
|
|
56
63
|
const ao = new GTAOPass(scene, camera, width, height);
|
|
57
64
|
ao.blendIntensity = config.ao.blend ?? 1;
|
|
58
65
|
ao.updateGtaoMaterial({
|
|
@@ -69,7 +76,7 @@ export function PostProcessing({ config }) {
|
|
|
69
76
|
composer.addPass(new UnrealBloomPass(new THREE.Vector2(width, height), b.strength ?? 0.32, b.radius ?? 0.55, b.threshold ?? 0.85));
|
|
70
77
|
}
|
|
71
78
|
let dof = null;
|
|
72
|
-
if (config.dof !== undefined && config.dof !== false) {
|
|
79
|
+
if (heavyPasses && config.dof !== undefined && config.dof !== false) {
|
|
73
80
|
const d = config.dof;
|
|
74
81
|
dof = new BokehPass(scene, camera, {
|
|
75
82
|
focus: d.focus ?? 18,
|
|
@@ -91,7 +98,7 @@ export function PostProcessing({ config }) {
|
|
|
91
98
|
disposeGraph(graph);
|
|
92
99
|
builtRef.current = null;
|
|
93
100
|
};
|
|
94
|
-
}, [gl, scene, camera, config]);
|
|
101
|
+
}, [gl, scene, camera, config, quality]);
|
|
95
102
|
useEffect(() => {
|
|
96
103
|
const prevTone = gl.toneMapping;
|
|
97
104
|
const prevExposure = gl.toneMappingExposure;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CatalogEntityRole } from "@jgengine/core/runtime/gameContext";
|
|
2
|
+
import type { PresentationEffectsConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
/** Resolved world-bar / nameplate config, or `null` when the feature is off. @internal */
|
|
4
|
+
export interface ResolvedWorldOverlayBars {
|
|
5
|
+
statId: string;
|
|
6
|
+
roles?: readonly CatalogEntityRole[];
|
|
7
|
+
maxDistance?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Normalize `worldHealthBars` / `nameplates` (`boolean | object | undefined`) into a
|
|
11
|
+
* single resolved shape. `undefined` / `false` → `null` (off); `true` → default stat `"health"`;
|
|
12
|
+
* object → explicit fields with `statId` defaulting to `"health"`.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveWorldOverlayBars(config: boolean | {
|
|
16
|
+
statId?: string;
|
|
17
|
+
roles?: readonly CatalogEntityRole[];
|
|
18
|
+
maxDistance?: number;
|
|
19
|
+
} | undefined): ResolvedWorldOverlayBars | null;
|
|
20
|
+
/** Resolved combat presentation channels (each boolean is whether that stack piece mounts). @internal */
|
|
21
|
+
export interface ResolvedPresentationEffects {
|
|
22
|
+
telegraphs: boolean;
|
|
23
|
+
vfx: boolean;
|
|
24
|
+
floatText: boolean;
|
|
25
|
+
tracers: boolean;
|
|
26
|
+
shake: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve `presentationEffects` for the 3D shell.
|
|
30
|
+
* - `undefined` / `true` → all channels on (historical default).
|
|
31
|
+
* - `false` → none.
|
|
32
|
+
* - object → per-channel; missing keys default to on so games can disable one channel.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolvePresentationEffects(config: boolean | PresentationEffectsConfig | undefined): ResolvedPresentationEffects;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize `worldHealthBars` / `nameplates` (`boolean | object | undefined`) into a
|
|
3
|
+
* single resolved shape. `undefined` / `false` → `null` (off); `true` → default stat `"health"`;
|
|
4
|
+
* object → explicit fields with `statId` defaulting to `"health"`.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export function resolveWorldOverlayBars(config) {
|
|
8
|
+
if (config === undefined || config === false)
|
|
9
|
+
return null;
|
|
10
|
+
if (config === true)
|
|
11
|
+
return { statId: "health" };
|
|
12
|
+
return {
|
|
13
|
+
statId: config.statId ?? "health",
|
|
14
|
+
...(config.roles === undefined ? {} : { roles: config.roles }),
|
|
15
|
+
...(config.maxDistance === undefined ? {} : { maxDistance: config.maxDistance }),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const ALL_ON = {
|
|
19
|
+
telegraphs: true,
|
|
20
|
+
vfx: true,
|
|
21
|
+
floatText: true,
|
|
22
|
+
tracers: true,
|
|
23
|
+
shake: true,
|
|
24
|
+
};
|
|
25
|
+
const ALL_OFF = {
|
|
26
|
+
telegraphs: false,
|
|
27
|
+
vfx: false,
|
|
28
|
+
floatText: false,
|
|
29
|
+
tracers: false,
|
|
30
|
+
shake: false,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Resolve `presentationEffects` for the 3D shell.
|
|
34
|
+
* - `undefined` / `true` → all channels on (historical default).
|
|
35
|
+
* - `false` → none.
|
|
36
|
+
* - object → per-channel; missing keys default to on so games can disable one channel.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export function resolvePresentationEffects(config) {
|
|
40
|
+
if (config === false)
|
|
41
|
+
return ALL_OFF;
|
|
42
|
+
if (config === undefined || config === true)
|
|
43
|
+
return ALL_ON;
|
|
44
|
+
return {
|
|
45
|
+
telegraphs: config.telegraphs !== false,
|
|
46
|
+
vfx: config.vfx !== false,
|
|
47
|
+
floatText: config.floatText !== false,
|
|
48
|
+
tracers: config.tracers !== false,
|
|
49
|
+
shake: config.shake !== false,
|
|
50
|
+
};
|
|
51
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
|
5
5
|
import type { ViewmodelProps } from "./camera/GameFirstPersonCamera.js";
|
|
6
6
|
export type RenderEntity = (entity: SceneEntity) => ReactNode;
|
|
7
7
|
export type RenderObject = (object: SceneObject) => ReactNode;
|
|
8
|
+
/** The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle. */
|
|
8
9
|
export type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps>>;
|
|
9
10
|
export type GameRegistry = Record<string, () => Promise<PlayableGame>>;
|
|
10
11
|
export declare function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a root-absolute asset URL (`/models/…`, `/materials/…`) against the
|
|
3
|
+
* installed app base. Relative, protocol-relative, absolute-scheme (`https:`,
|
|
4
|
+
* `blob:`, `data:`), and already-based URLs pass through unchanged. Installed
|
|
5
|
+
* as the URL modifier on THREE loading managers so every model and texture
|
|
6
|
+
* load resolves correctly wherever the app is mounted.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveAssetBaseUrl(url: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Installs the app base URL (pass `import.meta.env.BASE_URL`) so root-absolute
|
|
11
|
+
* asset paths load from under it. Call once at app startup, before any game
|
|
12
|
+
* loads. Bases that are not root-absolute (`/`, `./`) reset to the pass-through
|
|
13
|
+
* default. Also registers {@link resolveAssetBaseUrl} on
|
|
14
|
+
* `THREE.DefaultLoadingManager`, covering `TextureLoader`, drei's `useTexture`
|
|
15
|
+
* / `useGLTF`, and every other loader on the default manager.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installAssetBase(base: string): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/**
|
|
3
|
+
* The app base URL every root-absolute asset path resolves under. `/` (the
|
|
4
|
+
* default) leaves URLs untouched — standalone game runners serve assets from
|
|
5
|
+
* the site root. The website serves the runner under `/play/`, where a game's
|
|
6
|
+
* `/models/…` or `/materials/…` must load from `/play/models/…`.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
let assetBase = "/";
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a root-absolute asset URL (`/models/…`, `/materials/…`) against the
|
|
12
|
+
* installed app base. Relative, protocol-relative, absolute-scheme (`https:`,
|
|
13
|
+
* `blob:`, `data:`), and already-based URLs pass through unchanged. Installed
|
|
14
|
+
* as the URL modifier on THREE loading managers so every model and texture
|
|
15
|
+
* load resolves correctly wherever the app is mounted.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveAssetBaseUrl(url) {
|
|
18
|
+
if (assetBase === "/" || !url.startsWith("/") || url.startsWith("//") || url.startsWith(assetBase)) {
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
return `${assetBase.replace(/\/+$/, "")}${url}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Installs the app base URL (pass `import.meta.env.BASE_URL`) so root-absolute
|
|
25
|
+
* asset paths load from under it. Call once at app startup, before any game
|
|
26
|
+
* loads. Bases that are not root-absolute (`/`, `./`) reset to the pass-through
|
|
27
|
+
* default. Also registers {@link resolveAssetBaseUrl} on
|
|
28
|
+
* `THREE.DefaultLoadingManager`, covering `TextureLoader`, drei's `useTexture`
|
|
29
|
+
* / `useGLTF`, and every other loader on the default manager.
|
|
30
|
+
*/
|
|
31
|
+
export function installAssetBase(base) {
|
|
32
|
+
const normalized = base.endsWith("/") ? base : `${base}/`;
|
|
33
|
+
assetBase = normalized.startsWith("/") && !normalized.startsWith("//") ? normalized : "/";
|
|
34
|
+
THREE.DefaultLoadingManager.setURLModifier(resolveAssetBaseUrl);
|
|
35
|
+
}
|
package/dist/render/modelLoad.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as THREE from "three";
|
|
|
2
2
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
3
|
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
|
4
4
|
import { classifyAssetResponse, } from "@jgengine/core/scene/assetDiagnostics";
|
|
5
|
+
import { resolveAssetBaseUrl } from "./assetBase.js";
|
|
5
6
|
/**
|
|
6
7
|
* Dedicated LoadingManager for every GLB load instead of THREE.DefaultLoadingManager.
|
|
7
8
|
* The shared default manager is process-wide; under repeated dev-server navigations its
|
|
@@ -10,6 +11,7 @@ import { classifyAssetResponse, } from "@jgengine/core/scene/assetDiagnostics";
|
|
|
10
11
|
* @internal
|
|
11
12
|
*/
|
|
12
13
|
const modelLoadingManager = new THREE.LoadingManager();
|
|
14
|
+
modelLoadingManager.setURLModifier(resolveAssetBaseUrl);
|
|
13
15
|
/** How many leading bytes to read when probing a failed model URL for its signature. @internal */
|
|
14
16
|
const PROBE_BYTE_LIMIT = 64;
|
|
15
17
|
/**
|
|
@@ -52,7 +54,8 @@ export class DiagnosticGLTFLoader extends GLTFLoader {
|
|
|
52
54
|
super.load(url, onLoad, onProgress, (event) => {
|
|
53
55
|
if (onError === undefined)
|
|
54
56
|
return;
|
|
55
|
-
|
|
57
|
+
// Probe the URL the loader actually fetched (base-resolved), not the raw input.
|
|
58
|
+
void probeModelUrl(this.manager.resolveURL(url))
|
|
56
59
|
.then((diagnosis) => {
|
|
57
60
|
if (diagnosis.ok) {
|
|
58
61
|
// The bytes look fine on re-fetch — surface the original parse error unchanged.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
2
2
|
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { GameContextModels } from "@jgengine/core/runtime/gameContext";
|
|
3
4
|
export interface ModelResolveContext {
|
|
4
5
|
seam: "entityModels" | "objectModels" | "scatterModels";
|
|
5
6
|
key: string;
|
|
@@ -40,6 +41,20 @@ export declare function pickModel(assets: AssetCatalog, pick: ModelPick): ModelC
|
|
|
40
41
|
* @internal
|
|
41
42
|
*/
|
|
42
43
|
export declare function resolveModelPlan(assets: AssetCatalog, plan: Record<string, ModelPick>): Record<string, ModelConfig>;
|
|
44
|
+
/**
|
|
45
|
+
* The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
|
|
46
|
+
* object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
|
|
47
|
+
* ids (a missing catalog id keeps the default collider instead of throwing — the render path already
|
|
48
|
+
* reports it), passes inline configs through. `undefined` when the playable maps no models at all.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare function contextModels(playable: {
|
|
52
|
+
game: {
|
|
53
|
+
assets: AssetCatalog;
|
|
54
|
+
};
|
|
55
|
+
entityModels?: Record<string, string | ModelConfig>;
|
|
56
|
+
objectModels?: Record<string, string | ModelConfig>;
|
|
57
|
+
}): GameContextModels | undefined;
|
|
43
58
|
/**
|
|
44
59
|
* Builds a `(key) => ModelConfig | null` override from a `seam`'s id/config map plus its catalog —
|
|
45
60
|
* `undefined` when either half is missing so the caller can fall back to its own default renderer.
|