@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.
Files changed (286) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/CombatPresentation.d.ts +5 -0
  5. package/dist/CombatPresentation.js +8 -0
  6. package/dist/GameHost.d.ts +25 -1
  7. package/dist/GameHost.js +60 -4
  8. package/dist/GamePhaseStamp.d.ts +1 -0
  9. package/dist/GamePhaseStamp.js +9 -0
  10. package/dist/GamePlayer.d.ts +5 -0
  11. package/dist/GamePlayer.js +5 -0
  12. package/dist/GamePlayerShell.d.ts +5 -12
  13. package/dist/GamePlayerShell.js +67 -1332
  14. package/dist/GameUiPreview.js +3 -2
  15. package/dist/Shell3dPresentation.d.ts +63 -0
  16. package/dist/Shell3dPresentation.js +302 -0
  17. package/dist/ShellChrome.d.ts +51 -0
  18. package/dist/ShellChrome.js +51 -0
  19. package/dist/ShellHudPresentation.d.ts +41 -0
  20. package/dist/ShellHudPresentation.js +22 -0
  21. package/dist/audio/audioEngine.d.ts +4 -0
  22. package/dist/audio/audioEngine.js +128 -21
  23. package/dist/audio/audioWire.d.ts +40 -0
  24. package/dist/audio/audioWire.js +62 -0
  25. package/dist/audio/loopParams.d.ts +20 -0
  26. package/dist/audio/loopParams.js +28 -0
  27. package/dist/behaviour.d.ts +2 -1
  28. package/dist/behaviour.js +2 -1
  29. package/dist/behaviourAttach.d.ts +1 -0
  30. package/dist/behaviourAttach.js +1 -0
  31. package/dist/behaviourDriver.d.ts +1 -0
  32. package/dist/behaviourDriver.js +1 -0
  33. package/dist/boundActionDispatch.d.ts +13 -0
  34. package/dist/boundActionDispatch.js +37 -0
  35. package/dist/camera/GameCameraRig.d.ts +5 -2
  36. package/dist/camera/GameCameraRig.js +8 -3
  37. package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
  38. package/dist/camera/GameFirstPersonCamera.js +8 -3
  39. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  40. package/dist/camera/GameInspectionCamera.js +2 -2
  41. package/dist/camera/GameOrbitCamera.js +24 -9
  42. package/dist/camera/cameraBlendMath.d.ts +3 -0
  43. package/dist/camera/cameraBlendMath.js +3 -0
  44. package/dist/camera/cameraRigs.d.ts +3 -1
  45. package/dist/camera/cameraRigs.js +21 -3
  46. package/dist/camera/fovPreference.d.ts +6 -1
  47. package/dist/camera/fovPreference.js +6 -1
  48. package/dist/camera/index.d.ts +4 -7
  49. package/dist/camera/index.js +4 -7
  50. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  51. package/dist/camera/inspectionCameraMath.js +7 -2
  52. package/dist/camera/orbitCameraMath.d.ts +36 -6
  53. package/dist/camera/orbitCameraMath.js +56 -6
  54. package/dist/camera/rigMath.d.ts +114 -24
  55. package/dist/camera/rigMath.js +129 -24
  56. package/dist/camera/rigResolve.d.ts +4 -2
  57. package/dist/camera/rigResolve.js +4 -2
  58. package/dist/camera/shakeChannel.d.ts +6 -2
  59. package/dist/camera/shakeChannel.js +6 -2
  60. package/dist/camera/shakeChannelMath.d.ts +1 -0
  61. package/dist/camera/shakeChannelMath.js +1 -0
  62. package/dist/commandSink.d.ts +8 -3
  63. package/dist/commandSink.js +8 -3
  64. package/dist/defineGame.d.ts +15 -1
  65. package/dist/defineGame.js +50 -9
  66. package/dist/devtools/ColPanel.d.ts +1 -0
  67. package/dist/devtools/ColPanel.js +14 -0
  68. package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
  69. package/dist/devtools/DevtoolsOverlay.js +47 -462
  70. package/dist/devtools/KeysPanel.d.ts +4 -0
  71. package/dist/devtools/KeysPanel.js +31 -0
  72. package/dist/devtools/LogsPanel.d.ts +1 -0
  73. package/dist/devtools/LogsPanel.js +12 -0
  74. package/dist/devtools/NetPanel.d.ts +4 -0
  75. package/dist/devtools/NetPanel.js +10 -0
  76. package/dist/devtools/PerfPanel.d.ts +4 -0
  77. package/dist/devtools/PerfPanel.js +53 -0
  78. package/dist/devtools/TunePanel.d.ts +3 -0
  79. package/dist/devtools/TunePanel.js +309 -0
  80. package/dist/devtools/agentBridge.d.ts +41 -0
  81. package/dist/devtools/agentBridge.js +266 -0
  82. package/dist/devtools/collisionDebug.d.ts +6 -0
  83. package/dist/devtools/collisionDebug.js +6 -0
  84. package/dist/devtools/collisionDebugMath.d.ts +8 -2
  85. package/dist/devtools/collisionDebugMath.js +24 -5
  86. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  87. package/dist/devtools/devtoolsOverrides.js +48 -0
  88. package/dist/devtools/panelAtoms.d.ts +9 -0
  89. package/dist/devtools/panelAtoms.js +10 -0
  90. package/dist/devtools/perfDiagnose.d.ts +2 -0
  91. package/dist/devtools/perfDiagnose.js +43 -0
  92. package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
  93. package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
  94. package/dist/drivers/FrameDriver.d.ts +37 -0
  95. package/dist/drivers/FrameDriver.js +232 -0
  96. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  97. package/dist/drivers/HudOnlyDriver.js +60 -0
  98. package/dist/environment/Daylight.d.ts +23 -7
  99. package/dist/environment/Daylight.js +106 -13
  100. package/dist/environment/EnvironmentScene.js +44 -9
  101. package/dist/environment/VolumetricClouds.d.ts +17 -0
  102. package/dist/environment/VolumetricClouds.js +202 -0
  103. package/dist/environment/daylightCycle.d.ts +3 -1
  104. package/dist/environment/daylightCycle.js +3 -1
  105. package/dist/environment/groundPadMath.d.ts +3 -0
  106. package/dist/environment/groundPadMath.js +3 -0
  107. package/dist/environment/index.d.ts +1 -0
  108. package/dist/environment/index.js +1 -0
  109. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  110. package/dist/environment/skyLightingPolicy.js +2 -0
  111. package/dist/environment/terrainGroundColor.d.ts +9 -0
  112. package/dist/environment/terrainGroundColor.js +22 -0
  113. package/dist/gameKit.d.ts +27 -0
  114. package/dist/gameKit.js +29 -0
  115. package/dist/hotbarActions.d.ts +26 -0
  116. package/dist/hotbarActions.js +27 -0
  117. package/dist/inputSink.d.ts +16 -4
  118. package/dist/inputSink.js +57 -7
  119. package/dist/map/MapMarkerBeacons.d.ts +3 -2
  120. package/dist/materialOverride.d.ts +10 -0
  121. package/dist/materialOverride.js +15 -3
  122. package/dist/multiplayer.js +8 -2
  123. package/dist/postfx/PostProcessing.d.ts +9 -1
  124. package/dist/postfx/PostProcessing.js +56 -11
  125. package/dist/postfx/gradeShader.js +9 -1
  126. package/dist/presentationResolve.d.ts +35 -0
  127. package/dist/presentationResolve.js +51 -0
  128. package/dist/registry.d.ts +4 -2
  129. package/dist/render/EnvironmentLighting.d.ts +10 -0
  130. package/dist/render/EnvironmentLighting.js +34 -0
  131. package/dist/render/SceneLighting.d.ts +7 -0
  132. package/dist/render/SceneLighting.js +18 -0
  133. package/dist/render/SceneModels.d.ts +14 -0
  134. package/dist/render/SceneModels.js +337 -0
  135. package/dist/render/assetBase.d.ts +17 -0
  136. package/dist/render/assetBase.js +35 -0
  137. package/dist/render/defaultSurface.d.ts +29 -0
  138. package/dist/render/defaultSurface.js +101 -0
  139. package/dist/render/modelLoad.d.ts +24 -0
  140. package/dist/render/modelLoad.js +76 -0
  141. package/dist/render/modelRender.d.ts +11 -0
  142. package/dist/render/modelRender.js +15 -0
  143. package/dist/render/resolveModel.d.ts +54 -3
  144. package/dist/render/resolveModel.js +116 -4
  145. package/dist/render/useEntityRenderCues.d.ts +13 -0
  146. package/dist/render/useEntityRenderCues.js +62 -0
  147. package/dist/scatter/InstancedScatter.d.ts +24 -0
  148. package/dist/scatter/InstancedScatter.js +135 -0
  149. package/dist/scatter/scatterModels.d.ts +25 -0
  150. package/dist/scatter/scatterModels.js +61 -0
  151. package/dist/scatter/scatterProxies.d.ts +6 -0
  152. package/dist/scatter/scatterProxies.js +118 -0
  153. package/dist/scatter.d.ts +1 -0
  154. package/dist/scatter.js +1 -0
  155. package/dist/scene/AuthoredScene.d.ts +84 -0
  156. package/dist/scene/AuthoredScene.js +178 -0
  157. package/dist/scene/CityRenderer.d.ts +2 -0
  158. package/dist/scene/CityRenderer.js +1031 -0
  159. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  160. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  161. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  162. package/dist/scene/GrassFieldRenderer.js +73 -0
  163. package/dist/scene/PoleLineRenderer.d.ts +2 -0
  164. package/dist/scene/PoleLineRenderer.js +287 -0
  165. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  166. package/dist/scene/SoilPatchRenderer.js +84 -0
  167. package/dist/scene/StudioStage.d.ts +37 -0
  168. package/dist/scene/StudioStage.js +42 -0
  169. package/dist/scene/WaterRenderer.d.ts +2 -0
  170. package/dist/scene/WaterRenderer.js +61 -0
  171. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  172. package/dist/scene/builtinSceneKindRenderers.js +26 -0
  173. package/dist/scene/cityWindowMaterial.d.ts +16 -0
  174. package/dist/scene/cityWindowMaterial.js +81 -0
  175. package/dist/scene/sceneKindRenderers.d.ts +30 -0
  176. package/dist/scene/sceneKindRenderers.js +17 -0
  177. package/dist/scene.d.ts +1 -0
  178. package/dist/scene.js +1 -0
  179. package/dist/settings/appliedSettings.d.ts +7 -1
  180. package/dist/settings/appliedSettings.js +13 -2
  181. package/dist/settings/settingsController.d.ts +2 -0
  182. package/dist/settings/settingsController.js +22 -1
  183. package/dist/shellConstants.d.ts +4 -0
  184. package/dist/shellConstants.js +4 -0
  185. package/dist/shellMovement.d.ts +8 -0
  186. package/dist/shellMovement.js +11 -0
  187. package/dist/shellPointer.d.ts +20 -0
  188. package/dist/shellPointer.js +28 -0
  189. package/dist/structures/PlacementGhost.d.ts +4 -0
  190. package/dist/structures/PlacementGhost.js +4 -0
  191. package/dist/structures/TransformGizmo.d.ts +53 -0
  192. package/dist/structures/TransformGizmo.js +59 -0
  193. package/dist/structures/index.d.ts +1 -0
  194. package/dist/structures/index.js +1 -0
  195. package/dist/terrain/GrassField.d.ts +11 -3
  196. package/dist/terrain/GrassField.js +16 -6
  197. package/dist/terrain/TerraformBrushCursor.js +3 -0
  198. package/dist/terrain/grassBudget.d.ts +11 -2
  199. package/dist/terrain/grassBudget.js +20 -4
  200. package/dist/terrain/grassGeometry.d.ts +35 -0
  201. package/dist/terrain/grassGeometry.js +85 -20
  202. package/dist/terrain/grassMaterial.d.ts +20 -0
  203. package/dist/terrain/grassMaterial.js +68 -13
  204. package/dist/terrain/heightfieldGeometry.d.ts +34 -0
  205. package/dist/terrain/heightfieldGeometry.js +109 -0
  206. package/dist/terrain/index.d.ts +1 -0
  207. package/dist/terrain/index.js +1 -0
  208. package/dist/terrain/random.d.ts +3 -0
  209. package/dist/terrain/random.js +3 -0
  210. package/dist/terrain/soilPatchMaterial.d.ts +23 -0
  211. package/dist/terrain/soilPatchMaterial.js +147 -0
  212. package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
  213. package/dist/terrain/terrainDetailMaterial.js +87 -7
  214. package/dist/terrain/terrainMath.d.ts +8 -1
  215. package/dist/terrain/terrainMath.js +8 -1
  216. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  217. package/dist/touch/TouchControlsOverlay.js +201 -22
  218. package/dist/useShellMultiplayerSync.d.ts +9 -0
  219. package/dist/useShellMultiplayerSync.js +98 -0
  220. package/dist/visibility/CullingProvider.js +1 -1
  221. package/dist/water/Ocean.d.ts +7 -1
  222. package/dist/water/Ocean.js +16 -3
  223. package/dist/water/OceanConfig.d.ts +11 -0
  224. package/dist/water/OceanConfig.js +8 -0
  225. package/dist/water/OceanMaterial.d.ts +12 -0
  226. package/dist/water/OceanMaterial.js +9 -0
  227. package/dist/water/OceanShader.d.ts +2 -2
  228. package/dist/water/OceanShader.js +78 -10
  229. package/dist/weather/fireSpreadPose.d.ts +1 -0
  230. package/dist/weather/fireSpreadPose.js +1 -0
  231. package/dist/weather/weatherGeometry.d.ts +1 -0
  232. package/dist/weather/weatherGeometry.js +1 -0
  233. package/dist/weather/weatherMath.d.ts +3 -0
  234. package/dist/weather/weatherMath.js +3 -0
  235. package/dist/weather/weatherUniforms.d.ts +3 -0
  236. package/dist/weather/weatherUniforms.js +3 -0
  237. package/dist/world/PlaceScene.d.ts +11 -0
  238. package/dist/world/PlaceScene.js +27 -0
  239. package/dist/world/RetainedVfx.d.ts +10 -0
  240. package/dist/world/RetainedVfx.js +136 -0
  241. package/dist/world/WorldHud.d.ts +49 -2
  242. package/dist/world/WorldHud.js +87 -2
  243. package/dist/world/WorldScene.d.ts +21 -0
  244. package/dist/world/WorldScene.js +186 -0
  245. package/dist/world/WorldVfx.d.ts +9 -0
  246. package/dist/world/WorldVfx.js +184 -0
  247. package/dist/world/entityPose.d.ts +2 -0
  248. package/dist/world/entityPose.js +2 -0
  249. package/dist/world/floatTextStyle.d.ts +1 -0
  250. package/dist/world/floatTextStyle.js +1 -0
  251. package/dist/world/telegraphPulse.d.ts +1 -0
  252. package/dist/world/telegraphPulse.js +1 -0
  253. package/dist/world/worldBarSamples.d.ts +25 -0
  254. package/dist/world/worldBarSamples.js +38 -0
  255. package/dist/worldSky.d.ts +11 -0
  256. package/dist/worldSky.js +21 -0
  257. package/dist/worldSync.d.ts +2 -1
  258. package/dist/worldSync.js +2 -1
  259. package/package.json +6 -6
  260. package/skills/game-design/SKILL.md +28 -0
  261. package/skills/game-design/agents/openai.yaml +4 -0
  262. package/skills/game-design/references/application-playbook.md +170 -0
  263. package/skills/game-design/references/field-guide.md +288 -0
  264. package/skills/jgengine/SKILL.md +54 -0
  265. package/skills/jgengine/api.md +527 -0
  266. package/skills/jgengine/capabilities.md +131 -0
  267. package/skills/jgengine/recipes/minimal-game.md +88 -0
  268. package/skills/jgengine-ui/SKILL.md +41 -0
  269. package/skills/jgengine-ui/api.md +2007 -0
  270. package/skills/jgengine-ui/capabilities.md +211 -0
  271. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  272. package/skills/jgengine-ui/reference.md +513 -0
  273. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  274. package/skills/jgengine-world/SKILL.md +51 -0
  275. package/skills/jgengine-world/api.md +3056 -0
  276. package/skills/jgengine-world/capabilities.md +476 -0
  277. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  278. package/skills/jgengine-world/reference.md +261 -0
  279. package/skills/level-design/SKILL.md +54 -0
  280. package/skills/level-design/agents/openai.yaml +4 -0
  281. package/skills/level-design/references/application-playbook.md +192 -0
  282. package/skills/level-design/references/field-guide.md +337 -0
  283. package/skills/level-design/references/live-editor-loop.md +179 -0
  284. package/dist/cartridge.d.ts +0 -145
  285. package/dist/cartridge.js +0 -246
  286. package/llms.txt +0 -2761
@@ -1,67 +1,21 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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 { convertAngle, devtools, instrumentLatency, LONG_FRAME_MS, parseColor, parseOverridesPayload, } from "@jgengine/core/devtools/devtools";
5
- import { bindingLabel } from "@jgengine/core/input/actionBindings";
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 { COLLISION_DEBUG_LAYERS, collisionDebug, } from "./collisionDebug.js";
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
- const PHASE_BAR_BUDGET_MS = 16.7;
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 frameCounter = useRef(0);
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
- devtools.render.record({
82
- drawCalls: info.render.calls,
83
- triangles: info.render.triangles,
84
- geometries: info.memory.geometries,
85
- textures: info.memory.textures,
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-neutral-700 bg-neutral-950/95 p-3 text-xs text-neutral-100 shadow-2xl", children: [_jsx("style", { children: `
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-wide text-neutral-300", children: [playable.game.name, " devtools"] }), _jsx("button", { type: "button", className: "rounded border border-neutral-600 px-2 py-0.5 text-neutral-300 hover:bg-neutral-800", onClick: copyReport, children: copied ? "Copied" : "Copy report" })] }), _jsx("div", { className: "mb-2 flex gap-1", children: TABS.map((entry) => (_jsx("button", { type: "button", className: `rounded px-2 py-0.5 ${tab === entry.id ? "bg-neutral-100 text-neutral-950" : "text-neutral-400 hover:bg-neutral-800"}`, onClick: () => setTab(entry.id), children: entry.label }, entry.id))) }), tab === "perf" ? _jsx(PerfPanel, { ctx: ctx }) : null, tab === "logs" ? _jsx(LogsPanel, {}) : null, tab === "net" ? _jsx(NetPanel, { multiplayer: multiplayer }) : null, tab === "keys" ? _jsx(KeysPanel, { input: playable.game.input }) : null, tab === "tune" ? _jsx(TunePanel, { gameName: playable.game.name }) : null, tab === "col" ? _jsx(ColPanel, {}) : null, _jsx("div", { className: "mt-2 border-t border-neutral-800 pt-1.5 text-[9px] text-neutral-500", children: "F2 toggles \u00B7 Col = collision \u00B7 agents: __JG_DEVTOOLS.snapshot() \u00B7 .collisionDebug" })] }));
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,4 @@
1
+ import { type ActionCodesMap } from "@jgengine/core/input/actionBindings";
2
+ export declare function KeysPanel({ input }: {
3
+ input: ActionCodesMap | undefined;
4
+ }): import("react").JSX.Element;
@@ -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
+ }