@jgengine/shell 0.10.0 → 0.11.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 +13 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayerShell.d.ts +0 -12
- package/dist/GamePlayerShell.js +88 -1043
- package/dist/GameUiPreview.js +0 -2
- package/dist/audio/audioEngine.js +30 -8
- package/dist/audio/audioWire.d.ts +22 -0
- package/dist/audio/audioWire.js +19 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +2 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +94 -24
- package/dist/camera/rigMath.js +100 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/cartridge.d.ts +1 -1
- package/dist/cartridge.js +3 -3
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.js +25 -6
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
- package/dist/devtools/DevtoolsOverlay.js +19 -450
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +262 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +32 -6
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.js +47 -9
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/registry.d.ts +3 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +73 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +39 -3
- package/dist/render/resolveModel.js +79 -2
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +130 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +94 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +82 -0
- package/dist/scene/AuthoredScene.js +173 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +25 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +21 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +37 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +22 -0
- package/dist/scene/sceneKindRenderers.d.ts +28 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +5 -0
- package/dist/settings/appliedSettings.js +9 -0
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +43 -0
- package/dist/structures/TransformGizmo.js +55 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.js +1 -1
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +3 -0
- package/dist/terrain/grassGeometry.js +3 -0
- package/dist/terrain/grassMaterial.d.ts +2 -0
- package/dist/terrain/grassMaterial.js +2 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +10 -0
- package/dist/terrain/soilPatchMaterial.js +109 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +72 -5
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/OceanConfig.d.ts +2 -0
- package/dist/water/OceanConfig.js +2 -0
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +49 -2
- package/dist/world/WorldHud.js +87 -2
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +159 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +5 -0
- package/dist/worldSky.js +11 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- package/skills/jgengine/SKILL.md +51 -0
- package/skills/jgengine/api.md +434 -0
- package/skills/jgengine/capabilities.md +29 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine-ui/SKILL.md +36 -0
- package/skills/jgengine-ui/api.md +1852 -0
- package/skills/jgengine-ui/capabilities.md +142 -0
- package/skills/jgengine-ui/reference.md +500 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +38 -0
- package/skills/jgengine-world/api.md +2652 -0
- package/skills/jgengine-world/capabilities.md +370 -0
- package/skills/jgengine-world/reference.md +222 -0
- package/llms.txt +0 -2761
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { devtools, LONG_FRAME_MS } from "@jgengine/core/devtools/devtools";
|
|
4
|
+
import { ms, SectionLabel, StatRow } from "./panelAtoms.js";
|
|
5
|
+
import { diagnose } from "./perfDiagnose.js";
|
|
6
|
+
const PHASE_BAR_BUDGET_MS = 16.7;
|
|
7
|
+
function FrameBars({ frames }) {
|
|
8
|
+
return (_jsx("div", { className: "flex h-10 items-end gap-px", children: frames.map((frameMs, index) => (_jsx("div", { className: `w-1 rounded-full ${frameMs > LONG_FRAME_MS ? "bg-red-400" : "bg-cyan-400/70"}`, style: { height: `${Math.min(100, (frameMs / (LONG_FRAME_MS * 2)) * 100)}%` }, title: `${frameMs.toFixed(1)}ms` }, index))) }));
|
|
9
|
+
}
|
|
10
|
+
function PhaseBars({ phases, avgSimMs }) {
|
|
11
|
+
if (phases.length === 0) {
|
|
12
|
+
return _jsx("div", { className: "text-neutral-500", children: "No phase samples yet." });
|
|
13
|
+
}
|
|
14
|
+
const budget = Math.max(PHASE_BAR_BUDGET_MS, avgSimMs, ...phases.map((phase) => phase.avgMs));
|
|
15
|
+
return (_jsx("div", { className: "space-y-1", children: phases.slice(0, 8).map((phase) => {
|
|
16
|
+
const hot = phase.avgMs > 4 || phase.maxMs > 8;
|
|
17
|
+
return (_jsxs("div", { className: "space-y-0.5", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: `truncate ${hot ? "text-amber-300" : "text-neutral-300"}`, title: phase.name, children: phase.name }), _jsxs("span", { className: `shrink-0 font-mono ${hot ? "text-amber-300" : "text-neutral-100"}`, children: [ms(phase.avgMs), _jsx("span", { className: "text-neutral-500", children: " avg" }), _jsx("span", { className: "text-neutral-600", children: " \u00B7 " }), ms(phase.maxMs), _jsx("span", { className: "text-neutral-500", children: " max" }), _jsx("span", { className: "text-neutral-600", children: " \u00B7 " }), phase.pctOfSim.toFixed(0), "%"] })] }), _jsx("div", { className: "h-1.5 overflow-hidden rounded-full bg-white/[0.06]", children: _jsx("div", { className: `h-full rounded-full ${hot ? "bg-gradient-to-r from-amber-400 to-amber-300" : "bg-gradient-to-r from-cyan-500 to-cyan-400/70"}`, style: { width: `${Math.min(100, (phase.avgMs / budget) * 100)}%` } }) })] }, phase.name));
|
|
18
|
+
}) }));
|
|
19
|
+
}
|
|
20
|
+
function BudgetSplit({ simMs, outsideMs }) {
|
|
21
|
+
const total = Math.max(1e-6, simMs + outsideMs);
|
|
22
|
+
const simPct = (simMs / total) * 100;
|
|
23
|
+
const outsideHot = outsideMs > simMs && outsideMs > 4;
|
|
24
|
+
const simHot = simMs > 8;
|
|
25
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex h-2 overflow-hidden rounded-full bg-white/[0.06]", children: [_jsx("div", { className: `h-full ${simHot ? "bg-amber-400" : "bg-sky-400"}`, style: { width: `${simPct}%` }, title: `sim ${ms(simMs)}` }), _jsx("div", { className: `h-full ${outsideHot ? "bg-red-500" : "bg-violet-400/80"}`, style: { width: `${100 - simPct}%` }, title: `outside sim ${ms(outsideMs)}` })] }), _jsxs("div", { className: "flex justify-between gap-2 font-mono text-[10px]", children: [_jsxs("span", { className: simHot ? "text-amber-300" : "text-sky-300", children: ["sim ", ms(simMs)] }), _jsxs("span", { className: outsideHot ? "text-red-400" : "text-violet-300", children: ["outside ", ms(outsideMs)] })] }), _jsx("div", { className: "text-[10px] text-neutral-500", children: "outside = render / React / GPU / GC / missed vsync \u2014 not measured inside the sim driver" })] }));
|
|
26
|
+
}
|
|
27
|
+
function LongFrameList({ events }) {
|
|
28
|
+
if (events.length === 0) {
|
|
29
|
+
return _jsxs("div", { className: "text-neutral-500", children: ["No long frames (>", ms(LONG_FRAME_MS), ") captured yet."] });
|
|
30
|
+
}
|
|
31
|
+
const newest = [...events].reverse().slice(0, 12);
|
|
32
|
+
return (_jsx("div", { className: "jg-devtools-scroll max-h-40 space-y-1.5 overflow-auto", children: newest.map((event, index) => (_jsxs("div", { className: "rounded-md border border-white/[0.06] bg-white/[0.03] px-2 py-1.5", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "font-mono text-red-400", children: ms(event.frameMs) }), _jsx("span", { className: "truncate text-amber-300", title: event.culprit, children: event.culprit }), _jsx("span", { className: "shrink-0 text-neutral-500", children: new Date(event.at).toLocaleTimeString() })] }), _jsx("div", { className: "mt-0.5 text-[10px] leading-snug text-neutral-300", children: event.reason }), event.phases.length > 0 ? (_jsxs("div", { className: "mt-0.5 font-mono text-[9px] text-neutral-500", children: [event.phases
|
|
33
|
+
.slice(0, 4)
|
|
34
|
+
.map((phase) => `${phase.name} ${phase.ms.toFixed(1)}`)
|
|
35
|
+
.join(" · "), event.outsideMs >= 2 ? ` · outside ${event.outsideMs.toFixed(1)}` : ""] })) : event.outsideMs >= 2 ? (_jsxs("div", { className: "mt-0.5 font-mono text-[9px] text-neutral-500", children: ["outside ", event.outsideMs.toFixed(1)] })) : null, event.render !== null ? (_jsxs("div", { className: "mt-0.5 font-mono text-[9px] text-neutral-600", children: ["draws ", event.render.drawCalls, " \u00B7 tris ", event.render.triangles.toLocaleString()] })) : null] }, `${event.at}-${index}`))) }));
|
|
36
|
+
}
|
|
37
|
+
export function PerfPanel({ ctx }) {
|
|
38
|
+
const rateRef = useRef({ version: 0, at: 0, perSecond: 0 });
|
|
39
|
+
const frame = devtools.frame.stats();
|
|
40
|
+
const render = devtools.render.latest();
|
|
41
|
+
const longs = devtools.frame.longFrames();
|
|
42
|
+
const now = performance.now();
|
|
43
|
+
const rate = rateRef.current;
|
|
44
|
+
if (rate.at === 0) {
|
|
45
|
+
rateRef.current = { version: ctx.version(), at: now, perSecond: 0 };
|
|
46
|
+
}
|
|
47
|
+
else if (now - rate.at >= 900) {
|
|
48
|
+
const perSecond = ((ctx.version() - rate.version) / (now - rate.at)) * 1000;
|
|
49
|
+
rateRef.current = { version: ctx.version(), at: now, perSecond };
|
|
50
|
+
}
|
|
51
|
+
const diagnosis = frame !== null ? diagnose(frame, longs) : null;
|
|
52
|
+
return (_jsxs("div", { className: "jg-devtools-scroll max-h-[28rem] space-y-3 overflow-auto", 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: "outside avg / max", value: `${ms(frame.avgOutsideMs)} / ${ms(frame.maxOutsideMs)}`, alert: frame.avgOutsideMs > 12 }), _jsx(StatRow, { name: `long frames (of ${frame.samples})`, value: String(frame.longFrames), alert: frame.longFrames > 5 }), diagnosis !== null ? (_jsxs("div", { className: "rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[10px] leading-snug text-amber-100", children: [_jsx("span", { className: "font-semibold text-amber-300", children: "Why slow \u00B7 " }), diagnosis] })) : (_jsx("div", { className: "text-[10px] text-emerald-400/90", children: "Frame budget healthy \u2014 no dominant hitch pattern." })), _jsxs("div", { className: "space-y-1", children: [_jsx(SectionLabel, { children: "frame budget" }), _jsx(BudgetSplit, { simMs: frame.avgSimMs, outsideMs: frame.avgOutsideMs })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(SectionLabel, { children: "sim phases (avg)" }), _jsx(PhaseBars, { phases: frame.phases, avgSimMs: frame.avgSimMs })] })] })), _jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(SectionLabel, { children: "long frames" }), longs.length > 0 ? (_jsx("button", { type: "button", className: "rounded border border-neutral-700 px-1.5 py-0.5 text-[9px] text-neutral-400 hover:bg-neutral-800", onClick: () => devtools.frame.clearLongFrames(), children: "Clear" })) : null] }), _jsx(LongFrameList, { events: longs })] }), render !== null ? (_jsxs("div", { className: "space-y-1", children: [_jsx(SectionLabel, { children: "render sample" }), _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, _jsxs("div", { className: "space-y-1", children: [_jsx(SectionLabel, { children: "probes" }), _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)))] }), _jsx("div", { className: "text-[9px] leading-snug text-neutral-600", children: "Game code: measure(\"physics\", () => \u2026) inside onTick. Agents: snapshot().longFrames + frame.phases" })] }));
|
|
53
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { convertAngle, devtools, parseColor, } from "@jgengine/core/devtools/devtools";
|
|
4
|
+
import { getSaveEndpoint } from "@jgengine/core/devtools/saveEndpoint";
|
|
5
|
+
import { persistDevtoolsOverrides } from "./devtoolsOverrides.js";
|
|
6
|
+
function ControlInput({ control, onWrite }) {
|
|
7
|
+
const value = control.read();
|
|
8
|
+
const write = (next) => {
|
|
9
|
+
if (control.write(next))
|
|
10
|
+
onWrite();
|
|
11
|
+
};
|
|
12
|
+
if (control.kind === "slider" || control.kind === "angle") {
|
|
13
|
+
const unit = control.unit ?? "rad";
|
|
14
|
+
const displayUnit = control.displayUnit ?? (control.kind === "angle" ? "deg" : unit);
|
|
15
|
+
const displayValue = control.kind === "angle" && typeof value === "number"
|
|
16
|
+
? convertAngle(value, unit, displayUnit)
|
|
17
|
+
: Number(value);
|
|
18
|
+
const displayMin = control.min !== undefined && control.kind === "angle"
|
|
19
|
+
? convertAngle(control.min, unit, displayUnit)
|
|
20
|
+
: control.min;
|
|
21
|
+
const displayMax = control.max !== undefined && control.kind === "angle"
|
|
22
|
+
? convertAngle(control.max, unit, displayUnit)
|
|
23
|
+
: control.max;
|
|
24
|
+
const displayStep = control.step !== undefined && control.kind === "angle" && unit !== displayUnit
|
|
25
|
+
? convertAngle(control.step, unit, displayUnit)
|
|
26
|
+
: control.step;
|
|
27
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "range", className: "h-1 w-28 accent-cyan-400", min: displayMin, max: displayMax, step: displayStep, value: displayValue, onChange: (event) => {
|
|
28
|
+
const next = Number(event.target.value);
|
|
29
|
+
write(control.kind === "angle" ? convertAngle(next, displayUnit, unit) : next);
|
|
30
|
+
} }), _jsx("input", { type: "number", className: "w-16 rounded-md border border-white/10 bg-black/40 px-1 py-0.5 font-mono text-neutral-100", step: displayStep, value: Number(displayValue.toFixed(4)), onChange: (event) => {
|
|
31
|
+
const next = Number(event.target.value);
|
|
32
|
+
if (!Number.isNaN(next)) {
|
|
33
|
+
write(control.kind === "angle" ? convertAngle(next, displayUnit, unit) : next);
|
|
34
|
+
}
|
|
35
|
+
} }), control.kind === "angle" ? (_jsx("span", { className: "text-[10px] text-neutral-500", children: displayUnit })) : null] }));
|
|
36
|
+
}
|
|
37
|
+
if (control.kind === "toggle") {
|
|
38
|
+
return (_jsx("input", { type: "checkbox", className: "h-4 w-4 accent-cyan-400", checked: Boolean(value), onChange: (event) => write(event.target.checked) }));
|
|
39
|
+
}
|
|
40
|
+
if (control.kind === "color") {
|
|
41
|
+
const parsed = parseColor(value);
|
|
42
|
+
const rgb = parsed?.rgb ?? "#000000";
|
|
43
|
+
const alpha = parsed?.alpha ?? 1;
|
|
44
|
+
const showAlpha = control.hasAlpha === true || parsed?.hasAlpha === true;
|
|
45
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "color", className: "h-6 w-10 cursor-pointer rounded border border-neutral-600 bg-transparent", value: rgb, onChange: (event) => {
|
|
46
|
+
if (showAlpha) {
|
|
47
|
+
const a = Math.round(alpha * 255)
|
|
48
|
+
.toString(16)
|
|
49
|
+
.padStart(2, "0");
|
|
50
|
+
write(`${event.target.value}${a}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
write(event.target.value);
|
|
54
|
+
}
|
|
55
|
+
} }), showAlpha ? (_jsx("input", { type: "range", className: "h-1 w-16 accent-cyan-400", min: 0, max: 1, step: 0.01, value: alpha, onChange: (event) => {
|
|
56
|
+
const nextAlpha = Number(event.target.value);
|
|
57
|
+
const a = Math.round(nextAlpha * 255)
|
|
58
|
+
.toString(16)
|
|
59
|
+
.padStart(2, "0");
|
|
60
|
+
write(`${rgb}${a}`);
|
|
61
|
+
} })) : null] }));
|
|
62
|
+
}
|
|
63
|
+
if (control.kind === "select" || control.kind === "enum") {
|
|
64
|
+
const choices = control.choices ??
|
|
65
|
+
control.options?.map((option) => ({ value: option, label: String(option) })) ??
|
|
66
|
+
[];
|
|
67
|
+
return (_jsx("select", { className: "rounded-md border border-white/10 bg-black/40 px-1 py-0.5 text-neutral-100", value: String(value), onChange: (event) => {
|
|
68
|
+
const match = choices.find((choice) => String(choice.value) === event.target.value);
|
|
69
|
+
if (match !== undefined)
|
|
70
|
+
write(match.value);
|
|
71
|
+
}, children: choices.map((choice) => (_jsx("option", { value: String(choice.value), children: choice.label ?? String(choice.value) }, String(choice.value)))) }));
|
|
72
|
+
}
|
|
73
|
+
if (control.kind === "vec2" || control.kind === "vec3" || control.kind === "vec4") {
|
|
74
|
+
const axes = Array.isArray(value) ? value : [];
|
|
75
|
+
const labels = control.axisLabels ?? ["x", "y", "z", "w"];
|
|
76
|
+
return (_jsx("div", { className: "flex flex-wrap items-center gap-1", children: axes.map((axis, index) => (_jsxs("label", { className: "flex items-center gap-0.5 text-[10px] text-neutral-400", children: [_jsx("span", { children: labels[index] }), _jsx("input", { type: "number", className: "w-14 rounded-md border border-white/10 bg-black/40 px-1 py-0.5 font-mono text-neutral-100", min: control.axisMin?.[index], max: control.axisMax?.[index], step: control.axisStep?.[index] ?? control.step, value: axis, onChange: (event) => {
|
|
77
|
+
const next = Number(event.target.value);
|
|
78
|
+
if (Number.isNaN(next))
|
|
79
|
+
return;
|
|
80
|
+
const copy = axes.slice();
|
|
81
|
+
copy[index] = next;
|
|
82
|
+
write(copy);
|
|
83
|
+
} })] }, labels[index] ?? index))) }));
|
|
84
|
+
}
|
|
85
|
+
if (control.kind === "interval") {
|
|
86
|
+
const interval = value !== null && typeof value === "object" && !Array.isArray(value)
|
|
87
|
+
? value
|
|
88
|
+
: { min: 0, max: 0 };
|
|
89
|
+
const writeInterval = (min, max) => write({ min, max });
|
|
90
|
+
return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { type: "number", className: "w-14 rounded-md border border-white/10 bg-black/40 px-1 py-0.5 font-mono text-neutral-100", min: control.min, max: control.max, step: control.step, value: interval.min, onChange: (event) => {
|
|
91
|
+
const next = Number(event.target.value);
|
|
92
|
+
if (!Number.isNaN(next))
|
|
93
|
+
writeInterval(next, interval.max);
|
|
94
|
+
} }), _jsx("span", { className: "text-neutral-500", children: "\u2026" }), _jsx("input", { type: "number", className: "w-14 rounded-md border border-white/10 bg-black/40 px-1 py-0.5 font-mono text-neutral-100", min: control.min, max: control.max, step: control.step, value: interval.max, onChange: (event) => {
|
|
95
|
+
const next = Number(event.target.value);
|
|
96
|
+
if (!Number.isNaN(next))
|
|
97
|
+
writeInterval(interval.min, next);
|
|
98
|
+
} })] }));
|
|
99
|
+
}
|
|
100
|
+
return (_jsx("input", { type: "text", className: "w-32 rounded-md border border-white/10 bg-black/40 px-1 py-0.5 font-mono text-neutral-100", value: String(value), onChange: (event) => write(event.target.value) }));
|
|
101
|
+
}
|
|
102
|
+
function formatPreview(value) {
|
|
103
|
+
if (typeof value === "number")
|
|
104
|
+
return String(Math.round(value * 1000) / 1000);
|
|
105
|
+
if (Array.isArray(value))
|
|
106
|
+
return value.map((entry) => formatPreview(entry)).join(", ");
|
|
107
|
+
if (value !== null && typeof value === "object") {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.stringify(value);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return String(value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return String(value);
|
|
116
|
+
}
|
|
117
|
+
const TUNABLE_ACRONYMS = new Set([
|
|
118
|
+
"fov",
|
|
119
|
+
"fps",
|
|
120
|
+
"ms",
|
|
121
|
+
"ui",
|
|
122
|
+
"ai",
|
|
123
|
+
"id",
|
|
124
|
+
"uv",
|
|
125
|
+
"rgb",
|
|
126
|
+
"rgba",
|
|
127
|
+
"hp",
|
|
128
|
+
"mp",
|
|
129
|
+
"xp",
|
|
130
|
+
"npc",
|
|
131
|
+
"pvp",
|
|
132
|
+
"pve",
|
|
133
|
+
"rts",
|
|
134
|
+
"hud",
|
|
135
|
+
"lod",
|
|
136
|
+
"gpu",
|
|
137
|
+
"cpu",
|
|
138
|
+
]);
|
|
139
|
+
function humanizeTunableSegment(segment) {
|
|
140
|
+
if (/^\d+$/.test(segment))
|
|
141
|
+
return `#${segment}`;
|
|
142
|
+
return segment
|
|
143
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
144
|
+
.replace(/[_-]+/g, " ")
|
|
145
|
+
.split(/\s+/)
|
|
146
|
+
.filter((part) => part.length > 0)
|
|
147
|
+
.map((part) => {
|
|
148
|
+
const lower = part.toLowerCase();
|
|
149
|
+
if (TUNABLE_ACRONYMS.has(lower))
|
|
150
|
+
return lower.toUpperCase();
|
|
151
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
152
|
+
})
|
|
153
|
+
.join(" ");
|
|
154
|
+
}
|
|
155
|
+
function tunableGroupTitle(table, key) {
|
|
156
|
+
const dot = key.lastIndexOf(".");
|
|
157
|
+
if (dot < 0)
|
|
158
|
+
return humanizeTunableSegment(table);
|
|
159
|
+
const parent = key.slice(0, dot);
|
|
160
|
+
const parts = parent.split(".").map(humanizeTunableSegment);
|
|
161
|
+
return `${humanizeTunableSegment(table)} · ${parts.join(" · ")}`;
|
|
162
|
+
}
|
|
163
|
+
function tunableRowLabel(key) {
|
|
164
|
+
const leaf = key.includes(".") ? key.slice(key.lastIndexOf(".") + 1) : key;
|
|
165
|
+
return humanizeTunableSegment(leaf);
|
|
166
|
+
}
|
|
167
|
+
function tunableGroupKey(table, key) {
|
|
168
|
+
const dot = key.lastIndexOf(".");
|
|
169
|
+
return dot < 0 ? table : `${table}/${key.slice(0, dot)}`;
|
|
170
|
+
}
|
|
171
|
+
function deltaSnippet(discovered) {
|
|
172
|
+
const overrides = devtools.overrides.export();
|
|
173
|
+
if (Object.keys(overrides.values).length === 0)
|
|
174
|
+
return null;
|
|
175
|
+
const discoveredById = new Map(discovered.map((entry) => [entry.id, entry]));
|
|
176
|
+
const tables = new Map();
|
|
177
|
+
const flat = {};
|
|
178
|
+
for (const [name, value] of Object.entries(overrides.values)) {
|
|
179
|
+
const entry = discoveredById.get(name);
|
|
180
|
+
if (entry !== undefined) {
|
|
181
|
+
const values = tables.get(entry.table) ?? {};
|
|
182
|
+
values[entry.key] = value;
|
|
183
|
+
tables.set(entry.table, values);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
flat[name] = value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const parts = [];
|
|
190
|
+
for (const [table, values] of tables)
|
|
191
|
+
parts.push(`${table}: ${JSON.stringify(values, null, 2)}`);
|
|
192
|
+
if (Object.keys(flat).length > 0)
|
|
193
|
+
parts.push(`controls: ${JSON.stringify(flat, null, 2)}`);
|
|
194
|
+
return parts.join("\n\n");
|
|
195
|
+
}
|
|
196
|
+
function tunableDeltas(discovered) {
|
|
197
|
+
const overrides = devtools.overrides.export();
|
|
198
|
+
const discoveredById = new Map(discovered.map((entry) => [entry.id, entry]));
|
|
199
|
+
const deltas = [];
|
|
200
|
+
for (const [name, value] of Object.entries(overrides.values)) {
|
|
201
|
+
const entry = discoveredById.get(name);
|
|
202
|
+
if (entry !== undefined)
|
|
203
|
+
deltas.push({ table: entry.table, key: entry.key, value });
|
|
204
|
+
}
|
|
205
|
+
return deltas;
|
|
206
|
+
}
|
|
207
|
+
function SaveToSourceButton({ discovered }) {
|
|
208
|
+
const [state, setState] = useState("idle");
|
|
209
|
+
const [detail, setDetail] = useState(null);
|
|
210
|
+
const endpoint = getSaveEndpoint();
|
|
211
|
+
if (endpoint === null)
|
|
212
|
+
return null;
|
|
213
|
+
const deltas = tunableDeltas(discovered);
|
|
214
|
+
const save = () => {
|
|
215
|
+
if (deltas.length === 0 || state === "saving")
|
|
216
|
+
return;
|
|
217
|
+
setState("saving");
|
|
218
|
+
void fetch(endpoint.url, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers: { "content-type": "application/json" },
|
|
221
|
+
body: JSON.stringify({ kind: "tunables", gameId: endpoint.gameId, deltas }),
|
|
222
|
+
})
|
|
223
|
+
.then(async (response) => {
|
|
224
|
+
const result = (await response.json());
|
|
225
|
+
if (!result.ok) {
|
|
226
|
+
setState("error");
|
|
227
|
+
setDetail(result.error ?? "save failed");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const skipped = result.skipped ?? [];
|
|
231
|
+
if (skipped.length > 0) {
|
|
232
|
+
setState("partial");
|
|
233
|
+
setDetail(skipped.map((entry) => `${entry.table}/${entry.key}: ${entry.reason}`).join("\n"));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
setState("saved");
|
|
237
|
+
setDetail(null);
|
|
238
|
+
}
|
|
239
|
+
setTimeout(() => setState("idle"), 2500);
|
|
240
|
+
})
|
|
241
|
+
.catch((error) => {
|
|
242
|
+
setState("error");
|
|
243
|
+
setDetail(error instanceof Error ? error.message : String(error));
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
return (_jsx("button", { type: "button", disabled: deltas.length === 0, className: "rounded-md bg-cyan-500/15 px-2 py-0.5 text-cyan-200 ring-1 ring-inset ring-cyan-400/25 transition-colors hover:bg-cyan-500/25 disabled:opacity-40 disabled:hover:bg-cyan-500/15", onClick: save, title: detail ?? "Write changed values back into the game's source files", children: state === "saving"
|
|
247
|
+
? "Saving…"
|
|
248
|
+
: state === "saved"
|
|
249
|
+
? "Saved to source"
|
|
250
|
+
: state === "partial"
|
|
251
|
+
? "Saved (some skipped)"
|
|
252
|
+
: state === "error"
|
|
253
|
+
? "Save failed"
|
|
254
|
+
: `Save to source${deltas.length > 0 ? ` (${deltas.length})` : ""}` }));
|
|
255
|
+
}
|
|
256
|
+
export function TunePanel({ gameName }) {
|
|
257
|
+
const [deltasCopied, setDeltasCopied] = useState(false);
|
|
258
|
+
const [query, setQuery] = useState("");
|
|
259
|
+
const controls = devtools.controls.list();
|
|
260
|
+
const allDiscovered = devtools.discover.list();
|
|
261
|
+
const persist = () => persistDevtoolsOverrides(gameName);
|
|
262
|
+
const discoveredIds = new Set(allDiscovered.map((entry) => entry.id));
|
|
263
|
+
const needle = query.trim().toLowerCase();
|
|
264
|
+
const matches = (id) => needle === "" || id.toLowerCase().includes(needle);
|
|
265
|
+
const discovered = allDiscovered.filter((entry) => entry.enabled || matches(entry.id));
|
|
266
|
+
const explicit = controls.filter((control) => !discoveredIds.has(control.name) && matches(control.name));
|
|
267
|
+
const controlByName = new Map(controls.map((control) => [control.name, control]));
|
|
268
|
+
const snippet = deltaSnippet(allDiscovered);
|
|
269
|
+
const copyDeltas = () => {
|
|
270
|
+
if (snippet === null)
|
|
271
|
+
return;
|
|
272
|
+
const clipboard = navigator.clipboard;
|
|
273
|
+
if (clipboard !== undefined) {
|
|
274
|
+
void clipboard.writeText(snippet).catch(() => console.log(snippet));
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
console.log(snippet);
|
|
278
|
+
}
|
|
279
|
+
setDeltasCopied(true);
|
|
280
|
+
setTimeout(() => setDeltasCopied(false), 1500);
|
|
281
|
+
};
|
|
282
|
+
if (controls.length === 0 && allDiscovered.length === 0) {
|
|
283
|
+
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", {}), "Nested numbers/booleans/colors auto-discover. Schema kinds:", _jsx("br", {}), "vec2/3/4 · interval · angle · enum · color+alpha via tunable() or scan meta."] })] }));
|
|
284
|
+
}
|
|
285
|
+
const groups = new Map();
|
|
286
|
+
for (const entry of discovered) {
|
|
287
|
+
const groupKey = tunableGroupKey(entry.table, entry.key);
|
|
288
|
+
const existing = groups.get(groupKey);
|
|
289
|
+
if (existing !== undefined) {
|
|
290
|
+
existing.entries.push(entry);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
groups.set(groupKey, { title: tunableGroupTitle(entry.table, entry.key), entries: [entry] });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
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-md bg-white/[0.04] px-2 py-0.5 text-neutral-300 ring-1 ring-inset ring-white/[0.08] transition-colors hover:bg-white/10", onClick: () => {
|
|
297
|
+
devtools.controls.resetAll();
|
|
298
|
+
persist();
|
|
299
|
+
}, children: "Reset all" }), _jsx("button", { type: "button", disabled: snippet === null, className: "rounded-md bg-white/[0.04] px-2 py-0.5 text-neutral-300 ring-1 ring-inset ring-white/[0.08] transition-colors hover:bg-white/10 disabled:opacity-40 disabled:hover:bg-transparent", onClick: copyDeltas, title: "Copy changed values as source snippets to paste upstream", children: deltasCopied ? "Copied" : "Copy deltas" }), _jsx(SaveToSourceButton, { discovered: allDiscovered }), _jsx("input", { type: "text", value: query, onChange: (event) => setQuery(event.target.value), placeholder: "filter\u2026", className: "min-w-0 flex-1 rounded-md border border-white/10 bg-black/30 px-2 py-0.5 text-neutral-200 transition-colors placeholder:text-neutral-600 focus:border-cyan-400/60 focus:outline-none" })] }), explicit.length === 0 && discovered.length === 0 ? (_jsxs("div", { className: "text-neutral-500", children: ["No tunables match \u201C", query, "\u201D."] })) : null, 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.includes(".") ? tunableRowLabel(control.label) : control.label }), _jsx(ControlInput, { control: control, onWrite: persist })] }, control.name)))] })) : null, [...groups.entries()].map(([groupKey, group]) => (_jsxs("div", { className: "space-y-1.5", children: [_jsx("div", { className: "text-[9px] uppercase tracking-wide text-neutral-500", children: group.title }), group.entries.map((entry) => {
|
|
300
|
+
const control = entry.enabled ? controlByName.get(entry.id) : undefined;
|
|
301
|
+
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-cyan-400", checked: entry.enabled, onChange: (event) => {
|
|
302
|
+
if (event.target.checked)
|
|
303
|
+
devtools.discover.enable(entry.id);
|
|
304
|
+
else
|
|
305
|
+
devtools.discover.disable(entry.id);
|
|
306
|
+
persist();
|
|
307
|
+
} }), _jsx("span", { className: "truncate", children: tunableRowLabel(entry.key) })] }), control !== undefined ? (_jsx(ControlInput, { control: control, onWrite: persist })) : (_jsx("span", { className: "font-mono text-neutral-500", children: formatPreview(entry.read()) }))] }, entry.id));
|
|
308
|
+
})] }, groupKey)))] }));
|
|
309
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PlayableGame } from "../registry.js";
|
|
2
|
+
/** One RPC call into the agent bridge: a verb name plus its verb-specific fields. */
|
|
3
|
+
export type AgentBridgeRequest = {
|
|
4
|
+
method: string;
|
|
5
|
+
} & Record<string, unknown>;
|
|
6
|
+
/** Result of an agent bridge call: `ok: true` with verb payload, or `ok: false` with a reason. */
|
|
7
|
+
export type AgentBridgeResponse = {
|
|
8
|
+
ok: true;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
type EditorHost = {
|
|
15
|
+
gameId: string;
|
|
16
|
+
handle(request: AgentBridgeRequest): unknown;
|
|
17
|
+
};
|
|
18
|
+
declare global {
|
|
19
|
+
interface Window {
|
|
20
|
+
__jgengineAgent?: {
|
|
21
|
+
handle(request: AgentBridgeRequest): unknown;
|
|
22
|
+
};
|
|
23
|
+
__jgengineEditorHost?: EditorHost;
|
|
24
|
+
__jgengineSummonEditor?: () => void;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* @internal
|
|
29
|
+
* Headless control plane spanning the F2 chord family: debug mode (F2+D),
|
|
30
|
+
* canvas mode (F2+C), and editor mode (F2+E). Installed on every
|
|
31
|
+
* GamePlayerShell mount as `window.__jgengineAgent`; unknown verbs delegate
|
|
32
|
+
* to the editor host when one is live, so a single RPC surface covers all
|
|
33
|
+
* three modes for `bun run drive --rpc`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function installAgentBridge(options: {
|
|
36
|
+
playable: PlayableGame;
|
|
37
|
+
devtoolsEnabled: boolean;
|
|
38
|
+
isDevtoolsOpen: () => boolean;
|
|
39
|
+
setDevtoolsOpen: (open: boolean) => void;
|
|
40
|
+
}): () => void;
|
|
41
|
+
export {};
|