@jgengine/shell 0.10.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 +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- 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/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -12
- package/dist/GamePlayerShell.js +67 -1332
- package/dist/GameUiPreview.js +3 -2
- 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 +128 -21
- package/dist/audio/audioWire.d.ts +40 -0
- package/dist/audio/audioWire.js +62 -0
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -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 +8 -3
- 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/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- 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 +114 -24
- package/dist/camera/rigMath.js +129 -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/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +50 -9
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
- package/dist/devtools/DevtoolsOverlay.js +47 -462
- 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 +266 -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 +36 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +80 -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 +44 -9
- 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/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/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/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +56 -11
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +4 -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/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -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 +76 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +54 -3
- package/dist/render/resolveModel.js +116 -4
- 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 +135 -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 +118 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +84 -0
- package/dist/scene/AuthoredScene.js +178 -0
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -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 +73 -0
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +84 -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 +61 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +26 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +30 -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 +7 -1
- package/dist/settings/appliedSettings.js +13 -2
- 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 +53 -0
- package/dist/structures/TransformGizmo.js +59 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- 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 +35 -0
- package/dist/terrain/grassGeometry.js +85 -20
- package/dist/terrain/grassMaterial.d.ts +20 -0
- package/dist/terrain/grassMaterial.js +68 -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/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +23 -0
- package/dist/terrain/soilPatchMaterial.js +147 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +87 -7
- 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/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +11 -0
- package/dist/water/OceanConfig.js +8 -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/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/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -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 +186 -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 +11 -0
- package/dist/worldSky.js +21 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- 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 +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +51 -0
- package/skills/jgengine-world/api.md +3056 -0
- package/skills/jgengine-world/capabilities.md +476 -0
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +261 -0
- 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.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/llms.txt +0 -2761
|
@@ -1,67 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useFrame } from "@react-three/fiber";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { devtools, instrumentLatency, } from "@jgengine/core/devtools/devtools";
|
|
5
|
+
import { fallbackSeamsSnapshot } from "@jgengine/core/devtools/fallbackSeams";
|
|
6
6
|
import { MOVEMENT_TUNING } from "@jgengine/core/movement/movementModel";
|
|
7
|
-
import {
|
|
7
|
+
import { collisionDebug } from "./collisionDebug.js";
|
|
8
|
+
import { readStoredOverrides } from "./devtoolsOverrides.js";
|
|
9
|
+
import { diagnose } from "./perfDiagnose.js";
|
|
10
|
+
import { PerfPanel } from "./PerfPanel.js";
|
|
11
|
+
import { LogsPanel } from "./LogsPanel.js";
|
|
12
|
+
import { NetPanel } from "./NetPanel.js";
|
|
13
|
+
import { KeysPanel } from "./KeysPanel.js";
|
|
14
|
+
import { ColPanel } from "./ColPanel.js";
|
|
15
|
+
import { TunePanel } from "./TunePanel.js";
|
|
16
|
+
export { applyStoredDevtoolsOverrides, persistDevtoolsOverrides } from "./devtoolsOverrides.js";
|
|
8
17
|
const REFRESH_MS = 250;
|
|
9
|
-
|
|
10
|
-
const CULPRIT_HINTS = {
|
|
11
|
-
"outside-sim": "Cost is outside the sim driver — three.js render, React commit, GPU, GC, or tab throttle.",
|
|
12
|
-
sim: "Sim is slow but no named phase stands out — wrap hot onTick work with measure(\"name\", fn).",
|
|
13
|
-
onTick: "Game loop.onTick is the hotspot — profile inside it with measure(\"physics\"|\"ai\"|…, fn).",
|
|
14
|
-
pose: "Shell movement / collision / voxel step is expensive.",
|
|
15
|
-
actions: "Input → command dispatch / hotbar / interact is expensive.",
|
|
16
|
-
presence: "Multiplayer pose sync is expensive.",
|
|
17
|
-
pickup: "Auto-pickup nearest scan is expensive.",
|
|
18
|
-
"time+input": "Clock advance or input publish is unexpectedly heavy.",
|
|
19
|
-
};
|
|
20
|
-
function overridesStorageKey(gameName) {
|
|
21
|
-
return `jg-devtools:${gameName}`;
|
|
22
|
-
}
|
|
23
|
-
function readStoredOverrides(gameName) {
|
|
24
|
-
try {
|
|
25
|
-
const raw = localStorage.getItem(overridesStorageKey(gameName));
|
|
26
|
-
if (raw === null)
|
|
27
|
-
return null;
|
|
28
|
-
const parsed = parseOverridesPayload(JSON.parse(raw));
|
|
29
|
-
if (parsed.overrides === null) {
|
|
30
|
-
for (const message of parsed.diagnostics)
|
|
31
|
-
console.warn(`[jgengine:devtools] ${message}`);
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
for (const message of parsed.diagnostics)
|
|
35
|
-
console.info(`[jgengine:devtools] ${message}`);
|
|
36
|
-
return parsed.overrides;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
export function persistDevtoolsOverrides(gameName) {
|
|
43
|
-
const overrides = devtools.overrides.export();
|
|
44
|
-
try {
|
|
45
|
-
localStorage.setItem(overridesStorageKey(gameName), JSON.stringify(overrides));
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return overrides;
|
|
49
|
-
}
|
|
50
|
-
return overrides;
|
|
51
|
-
}
|
|
52
|
-
export function applyStoredDevtoolsOverrides(gameName) {
|
|
53
|
-
const stored = readStoredOverrides(gameName);
|
|
54
|
-
if (stored === null)
|
|
55
|
-
return;
|
|
56
|
-
const result = devtools.overrides.apply(stored);
|
|
57
|
-
if (result.applied === 0 && result.skipped.length === 0)
|
|
58
|
-
return;
|
|
59
|
-
console.info(`[jgengine:devtools] applied ${result.applied} stored override(s) for ${gameName}` +
|
|
60
|
-
(result.skipped.length > 0 ? ` · skipped ${result.skipped.length}` : ""));
|
|
61
|
-
for (const entry of result.skipped) {
|
|
62
|
-
console.warn(`[jgengine:devtools] skipped override ${entry.id}: ${entry.reason}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
18
|
+
/** @internal */
|
|
65
19
|
export function withDevtoolsLatency(multiplayer) {
|
|
66
20
|
return {
|
|
67
21
|
...multiplayer,
|
|
@@ -71,45 +25,37 @@ export function withDevtoolsLatency(multiplayer) {
|
|
|
71
25
|
},
|
|
72
26
|
};
|
|
73
27
|
}
|
|
28
|
+
const RENDER_SAMPLE_MS = 500;
|
|
29
|
+
const RENDER_PROBE_FRAME_PRIORITY = -10;
|
|
30
|
+
/** @internal */
|
|
74
31
|
export function DevtoolsRendererProbe() {
|
|
75
|
-
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]);
|
|
76
43
|
useFrame((state) => {
|
|
77
|
-
frameCounter.current += 1;
|
|
78
|
-
if (frameCounter.current % 30 !== 0)
|
|
79
|
-
return;
|
|
80
44
|
const info = state.gl.info;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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);
|
|
88
57
|
return null;
|
|
89
58
|
}
|
|
90
|
-
function keybindRows(input) {
|
|
91
|
-
const rows = [];
|
|
92
|
-
for (const [action, codes] of Object.entries(input ?? {})) {
|
|
93
|
-
const entries = flattenCodes(codes);
|
|
94
|
-
for (const entry of entries)
|
|
95
|
-
rows.push({ action, ...entry });
|
|
96
|
-
}
|
|
97
|
-
return rows;
|
|
98
|
-
}
|
|
99
|
-
function flattenCodes(codes) {
|
|
100
|
-
if (Array.isArray(codes)) {
|
|
101
|
-
return [{ codes: codes.map(bindingLabel).join(" / "), mode: "press" }];
|
|
102
|
-
}
|
|
103
|
-
const modes = codes;
|
|
104
|
-
const result = [];
|
|
105
|
-
if (modes.hold !== undefined && modes.hold.length > 0) {
|
|
106
|
-
result.push({ codes: modes.hold.map(bindingLabel).join(" / "), mode: "hold" });
|
|
107
|
-
}
|
|
108
|
-
if (modes.toggle !== undefined && modes.toggle.length > 0) {
|
|
109
|
-
result.push({ codes: modes.toggle.map(bindingLabel).join(" / "), mode: "toggle" });
|
|
110
|
-
}
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
59
|
const TABS = [
|
|
114
60
|
{ id: "perf", label: "Perf" },
|
|
115
61
|
{ id: "tune", label: "Tune" },
|
|
@@ -118,371 +64,6 @@ const TABS = [
|
|
|
118
64
|
{ id: "net", label: "Net" },
|
|
119
65
|
{ id: "keys", label: "Keys" },
|
|
120
66
|
];
|
|
121
|
-
function ms(value) {
|
|
122
|
-
return `${value.toFixed(1)}ms`;
|
|
123
|
-
}
|
|
124
|
-
function StatRow({ name, value, alert }) {
|
|
125
|
-
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 })] }));
|
|
126
|
-
}
|
|
127
|
-
function FrameBars({ frames }) {
|
|
128
|
-
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)}%` }, title: `${frameMs.toFixed(1)}ms` }, index))) }));
|
|
129
|
-
}
|
|
130
|
-
function SectionLabel({ children }) {
|
|
131
|
-
return _jsx("div", { className: "text-[9px] uppercase tracking-wide text-neutral-500", children: children });
|
|
132
|
-
}
|
|
133
|
-
function PhaseBars({ phases, avgSimMs }) {
|
|
134
|
-
if (phases.length === 0) {
|
|
135
|
-
return _jsx("div", { className: "text-neutral-500", children: "No phase samples yet." });
|
|
136
|
-
}
|
|
137
|
-
const budget = Math.max(PHASE_BAR_BUDGET_MS, avgSimMs, ...phases.map((phase) => phase.avgMs));
|
|
138
|
-
return (_jsx("div", { className: "space-y-1", children: phases.slice(0, 8).map((phase) => {
|
|
139
|
-
const hot = phase.avgMs > 4 || phase.maxMs > 8;
|
|
140
|
-
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-neutral-800", children: _jsx("div", { className: `h-full rounded-full ${hot ? "bg-amber-400" : "bg-emerald-500/80"}`, style: { width: `${Math.min(100, (phase.avgMs / budget) * 100)}%` } }) })] }, phase.name));
|
|
141
|
-
}) }));
|
|
142
|
-
}
|
|
143
|
-
function BudgetSplit({ simMs, outsideMs }) {
|
|
144
|
-
const total = Math.max(1e-6, simMs + outsideMs);
|
|
145
|
-
const simPct = (simMs / total) * 100;
|
|
146
|
-
const outsideHot = outsideMs > simMs && outsideMs > 4;
|
|
147
|
-
const simHot = simMs > 8;
|
|
148
|
-
return (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex h-2 overflow-hidden rounded-full bg-neutral-800", 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" })] }));
|
|
149
|
-
}
|
|
150
|
-
function diagnose(frame, longs) {
|
|
151
|
-
if (frame.fps >= 55 && frame.longFrames === 0 && longs.length === 0)
|
|
152
|
-
return null;
|
|
153
|
-
const recent = longs.slice(-5);
|
|
154
|
-
const culpritCounts = new Map();
|
|
155
|
-
for (const event of recent) {
|
|
156
|
-
culpritCounts.set(event.culprit, (culpritCounts.get(event.culprit) ?? 0) + 1);
|
|
157
|
-
}
|
|
158
|
-
let topCulprit = null;
|
|
159
|
-
let topCount = 0;
|
|
160
|
-
for (const [name, count] of culpritCounts) {
|
|
161
|
-
if (count > topCount) {
|
|
162
|
-
topCulprit = name;
|
|
163
|
-
topCount = count;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (topCulprit === null && frame.phases[0] !== undefined && frame.avgSimMs > 8) {
|
|
167
|
-
topCulprit = frame.phases[0].name;
|
|
168
|
-
}
|
|
169
|
-
if (topCulprit === null && frame.avgOutsideMs > frame.avgSimMs && frame.avgOutsideMs > 4) {
|
|
170
|
-
topCulprit = "outside-sim";
|
|
171
|
-
}
|
|
172
|
-
if (topCulprit === null) {
|
|
173
|
-
if (frame.p95FrameMs > LONG_FRAME_MS)
|
|
174
|
-
return `p95 ${ms(frame.p95FrameMs)} — hitching without a clear phase; check long-frame log as it fills.`;
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
const hint = CULPRIT_HINTS[topCulprit] ?? `Hotspot “${topCulprit}” — dig into that phase with measure() or reduce its work.`;
|
|
178
|
-
const countText = topCount > 0 ? ` (${topCount}/${recent.length || topCount} recent long frames)` : "";
|
|
179
|
-
return `${topCulprit}${countText}: ${hint}`;
|
|
180
|
-
}
|
|
181
|
-
function LongFrameList({ events }) {
|
|
182
|
-
if (events.length === 0) {
|
|
183
|
-
return _jsxs("div", { className: "text-neutral-500", children: ["No long frames (>", ms(LONG_FRAME_MS), ") captured yet."] });
|
|
184
|
-
}
|
|
185
|
-
const newest = [...events].reverse().slice(0, 12);
|
|
186
|
-
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 border border-neutral-800 bg-neutral-900/60 px-1.5 py-1", 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
|
|
187
|
-
.slice(0, 4)
|
|
188
|
-
.map((phase) => `${phase.name} ${phase.ms.toFixed(1)}`)
|
|
189
|
-
.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}`))) }));
|
|
190
|
-
}
|
|
191
|
-
function PerfPanel({ ctx }) {
|
|
192
|
-
const rateRef = useRef({ version: 0, at: 0, perSecond: 0 });
|
|
193
|
-
const frame = devtools.frame.stats();
|
|
194
|
-
const render = devtools.render.latest();
|
|
195
|
-
const longs = devtools.frame.longFrames();
|
|
196
|
-
const now = performance.now();
|
|
197
|
-
const rate = rateRef.current;
|
|
198
|
-
if (rate.at === 0) {
|
|
199
|
-
rateRef.current = { version: ctx.version(), at: now, perSecond: 0 };
|
|
200
|
-
}
|
|
201
|
-
else if (now - rate.at >= 900) {
|
|
202
|
-
const perSecond = ((ctx.version() - rate.version) / (now - rate.at)) * 1000;
|
|
203
|
-
rateRef.current = { version: ctx.version(), at: now, perSecond };
|
|
204
|
-
}
|
|
205
|
-
const diagnosis = frame !== null ? diagnose(frame, longs) : null;
|
|
206
|
-
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" })] }));
|
|
207
|
-
}
|
|
208
|
-
function LogsPanel() {
|
|
209
|
-
const entries = devtools.logs.list().slice(-60);
|
|
210
|
-
const colors = {
|
|
211
|
-
error: "text-red-400",
|
|
212
|
-
warn: "text-amber-300",
|
|
213
|
-
info: "text-sky-300",
|
|
214
|
-
log: "text-neutral-300",
|
|
215
|
-
};
|
|
216
|
-
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)))] })] }));
|
|
217
|
-
}
|
|
218
|
-
function NetPanel({ multiplayer }) {
|
|
219
|
-
const stats = devtools.latency.stats();
|
|
220
|
-
if (multiplayer === null) {
|
|
221
|
-
return _jsx("div", { className: "text-neutral-400", children: "Offline \u2014 no multiplayer backend attached." });
|
|
222
|
-
}
|
|
223
|
-
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) })] }))] }));
|
|
224
|
-
}
|
|
225
|
-
function KeysPanel({ input }) {
|
|
226
|
-
const rows = keybindRows(input);
|
|
227
|
-
if (rows.length === 0)
|
|
228
|
-
return _jsx("div", { className: "text-neutral-400", children: "This game declares no keybinds." });
|
|
229
|
-
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))) }));
|
|
230
|
-
}
|
|
231
|
-
const LAYER_LABELS = {
|
|
232
|
-
hitboxes: "Damage hitboxes",
|
|
233
|
-
bodies: "Physical bodies",
|
|
234
|
-
projectiles: "Projectile paths",
|
|
235
|
-
muzzles: "Muzzle / shot origins",
|
|
236
|
-
aimLaser: "Aim laser (authoritative)",
|
|
237
|
-
};
|
|
238
|
-
function ColPanel() {
|
|
239
|
-
const state = useSyncExternalStore(collisionDebug.subscribe, () => collisionDebug.getState(), () => collisionDebug.getState());
|
|
240
|
-
return (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "text-[10px] text-neutral-500", children: "F2 world collision debugger \u00B7 layers stay on when panel closes \u00B7 zero cost when all off" }), _jsxs("div", { className: "flex gap-1", children: [_jsx("button", { type: "button", className: "rounded border border-neutral-600 px-2 py-0.5 text-neutral-300 hover:bg-neutral-800", onClick: () => collisionDebug.setAllLayers(true), children: "All on" }), _jsx("button", { type: "button", className: "rounded border border-neutral-600 px-2 py-0.5 text-neutral-300 hover:bg-neutral-800", onClick: () => collisionDebug.setAllLayers(false), children: "All off" })] }), _jsx("div", { className: "space-y-1", children: COLLISION_DEBUG_LAYERS.map((layer) => (_jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-neutral-200", children: [_jsx("input", { type: "checkbox", checked: state.layers[layer], onChange: (event) => collisionDebug.setLayer(layer, event.target.checked) }), _jsx("span", { children: LAYER_LABELS[layer] })] }, layer))) }), _jsx("div", { className: "border-t border-neutral-800 pt-1.5 font-mono text-[9px] text-neutral-500", children: "hitbox pink \u00B7 body cyan \u00B7 muzzle red \u00B7 laser lime \u00B7 X damage \u00B7 \u25CB solid \u00B7 \u00B7 miss" })] }));
|
|
241
|
-
}
|
|
242
|
-
function ControlInput({ control, onWrite }) {
|
|
243
|
-
const value = control.read();
|
|
244
|
-
const write = (next) => {
|
|
245
|
-
if (control.write(next))
|
|
246
|
-
onWrite();
|
|
247
|
-
};
|
|
248
|
-
if (control.kind === "slider" || control.kind === "angle") {
|
|
249
|
-
const unit = control.unit ?? "rad";
|
|
250
|
-
const displayUnit = control.displayUnit ?? (control.kind === "angle" ? "deg" : unit);
|
|
251
|
-
const displayValue = control.kind === "angle" && typeof value === "number"
|
|
252
|
-
? convertAngle(value, unit, displayUnit)
|
|
253
|
-
: Number(value);
|
|
254
|
-
const displayMin = control.min !== undefined && control.kind === "angle"
|
|
255
|
-
? convertAngle(control.min, unit, displayUnit)
|
|
256
|
-
: control.min;
|
|
257
|
-
const displayMax = control.max !== undefined && control.kind === "angle"
|
|
258
|
-
? convertAngle(control.max, unit, displayUnit)
|
|
259
|
-
: control.max;
|
|
260
|
-
const displayStep = control.step !== undefined && control.kind === "angle" && unit !== displayUnit
|
|
261
|
-
? convertAngle(control.step, unit, displayUnit)
|
|
262
|
-
: control.step;
|
|
263
|
-
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "range", className: "h-1 w-28 accent-emerald-400", min: displayMin, max: displayMax, step: displayStep, value: displayValue, onChange: (event) => {
|
|
264
|
-
const next = Number(event.target.value);
|
|
265
|
-
write(control.kind === "angle" ? convertAngle(next, displayUnit, unit) : next);
|
|
266
|
-
} }), _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: displayStep, value: Number(displayValue.toFixed(4)), onChange: (event) => {
|
|
267
|
-
const next = Number(event.target.value);
|
|
268
|
-
if (!Number.isNaN(next)) {
|
|
269
|
-
write(control.kind === "angle" ? convertAngle(next, displayUnit, unit) : next);
|
|
270
|
-
}
|
|
271
|
-
} }), control.kind === "angle" ? (_jsx("span", { className: "text-[10px] text-neutral-500", children: displayUnit })) : null] }));
|
|
272
|
-
}
|
|
273
|
-
if (control.kind === "toggle") {
|
|
274
|
-
return (_jsx("input", { type: "checkbox", className: "h-4 w-4 accent-emerald-400", checked: Boolean(value), onChange: (event) => write(event.target.checked) }));
|
|
275
|
-
}
|
|
276
|
-
if (control.kind === "color") {
|
|
277
|
-
const parsed = parseColor(value);
|
|
278
|
-
const rgb = parsed?.rgb ?? "#000000";
|
|
279
|
-
const alpha = parsed?.alpha ?? 1;
|
|
280
|
-
const showAlpha = control.hasAlpha === true || parsed?.hasAlpha === true;
|
|
281
|
-
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) => {
|
|
282
|
-
if (showAlpha) {
|
|
283
|
-
const a = Math.round(alpha * 255)
|
|
284
|
-
.toString(16)
|
|
285
|
-
.padStart(2, "0");
|
|
286
|
-
write(`${event.target.value}${a}`);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
write(event.target.value);
|
|
290
|
-
}
|
|
291
|
-
} }), showAlpha ? (_jsx("input", { type: "range", className: "h-1 w-16 accent-emerald-400", min: 0, max: 1, step: 0.01, value: alpha, onChange: (event) => {
|
|
292
|
-
const nextAlpha = Number(event.target.value);
|
|
293
|
-
const a = Math.round(nextAlpha * 255)
|
|
294
|
-
.toString(16)
|
|
295
|
-
.padStart(2, "0");
|
|
296
|
-
write(`${rgb}${a}`);
|
|
297
|
-
} })) : null] }));
|
|
298
|
-
}
|
|
299
|
-
if (control.kind === "select" || control.kind === "enum") {
|
|
300
|
-
const choices = control.choices ??
|
|
301
|
-
control.options?.map((option) => ({ value: option, label: String(option) })) ??
|
|
302
|
-
[];
|
|
303
|
-
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) => {
|
|
304
|
-
const match = choices.find((choice) => String(choice.value) === event.target.value);
|
|
305
|
-
if (match !== undefined)
|
|
306
|
-
write(match.value);
|
|
307
|
-
}, children: choices.map((choice) => (_jsx("option", { value: String(choice.value), children: choice.label ?? String(choice.value) }, String(choice.value)))) }));
|
|
308
|
-
}
|
|
309
|
-
if (control.kind === "vec2" || control.kind === "vec3" || control.kind === "vec4") {
|
|
310
|
-
const axes = Array.isArray(value) ? value : [];
|
|
311
|
-
const labels = control.axisLabels ?? ["x", "y", "z", "w"];
|
|
312
|
-
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 border border-neutral-600 bg-neutral-900 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) => {
|
|
313
|
-
const next = Number(event.target.value);
|
|
314
|
-
if (Number.isNaN(next))
|
|
315
|
-
return;
|
|
316
|
-
const copy = axes.slice();
|
|
317
|
-
copy[index] = next;
|
|
318
|
-
write(copy);
|
|
319
|
-
} })] }, labels[index] ?? index))) }));
|
|
320
|
-
}
|
|
321
|
-
if (control.kind === "interval") {
|
|
322
|
-
const interval = value !== null && typeof value === "object" && !Array.isArray(value)
|
|
323
|
-
? value
|
|
324
|
-
: { min: 0, max: 0 };
|
|
325
|
-
const writeInterval = (min, max) => write({ min, max });
|
|
326
|
-
return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { type: "number", className: "w-14 rounded border border-neutral-600 bg-neutral-900 px-1 py-0.5 font-mono text-neutral-100", min: control.min, max: control.max, step: control.step, value: interval.min, onChange: (event) => {
|
|
327
|
-
const next = Number(event.target.value);
|
|
328
|
-
if (!Number.isNaN(next))
|
|
329
|
-
writeInterval(next, interval.max);
|
|
330
|
-
} }), _jsx("span", { className: "text-neutral-500", children: "\u2026" }), _jsx("input", { type: "number", className: "w-14 rounded border border-neutral-600 bg-neutral-900 px-1 py-0.5 font-mono text-neutral-100", min: control.min, max: control.max, step: control.step, value: interval.max, onChange: (event) => {
|
|
331
|
-
const next = Number(event.target.value);
|
|
332
|
-
if (!Number.isNaN(next))
|
|
333
|
-
writeInterval(interval.min, next);
|
|
334
|
-
} })] }));
|
|
335
|
-
}
|
|
336
|
-
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) }));
|
|
337
|
-
}
|
|
338
|
-
function formatPreview(value) {
|
|
339
|
-
if (typeof value === "number")
|
|
340
|
-
return String(Math.round(value * 1000) / 1000);
|
|
341
|
-
if (Array.isArray(value))
|
|
342
|
-
return value.map((entry) => formatPreview(entry)).join(", ");
|
|
343
|
-
if (value !== null && typeof value === "object") {
|
|
344
|
-
try {
|
|
345
|
-
return JSON.stringify(value);
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
return String(value);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return String(value);
|
|
352
|
-
}
|
|
353
|
-
const TUNABLE_ACRONYMS = new Set([
|
|
354
|
-
"fov",
|
|
355
|
-
"fps",
|
|
356
|
-
"ms",
|
|
357
|
-
"ui",
|
|
358
|
-
"ai",
|
|
359
|
-
"id",
|
|
360
|
-
"uv",
|
|
361
|
-
"rgb",
|
|
362
|
-
"rgba",
|
|
363
|
-
"hp",
|
|
364
|
-
"mp",
|
|
365
|
-
"xp",
|
|
366
|
-
"npc",
|
|
367
|
-
"pvp",
|
|
368
|
-
"pve",
|
|
369
|
-
"rts",
|
|
370
|
-
"hud",
|
|
371
|
-
"lod",
|
|
372
|
-
"gpu",
|
|
373
|
-
"cpu",
|
|
374
|
-
]);
|
|
375
|
-
function humanizeTunableSegment(segment) {
|
|
376
|
-
if (/^\d+$/.test(segment))
|
|
377
|
-
return `#${segment}`;
|
|
378
|
-
return segment
|
|
379
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
380
|
-
.replace(/[_-]+/g, " ")
|
|
381
|
-
.split(/\s+/)
|
|
382
|
-
.filter((part) => part.length > 0)
|
|
383
|
-
.map((part) => {
|
|
384
|
-
const lower = part.toLowerCase();
|
|
385
|
-
if (TUNABLE_ACRONYMS.has(lower))
|
|
386
|
-
return lower.toUpperCase();
|
|
387
|
-
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
388
|
-
})
|
|
389
|
-
.join(" ");
|
|
390
|
-
}
|
|
391
|
-
function tunableGroupTitle(table, key) {
|
|
392
|
-
const dot = key.lastIndexOf(".");
|
|
393
|
-
if (dot < 0)
|
|
394
|
-
return humanizeTunableSegment(table);
|
|
395
|
-
const parent = key.slice(0, dot);
|
|
396
|
-
const parts = parent.split(".").map(humanizeTunableSegment);
|
|
397
|
-
return `${humanizeTunableSegment(table)} · ${parts.join(" · ")}`;
|
|
398
|
-
}
|
|
399
|
-
function tunableRowLabel(key) {
|
|
400
|
-
const leaf = key.includes(".") ? key.slice(key.lastIndexOf(".") + 1) : key;
|
|
401
|
-
return humanizeTunableSegment(leaf);
|
|
402
|
-
}
|
|
403
|
-
function tunableGroupKey(table, key) {
|
|
404
|
-
const dot = key.lastIndexOf(".");
|
|
405
|
-
return dot < 0 ? table : `${table}/${key.slice(0, dot)}`;
|
|
406
|
-
}
|
|
407
|
-
function deltaSnippet(discovered) {
|
|
408
|
-
const overrides = devtools.overrides.export();
|
|
409
|
-
if (Object.keys(overrides.values).length === 0)
|
|
410
|
-
return null;
|
|
411
|
-
const discoveredById = new Map(discovered.map((entry) => [entry.id, entry]));
|
|
412
|
-
const tables = new Map();
|
|
413
|
-
const flat = {};
|
|
414
|
-
for (const [name, value] of Object.entries(overrides.values)) {
|
|
415
|
-
const entry = discoveredById.get(name);
|
|
416
|
-
if (entry !== undefined) {
|
|
417
|
-
const values = tables.get(entry.table) ?? {};
|
|
418
|
-
values[entry.key] = value;
|
|
419
|
-
tables.set(entry.table, values);
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
flat[name] = value;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
const parts = [];
|
|
426
|
-
for (const [table, values] of tables)
|
|
427
|
-
parts.push(`${table}: ${JSON.stringify(values, null, 2)}`);
|
|
428
|
-
if (Object.keys(flat).length > 0)
|
|
429
|
-
parts.push(`controls: ${JSON.stringify(flat, null, 2)}`);
|
|
430
|
-
return parts.join("\n\n");
|
|
431
|
-
}
|
|
432
|
-
function TunePanel({ gameName }) {
|
|
433
|
-
const [deltasCopied, setDeltasCopied] = useState(false);
|
|
434
|
-
const [query, setQuery] = useState("");
|
|
435
|
-
const controls = devtools.controls.list();
|
|
436
|
-
const allDiscovered = devtools.discover.list();
|
|
437
|
-
const persist = () => persistDevtoolsOverrides(gameName);
|
|
438
|
-
const discoveredIds = new Set(allDiscovered.map((entry) => entry.id));
|
|
439
|
-
const needle = query.trim().toLowerCase();
|
|
440
|
-
const matches = (id) => needle === "" || id.toLowerCase().includes(needle);
|
|
441
|
-
const discovered = allDiscovered.filter((entry) => entry.enabled || matches(entry.id));
|
|
442
|
-
const explicit = controls.filter((control) => !discoveredIds.has(control.name) && matches(control.name));
|
|
443
|
-
const controlByName = new Map(controls.map((control) => [control.name, control]));
|
|
444
|
-
const snippet = deltaSnippet(allDiscovered);
|
|
445
|
-
const copyDeltas = () => {
|
|
446
|
-
if (snippet === null)
|
|
447
|
-
return;
|
|
448
|
-
const clipboard = navigator.clipboard;
|
|
449
|
-
if (clipboard !== undefined) {
|
|
450
|
-
void clipboard.writeText(snippet).catch(() => console.log(snippet));
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
console.log(snippet);
|
|
454
|
-
}
|
|
455
|
-
setDeltasCopied(true);
|
|
456
|
-
setTimeout(() => setDeltasCopied(false), 1500);
|
|
457
|
-
};
|
|
458
|
-
if (controls.length === 0 && allDiscovered.length === 0) {
|
|
459
|
-
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."] })] }));
|
|
460
|
-
}
|
|
461
|
-
const groups = new Map();
|
|
462
|
-
for (const entry of discovered) {
|
|
463
|
-
const groupKey = tunableGroupKey(entry.table, entry.key);
|
|
464
|
-
const existing = groups.get(groupKey);
|
|
465
|
-
if (existing !== undefined) {
|
|
466
|
-
existing.entries.push(entry);
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
groups.set(groupKey, { title: tunableGroupTitle(entry.table, entry.key), entries: [entry] });
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
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: () => {
|
|
473
|
-
devtools.controls.resetAll();
|
|
474
|
-
persist();
|
|
475
|
-
}, 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" }), _jsx("input", { type: "text", value: query, onChange: (event) => setQuery(event.target.value), placeholder: "filter\u2026", className: "min-w-0 flex-1 rounded border border-neutral-600 bg-transparent px-2 py-0.5 text-neutral-200 placeholder:text-neutral-600 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) => {
|
|
476
|
-
const control = entry.enabled ? controlByName.get(entry.id) : undefined;
|
|
477
|
-
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) => {
|
|
478
|
-
if (event.target.checked)
|
|
479
|
-
devtools.discover.enable(entry.id);
|
|
480
|
-
else
|
|
481
|
-
devtools.discover.disable(entry.id);
|
|
482
|
-
persist();
|
|
483
|
-
} }), _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));
|
|
484
|
-
})] }, groupKey)))] }));
|
|
485
|
-
}
|
|
486
67
|
function roundMs(value, digits = 1) {
|
|
487
68
|
const scale = 10 ** digits;
|
|
488
69
|
return Math.round(value * scale) / scale;
|
|
@@ -529,6 +110,7 @@ function summarizeLongFrames(events) {
|
|
|
529
110
|
maxGeometries: maxGeometries > 0 ? maxGeometries : undefined,
|
|
530
111
|
};
|
|
531
112
|
}
|
|
113
|
+
/** @internal */
|
|
532
114
|
export function buildLeanReport(playable) {
|
|
533
115
|
const snap = devtools.snapshot();
|
|
534
116
|
const frame = snap.frame;
|
|
@@ -576,9 +158,11 @@ export function buildLeanReport(playable) {
|
|
|
576
158
|
discoveredCount: snap.discovered.length,
|
|
577
159
|
};
|
|
578
160
|
}
|
|
161
|
+
/** @internal */
|
|
579
162
|
export function buildFullReport(playable) {
|
|
580
163
|
return { game: playable.game.name, ...devtools.snapshot() };
|
|
581
164
|
}
|
|
165
|
+
/** @internal */
|
|
582
166
|
export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
|
|
583
167
|
const [tab, setTab] = useState("perf");
|
|
584
168
|
const [, setTick] = useState(0);
|
|
@@ -625,6 +209,7 @@ export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
|
|
|
625
209
|
const disposers = [
|
|
626
210
|
devtools.probes.register("entities", () => ctx.scene.entity.list().length),
|
|
627
211
|
devtools.probes.register("objects", () => ctx.scene.object.list().length),
|
|
212
|
+
devtools.probes.register("fallbacks", () => fallbackSeamsSnapshot()),
|
|
628
213
|
];
|
|
629
214
|
return () => {
|
|
630
215
|
for (const dispose of disposers)
|
|
@@ -651,7 +236,7 @@ export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
|
|
|
651
236
|
setCopied(true);
|
|
652
237
|
setTimeout(() => setCopied(false), 1500);
|
|
653
238
|
};
|
|
654
|
-
return (_jsxs("div", { className: "pointer-events-auto absolute left-4 top-4 z-50 w-[22rem] rounded border border-
|
|
239
|
+
return (_jsxs("div", { className: "pointer-events-auto absolute left-4 top-4 z-50 w-[22rem] rounded-xl border border-white/10 bg-[#0c0e12]/95 p-3 text-xs text-neutral-100 shadow-2xl shadow-black/60 backdrop-blur-md", children: [_jsx("style", { children: `
|
|
655
240
|
.jg-devtools-scroll {
|
|
656
241
|
scrollbar-width: thin;
|
|
657
242
|
scrollbar-color: #52525b #18181b;
|
|
@@ -672,5 +257,5 @@ export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
|
|
|
672
257
|
.jg-devtools-scroll::-webkit-scrollbar-thumb:hover {
|
|
673
258
|
background-color: #71717a;
|
|
674
259
|
}
|
|
675
|
-
` }), _jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "font-semibold uppercase tracking-
|
|
260
|
+
` }), _jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-neutral-300", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-cyan-400 shadow-[0_0_6px_rgba(34,211,238,0.8)]" }), playable.game.name, " devtools"] }), _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: copyReport, children: copied ? "Copied" : "Copy report" })] }), _jsx("div", { className: "mb-2.5 flex gap-0.5 rounded-lg bg-black/40 p-0.5 ring-1 ring-inset ring-white/[0.06]", children: TABS.map((entry) => (_jsx("button", { type: "button", className: `flex-1 rounded-md px-2 py-1 transition-colors ${tab === entry.id ? "bg-cyan-500/90 font-medium text-white shadow-sm shadow-cyan-950/50" : "text-neutral-400 hover:bg-white/[0.06] hover:text-neutral-200"}`, 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, tab === "col" ? _jsx(ColPanel, {}) : null, _jsx("div", { className: "mt-2.5 border-t border-white/[0.06] pt-2 text-[9px] tracking-wide text-neutral-500", children: "F2+D toggles \u00B7 Col = collision \u00B7 agents: __JG_DEVTOOLS.snapshot() \u00B7 .collisionDebug" })] }));
|
|
676
261
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { bindingLabel } from "@jgengine/core/input/actionBindings";
|
|
3
|
+
function flattenCodes(codes) {
|
|
4
|
+
if (Array.isArray(codes)) {
|
|
5
|
+
return [{ codes: codes.map(bindingLabel).join(" / "), mode: "press" }];
|
|
6
|
+
}
|
|
7
|
+
const modes = codes;
|
|
8
|
+
const result = [];
|
|
9
|
+
if (modes.hold !== undefined && modes.hold.length > 0) {
|
|
10
|
+
result.push({ codes: modes.hold.map(bindingLabel).join(" / "), mode: "hold" });
|
|
11
|
+
}
|
|
12
|
+
if (modes.toggle !== undefined && modes.toggle.length > 0) {
|
|
13
|
+
result.push({ codes: modes.toggle.map(bindingLabel).join(" / "), mode: "toggle" });
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function keybindRows(input) {
|
|
18
|
+
const rows = [];
|
|
19
|
+
for (const [action, codes] of Object.entries(input ?? {})) {
|
|
20
|
+
const entries = flattenCodes(codes);
|
|
21
|
+
for (const entry of entries)
|
|
22
|
+
rows.push({ action, ...entry });
|
|
23
|
+
}
|
|
24
|
+
return rows;
|
|
25
|
+
}
|
|
26
|
+
export function KeysPanel({ input }) {
|
|
27
|
+
const rows = keybindRows(input);
|
|
28
|
+
if (rows.length === 0)
|
|
29
|
+
return _jsx("div", { className: "text-neutral-400", children: "This game declares no keybinds." });
|
|
30
|
+
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))) }));
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function LogsPanel(): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
3
|
+
export function LogsPanel() {
|
|
4
|
+
const entries = devtools.logs.list().slice(-60);
|
|
5
|
+
const colors = {
|
|
6
|
+
error: "text-red-400",
|
|
7
|
+
warn: "text-amber-300",
|
|
8
|
+
info: "text-sky-300",
|
|
9
|
+
log: "text-neutral-300",
|
|
10
|
+
};
|
|
11
|
+
return (_jsxs("div", { className: "space-y-2", 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: () => 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)))] })] }));
|
|
12
|
+
}
|