@jgengine/shell 0.8.0 → 0.9.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 (127) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +11 -0
  3. package/dist/GamePlayerShell.d.ts +5 -2
  4. package/dist/GamePlayerShell.js +358 -98
  5. package/dist/GameUiPreview.js +2 -1
  6. package/dist/audio/AudioComponents.js +25 -8
  7. package/dist/audio/audioEngine.d.ts +1 -0
  8. package/dist/audio/audioEngine.js +4 -0
  9. package/dist/behaviour.d.ts +5 -24
  10. package/dist/behaviour.js +14 -50
  11. package/dist/behaviourAttach.d.ts +9 -0
  12. package/dist/behaviourAttach.js +32 -0
  13. package/dist/behaviourDriver.d.ts +7 -0
  14. package/dist/behaviourDriver.js +21 -0
  15. package/dist/camera/GameCameraRig.js +1 -1
  16. package/dist/camera/GameFirstPersonCamera.js +14 -0
  17. package/dist/camera/GameOrbitCamera.js +10 -1
  18. package/dist/camera/PlayerFov.d.ts +18 -0
  19. package/dist/camera/PlayerFov.js +48 -0
  20. package/dist/camera/cameraBlendMath.d.ts +13 -0
  21. package/dist/camera/cameraBlendMath.js +36 -0
  22. package/dist/camera/cameraRigs.d.ts +3 -0
  23. package/dist/camera/cameraRigs.js +36 -33
  24. package/dist/camera/fovPreference.d.ts +25 -0
  25. package/dist/camera/fovPreference.js +73 -0
  26. package/dist/camera/index.d.ts +3 -0
  27. package/dist/camera/index.js +3 -0
  28. package/dist/camera/rigMath.d.ts +10 -0
  29. package/dist/camera/rigMath.js +36 -1
  30. package/dist/camera/shakeChannel.d.ts +3 -17
  31. package/dist/camera/shakeChannel.js +2 -21
  32. package/dist/camera/shakeChannelMath.d.ts +8 -0
  33. package/dist/camera/shakeChannelMath.js +21 -0
  34. package/dist/cartridge.d.ts +145 -0
  35. package/dist/cartridge.js +245 -0
  36. package/dist/defineGame.js +2 -1
  37. package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
  38. package/dist/devtools/CollisionDebugWorld.js +180 -0
  39. package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
  40. package/dist/devtools/DevtoolsOverlay.js +383 -41
  41. package/dist/devtools/collisionDebug.d.ts +57 -0
  42. package/dist/devtools/collisionDebug.js +127 -0
  43. package/dist/devtools/collisionDebugMath.d.ts +102 -0
  44. package/dist/devtools/collisionDebugMath.js +128 -0
  45. package/dist/environment/Daylight.d.ts +13 -7
  46. package/dist/environment/Daylight.js +12 -11
  47. package/dist/environment/EnvironmentScene.js +82 -35
  48. package/dist/environment/groundPadMath.d.ts +2 -2
  49. package/dist/environment/groundPadMath.js +1 -1
  50. package/dist/environment/index.d.ts +2 -1
  51. package/dist/environment/index.js +1 -0
  52. package/dist/environment/skyLightingPolicy.d.ts +10 -0
  53. package/dist/environment/skyLightingPolicy.js +6 -0
  54. package/dist/input/mouseLook.d.ts +27 -0
  55. package/dist/input/mouseLook.js +35 -0
  56. package/dist/materialOverride.d.ts +4 -6
  57. package/dist/materialOverride.js +12 -16
  58. package/dist/pointer/PointerProbe.js +6 -2
  59. package/dist/pointer/pointerService.d.ts +3 -0
  60. package/dist/pointer/pointerService.js +6 -0
  61. package/dist/render/modelRender.d.ts +10 -1
  62. package/dist/render/modelRender.js +32 -5
  63. package/dist/render/resolveModel.d.ts +14 -0
  64. package/dist/render/resolveModel.js +24 -0
  65. package/dist/settings/QuickControls.d.ts +4 -0
  66. package/dist/settings/QuickControls.js +42 -0
  67. package/dist/settings/SettingsChrome.d.ts +1 -0
  68. package/dist/settings/SettingsChrome.js +10 -0
  69. package/dist/settings/SettingsMenu.d.ts +6 -0
  70. package/dist/settings/SettingsMenu.js +148 -0
  71. package/dist/settings/SettingsRuntime.d.ts +11 -0
  72. package/dist/settings/SettingsRuntime.js +19 -0
  73. package/dist/settings/appliedSettings.d.ts +14 -0
  74. package/dist/settings/appliedSettings.js +27 -0
  75. package/dist/settings/settingsController.d.ts +20 -0
  76. package/dist/settings/settingsController.js +165 -0
  77. package/dist/structures/GeneratedBuilding.d.ts +10 -0
  78. package/dist/structures/GeneratedBuilding.js +96 -8
  79. package/dist/structures/index.d.ts +1 -1
  80. package/dist/structures/index.js +1 -1
  81. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  82. package/dist/terrain/CarvedTerrain.js +3 -3
  83. package/dist/terrain/GrassField.d.ts +3 -1
  84. package/dist/terrain/GrassField.js +4 -2
  85. package/dist/terrain/grassBudget.d.ts +3 -0
  86. package/dist/terrain/grassBudget.js +6 -0
  87. package/dist/terrain/grassGeometry.js +1 -1
  88. package/dist/terrain/terrainMath.d.ts +8 -0
  89. package/dist/terrain/terrainMath.js +9 -0
  90. package/dist/touch/OrientationHint.d.ts +3 -0
  91. package/dist/touch/OrientationHint.js +13 -0
  92. package/dist/touch/TouchControlsOverlay.d.ts +17 -1
  93. package/dist/touch/TouchControlsOverlay.js +115 -9
  94. package/dist/visibility/CullingProvider.d.ts +21 -0
  95. package/dist/visibility/CullingProvider.js +134 -0
  96. package/dist/vision/FrustumSensorHud.d.ts +1 -6
  97. package/dist/vision/FrustumSensorHud.js +25 -27
  98. package/dist/vision/frustumSampleEqual.d.ts +2 -0
  99. package/dist/vision/frustumSampleEqual.js +10 -0
  100. package/dist/water/Ocean.js +1 -1
  101. package/dist/water/OceanConfig.d.ts +13 -0
  102. package/dist/water/OceanConfig.js +25 -17
  103. package/dist/water/index.d.ts +1 -1
  104. package/dist/water/index.js +1 -1
  105. package/dist/weather/FireSpreadLayer.js +7 -2
  106. package/dist/weather/RainField.d.ts +3 -1
  107. package/dist/weather/RainField.js +4 -4
  108. package/dist/weather/SnowField.d.ts +3 -1
  109. package/dist/weather/SnowField.js +4 -4
  110. package/dist/weather/fireSpreadPose.d.ts +2 -0
  111. package/dist/weather/fireSpreadPose.js +4 -0
  112. package/dist/weather/weatherMath.d.ts +5 -1
  113. package/dist/weather/weatherMath.js +7 -2
  114. package/dist/world/DataObjects.d.ts +1 -1
  115. package/dist/world/DataObjects.js +3 -1
  116. package/dist/world/SpriteBatch.d.ts +44 -0
  117. package/dist/world/SpriteBatch.js +112 -0
  118. package/dist/world/WorldHud.d.ts +3 -0
  119. package/dist/world/WorldHud.js +89 -42
  120. package/dist/world/entityPose.d.ts +14 -0
  121. package/dist/world/entityPose.js +10 -0
  122. package/dist/world/telegraphPulse.d.ts +1 -0
  123. package/dist/world/telegraphPulse.js +4 -0
  124. package/dist/world/worldBarSamples.d.ts +30 -0
  125. package/dist/world/worldBarSamples.js +51 -0
  126. package/llms.txt +1404 -1143
  127. package/package.json +4 -4
@@ -0,0 +1,27 @@
1
+ export interface MouseLookOptions {
2
+ /** Radians of look per pixel of mouse movement; default `0.0024`. */
3
+ sensitivity?: number;
4
+ /** Pitch clamp in radians; default `1.15`. */
5
+ maxPitch?: number;
6
+ /** Request pointer lock on click (skipped on coarse-pointer/touch devices); default `true`. */
7
+ pointerLock?: boolean;
8
+ initialYaw?: number;
9
+ initialPitch?: number;
10
+ }
11
+ export interface MouseLookAim {
12
+ yaw: number;
13
+ pitch: number;
14
+ }
15
+ /**
16
+ * The analog mouse-look service chase/orbit-cam games hand-rolled (#282.8) — pointer-lock
17
+ * lifecycle plus delta accumulation into a yaw/pitch aim, decoupled from the first-person rig.
18
+ * Attach it to the canvas, read `aim()` from `onTick`/`useFrame`, dispose on unmount.
19
+ */
20
+ export interface MouseLookTracker {
21
+ aim(): MouseLookAim;
22
+ setAim(yaw: number, pitch: number): void;
23
+ /** True while pointer lock is held on the tracked element. */
24
+ locked(): boolean;
25
+ dispose(): void;
26
+ }
27
+ export declare function createMouseLookTracker(element: HTMLElement, options?: MouseLookOptions): MouseLookTracker;
@@ -0,0 +1,35 @@
1
+ export function createMouseLookTracker(element, options = {}) {
2
+ const sensitivity = options.sensitivity ?? 0.0024;
3
+ const maxPitch = options.maxPitch ?? 1.15;
4
+ const pointerLock = options.pointerLock ?? true;
5
+ let yaw = options.initialYaw ?? 0;
6
+ let pitch = options.initialPitch ?? 0;
7
+ const requestLock = () => {
8
+ if (!pointerLock)
9
+ return;
10
+ if (window.matchMedia?.("(pointer: coarse)").matches)
11
+ return;
12
+ if (document.pointerLockElement !== element)
13
+ void element.requestPointerLock?.();
14
+ };
15
+ const onMove = (event) => {
16
+ if (pointerLock && document.pointerLockElement !== element)
17
+ return;
18
+ yaw -= event.movementX * sensitivity;
19
+ pitch = Math.max(-maxPitch, Math.min(maxPitch, pitch - event.movementY * sensitivity));
20
+ };
21
+ element.addEventListener("click", requestLock);
22
+ window.addEventListener("mousemove", onMove);
23
+ return {
24
+ aim: () => ({ yaw, pitch }),
25
+ setAim(nextYaw, nextPitch) {
26
+ yaw = nextYaw;
27
+ pitch = Math.max(-maxPitch, Math.min(maxPitch, nextPitch));
28
+ },
29
+ locked: () => document.pointerLockElement === element,
30
+ dispose() {
31
+ element.removeEventListener("click", requestLock);
32
+ window.removeEventListener("mousemove", onMove);
33
+ },
34
+ };
35
+ }
@@ -1,8 +1,6 @@
1
1
  import * as THREE from "three";
2
2
  import type { ModelMaterialOverride } from "@jgengine/core/game/playableGame";
3
- /**
4
- * Clones each `MeshStandardMaterial` under `root` and applies `override`'s
5
- * color/finish onto the clone, so shared GLTF-cache scenes are never mutated
6
- * in place (#151.3).
7
- */
8
- export declare function applyMaterialOverride(root: THREE.Object3D, override: ModelMaterialOverride): void;
3
+ export interface MaterialOverrideOptions {
4
+ clone?: boolean;
5
+ }
6
+ export declare function applyMaterialOverride(root: THREE.Object3D, override: ModelMaterialOverride, options?: MaterialOverrideOptions): void;
@@ -1,32 +1,28 @@
1
1
  import * as THREE from "three";
2
- /**
3
- * Clones each `MeshStandardMaterial` under `root` and applies `override`'s
4
- * color/finish onto the clone, so shared GLTF-cache scenes are never mutated
5
- * in place (#151.3).
6
- */
7
- export function applyMaterialOverride(root, override) {
2
+ export function applyMaterialOverride(root, override, options) {
3
+ const clone = options?.clone !== false;
8
4
  root.traverse((node) => {
9
5
  const mesh = node;
10
6
  if (!mesh.isMesh)
11
7
  return;
12
8
  mesh.material = Array.isArray(mesh.material)
13
- ? mesh.material.map((material) => overrideOne(material, override))
14
- : overrideOne(mesh.material, override);
9
+ ? mesh.material.map((material) => overrideOne(material, override, clone))
10
+ : overrideOne(mesh.material, override, clone);
15
11
  });
16
12
  }
17
- function overrideOne(material, override) {
13
+ function overrideOne(material, override, clone) {
18
14
  if (!(material instanceof THREE.MeshStandardMaterial))
19
15
  return material;
20
- const clone = material.clone();
16
+ const target = clone ? material.clone() : material;
21
17
  if (override.color !== undefined)
22
- clone.color.set(override.color);
18
+ target.color.set(override.color);
23
19
  if (override.metalness !== undefined)
24
- clone.metalness = override.metalness;
20
+ target.metalness = override.metalness;
25
21
  if (override.roughness !== undefined)
26
- clone.roughness = override.roughness;
22
+ target.roughness = override.roughness;
27
23
  if (override.emissive !== undefined)
28
- clone.emissive.set(override.emissive);
24
+ target.emissive.set(override.emissive);
29
25
  if (override.emissiveIntensity !== undefined)
30
- clone.emissiveIntensity = override.emissiveIntensity;
31
- return clone;
26
+ target.emissiveIntensity = override.emissiveIntensity;
27
+ return target;
32
28
  }
@@ -5,7 +5,12 @@ export function PointerProbe({ service }) {
5
5
  const scene = useThree((state) => state.scene);
6
6
  const gl = useThree((state) => state.gl);
7
7
  const size = useThree((state) => state.size);
8
- service.bind({ camera, scene, width: size.width, height: size.height });
8
+ useEffect(() => {
9
+ service.bind({ camera, scene, width: size.width, height: size.height });
10
+ return () => {
11
+ service.bind(null);
12
+ };
13
+ }, [service, camera, scene, size.width, size.height]);
9
14
  useEffect(() => {
10
15
  const el = gl.domElement;
11
16
  let locked = false;
@@ -33,7 +38,6 @@ export function PointerProbe({ service }) {
33
38
  el.removeEventListener("pointermove", onMove);
34
39
  el.removeEventListener("pointerleave", onLeave);
35
40
  document.removeEventListener("pointerlockchange", onLockChange);
36
- service.bind(null);
37
41
  };
38
42
  }, [gl, service]);
39
43
  return null;
@@ -2,11 +2,14 @@ import * as THREE from "three";
2
2
  import type { PointerHit, PointerVec3 } from "@jgengine/core/input/pointer";
3
3
  export declare const POINTER_ENTITY_KEY = "jgEntityId";
4
4
  export declare const POINTER_OBJECT_KEY = "jgObjectId";
5
+ export type PointerHitFilter = (object: THREE.Object3D) => boolean;
5
6
  interface PointerDeps {
6
7
  camera: THREE.Camera;
7
8
  scene: THREE.Scene;
8
9
  width: number;
9
10
  height: number;
11
+ layers?: THREE.Layers;
12
+ filter?: PointerHitFilter;
10
13
  }
11
14
  export interface PointerService {
12
15
  /** Cast the current cursor into the world; null when the cursor is off-canvas. */
@@ -30,9 +30,15 @@ export function createPointerService() {
30
30
  function hitAtNdc(target) {
31
31
  if (deps === null)
32
32
  return null;
33
+ if (deps.layers !== undefined)
34
+ raycaster.layers.mask = deps.layers.mask;
35
+ else
36
+ raycaster.layers.enableAll();
33
37
  raycaster.setFromCamera(target, deps.camera);
34
38
  const intersects = raycaster.intersectObjects(deps.scene.children, true);
35
39
  for (const hit of intersects) {
40
+ if (deps.filter !== undefined && !deps.filter(hit.object))
41
+ continue;
36
42
  const mesh = hit.object;
37
43
  if (!mesh.isMesh)
38
44
  continue;
@@ -1,8 +1,17 @@
1
1
  import * as THREE from "three";
2
2
  import type { PaintStroke } from "@jgengine/core/scene/paintLayer";
3
3
  export declare const PAINT_TEXTURE_SIZE = 512;
4
- export declare function cloneModelScene(source: THREE.Object3D): THREE.Object3D;
4
+ export declare function cloneModelScene(source: THREE.Object3D, options?: {
5
+ cloneMaterials?: boolean;
6
+ }): THREE.Object3D;
7
+ export declare function disposeClonedMaterials(root: THREE.Object3D): void;
5
8
  export declare function standardMaterialsOf(root: THREE.Object3D): THREE.MeshStandardMaterial[];
9
+ export interface MaterialCache {
10
+ materials: THREE.MeshStandardMaterial[];
11
+ seedColor: THREE.Color;
12
+ }
13
+ export declare function cacheStandardMaterials(root: THREE.Object3D, into?: MaterialCache | null): MaterialCache;
14
+ export declare function applyPaintTextureToMaterials(materials: readonly THREE.MeshStandardMaterial[], paint: PaintCanvas): void;
6
15
  export interface PaintCanvas {
7
16
  canvas: HTMLCanvasElement;
8
17
  context: CanvasRenderingContext2D;
@@ -1,8 +1,10 @@
1
1
  import * as THREE from "three";
2
2
  import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
3
3
  export const PAINT_TEXTURE_SIZE = 512;
4
- export function cloneModelScene(source) {
4
+ export function cloneModelScene(source, options) {
5
5
  const clone = cloneSkinned(source);
6
+ if (options?.cloneMaterials === false)
7
+ return clone;
6
8
  clone.traverse((node) => {
7
9
  const mesh = node;
8
10
  if (!mesh.isMesh)
@@ -13,6 +15,21 @@ export function cloneModelScene(source) {
13
15
  });
14
16
  return clone;
15
17
  }
18
+ export function disposeClonedMaterials(root) {
19
+ const seen = new Set();
20
+ root.traverse((node) => {
21
+ const mesh = node;
22
+ if (!mesh.isMesh)
23
+ return;
24
+ const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
25
+ for (const material of list) {
26
+ if (seen.has(material))
27
+ continue;
28
+ seen.add(material);
29
+ material.dispose();
30
+ }
31
+ });
32
+ }
16
33
  function isMeshStandardMaterial(material) {
17
34
  return material.isMeshStandardMaterial === true;
18
35
  }
@@ -29,6 +46,19 @@ export function standardMaterialsOf(root) {
29
46
  });
30
47
  return materials;
31
48
  }
49
+ export function cacheStandardMaterials(root, into) {
50
+ if (into !== null && into !== undefined)
51
+ return into;
52
+ const materials = standardMaterialsOf(root);
53
+ const seedColor = materials[0]?.color.clone() ?? new THREE.Color("#ffffff");
54
+ return { materials, seedColor };
55
+ }
56
+ export function applyPaintTextureToMaterials(materials, paint) {
57
+ for (const material of materials) {
58
+ material.map = paint.texture;
59
+ material.needsUpdate = true;
60
+ }
61
+ }
32
62
  export function createPaintCanvas(seed, size = PAINT_TEXTURE_SIZE) {
33
63
  const canvas = document.createElement("canvas");
34
64
  canvas.width = size;
@@ -57,10 +87,7 @@ export function drawPaintStrokes(paint, strokes) {
57
87
  texture.needsUpdate = true;
58
88
  }
59
89
  export function applyPaintTexture(root, paint) {
60
- for (const material of standardMaterialsOf(root)) {
61
- material.map = paint.texture;
62
- material.needsUpdate = true;
63
- }
90
+ applyPaintTextureToMaterials(standardMaterialsOf(root), paint);
64
91
  }
65
92
  export function syncPaintCanvas(paint, seedColor, strokes, drawnCount) {
66
93
  if (strokes.length < drawnCount) {
@@ -0,0 +1,14 @@
1
+ import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
2
+ import type { ModelConfig } from "@jgengine/core/game/playableGame";
3
+ export interface ModelResolveContext {
4
+ seam: "entityModels" | "objectModels";
5
+ key: string;
6
+ }
7
+ /**
8
+ * Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog
9
+ * ids throw — silent generic-primitive fallback only happens when the mapping
10
+ * omits the key entirely (or uses tryResolveCatalogModel for optional ids).
11
+ */
12
+ export declare function resolveModel(value: string | ModelConfig | undefined, assets: AssetCatalog, context?: ModelResolveContext): ModelConfig | undefined;
13
+ /** Soft lookup used when an object catalog id may double as a model asset id. */
14
+ export declare function tryResolveCatalogModel(id: string, assets: AssetCatalog): ModelConfig | undefined;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog
3
+ * ids throw — silent generic-primitive fallback only happens when the mapping
4
+ * omits the key entirely (or uses tryResolveCatalogModel for optional ids).
5
+ */
6
+ export function resolveModel(value, assets, context) {
7
+ if (value === undefined)
8
+ return undefined;
9
+ if (typeof value !== "string")
10
+ return value;
11
+ const ref = assets.resolve(value);
12
+ if (ref === null) {
13
+ const where = context === undefined ? `model asset "${value}"` : `${context.seam}["${context.key}"] → "${value}"`;
14
+ throw new Error(`[jgengine] missing ${where} — not in the asset catalog. Fix the id, register it, or omit the mapping to keep the deliberate primitive/sprite fallback.`);
15
+ }
16
+ return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
17
+ }
18
+ /** Soft lookup used when an object catalog id may double as a model asset id. */
19
+ export function tryResolveCatalogModel(id, assets) {
20
+ const ref = assets.resolve(id);
21
+ if (ref === null)
22
+ return undefined;
23
+ return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
24
+ }
@@ -0,0 +1,4 @@
1
+ import type { SettingsController } from "./settingsController.js";
2
+ export declare function QuickControls({ controller }: {
3
+ controller: SettingsController;
4
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ function QuickSlider({ row }) {
4
+ const value = Number(row.value);
5
+ const readout = row.format?.(value) ?? String(value);
6
+ return (_jsxs("label", { className: "flex items-center gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "w-16 shrink-0 truncate", children: row.label }), _jsx("input", { type: "range", min: row.min, max: row.max, step: row.step, value: value, "aria-label": row.label, className: "h-1.5 flex-1 cursor-pointer accent-emerald-400", onChange: (event) => row.set(Number(event.target.value)) }), _jsx("span", { className: "w-9 shrink-0 text-right tabular-nums text-neutral-100", children: readout })] }));
7
+ }
8
+ function QuickToggle({ row }) {
9
+ const on = Boolean(row.value);
10
+ return (_jsxs("div", { className: "flex items-center justify-between gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "truncate", children: row.label }), _jsx("button", { type: "button", role: "switch", "aria-checked": on, "aria-label": row.label, onClick: () => row.set(!on), className: `relative h-6 w-11 shrink-0 rounded-full ring-1 transition ${on ? "bg-emerald-500 ring-emerald-400" : "bg-neutral-700 ring-white/10"}`, children: _jsx("span", { className: `absolute top-1 h-4 w-4 rounded-full bg-white shadow transition-all ${on ? "left-6" : "left-1"}` }) })] }));
11
+ }
12
+ function QuickSelect({ row }) {
13
+ const options = row.options ?? [];
14
+ const current = String(row.value);
15
+ return (_jsxs("div", { className: "flex items-center justify-between gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "truncate", children: row.label }), _jsx("div", { className: "flex shrink-0 gap-1 rounded-md bg-neutral-950/60 p-0.5 ring-1 ring-white/10", children: options.map((option) => {
16
+ const selected = option.value === current;
17
+ return (_jsx("button", { type: "button", onClick: () => row.set(option.value), className: `rounded px-2 py-1 font-medium transition ${selected ? "bg-emerald-500 text-neutral-950" : "text-neutral-300 hover:bg-neutral-800"}`, children: option.label }, option.value));
18
+ }) })] }));
19
+ }
20
+ function QuickRow({ row }) {
21
+ if (row.kind === "slider")
22
+ return _jsx(QuickSlider, { row: row });
23
+ if (row.kind === "toggle")
24
+ return _jsx(QuickToggle, { row: row });
25
+ return _jsx(QuickSelect, { row: row });
26
+ }
27
+ function Popover({ children }) {
28
+ return (_jsx("div", { className: "absolute bottom-12 left-0 w-64 space-y-2.5 rounded-lg bg-neutral-950/90 p-3 shadow-2xl ring-1 ring-white/10 backdrop-blur-sm", children: children }));
29
+ }
30
+ function QuickButton({ label, open, onToggle, children, }) {
31
+ return (_jsx("button", { type: "button", onClick: onToggle, "aria-label": label, "aria-pressed": open, className: `flex h-11 w-11 items-center justify-center rounded-md text-lg shadow-lg ring-1 backdrop-blur-sm transition ${open ? "bg-emerald-500/20 text-emerald-300 ring-emerald-400/50" : "bg-neutral-950/75 text-neutral-200 ring-white/10 hover:bg-neutral-900/80"}`, children: children }));
32
+ }
33
+ export function QuickControls({ controller }) {
34
+ const [open, setOpen] = useState(null);
35
+ const byId = (id) => controller.categories.find((category) => category.id === id);
36
+ const sound = byId("sound");
37
+ const graphics = byId("graphics");
38
+ if (sound === undefined && graphics === undefined)
39
+ return null;
40
+ const toggle = (which) => setOpen((current) => (current === which ? null : which));
41
+ return (_jsxs("div", { className: "pointer-events-auto absolute bottom-3 left-3 z-30 flex items-end gap-2", children: [sound !== undefined ? (_jsxs("div", { className: "relative", children: [open === "volume" ? (_jsx(Popover, { children: sound.rows.map((row) => (_jsx(QuickRow, { row: row }, row.id))) })) : null, _jsx(QuickButton, { label: "Volume", open: open === "volume", onToggle: () => toggle("volume"), children: _jsx("span", { "aria-hidden": true, children: "\uD83D\uDD0A" }) })] })) : null, graphics !== undefined ? (_jsxs("div", { className: "relative", children: [open === "graphics" ? (_jsx(Popover, { children: graphics.rows.map((row) => (_jsx(QuickRow, { row: row }, row.id))) })) : null, _jsx(QuickButton, { label: "Graphics", open: open === "graphics", onToggle: () => toggle("graphics"), children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", className: "h-5 w-5", "aria-hidden": true, children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 3a9 9 0 0 0 0 18M3 12h18M5 6c2 1.5 5 2.5 7 2.5S17 7.5 19 6M5 18c2-1.5 5-2.5 7-2.5s5 1 7 2.5" })] }) })] })) : null] }));
42
+ }
@@ -0,0 +1 @@
1
+ export declare function SettingsChrome(): import("react").JSX.Element | null;
@@ -0,0 +1,10 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useSettings } from "@jgengine/react/settings";
3
+ import { QuickControls } from "./QuickControls.js";
4
+ import { SettingsMenu } from "./SettingsMenu.js";
5
+ export function SettingsChrome() {
6
+ const settings = useSettings();
7
+ if (settings.categories.length === 0 && settings.actions.length === 0)
8
+ return null;
9
+ return (_jsxs(_Fragment, { children: [settings.surface === "quick" ? _jsx(QuickControls, { controller: settings }) : null, settings.isOpen ? _jsx(SettingsMenu, { controller: settings, onClose: settings.close }) : null] }));
10
+ }
@@ -0,0 +1,6 @@
1
+ import { type SettingsController } from "./settingsController.js";
2
+ export declare function SettingsMenu({ controller, onClose, initialTab, }: {
3
+ controller: SettingsController;
4
+ onClose: () => void;
5
+ initialTab?: string;
6
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,148 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import {} from "./settingsController.js";
4
+ const surface = "var(--jg-surface, #16161d)";
5
+ const surfaceDeep = "var(--jg-surface-deep, #0b0b11)";
6
+ const edge = "var(--jg-edge, #2b2b35)";
7
+ const text = "var(--jg-text, #ececef)";
8
+ const textDim = "var(--jg-text-dim, #9494a0)";
9
+ const accent = "var(--jg-accent, #6ee7a8)";
10
+ const accentGlow = "var(--jg-accent-glow, rgba(110,231,168,0.35))";
11
+ const onAccent = "var(--jg-surface-deep, #0b0b0f)";
12
+ const danger = "var(--jg-danger, #e0574e)";
13
+ const fontDisplay = "var(--jg-font-display, inherit)";
14
+ const rowStyle = { background: "rgba(255,255,255,0.045)", border: "1px solid rgba(255,255,255,0.08)" };
15
+ const badgeStyle = { background: surfaceDeep, border: "1px solid rgba(255,255,255,0.12)", color: text };
16
+ function GlyphClose() {
17
+ return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", className: "h-4 w-4", "aria-hidden": true, children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }));
18
+ }
19
+ function GlyphReset() {
20
+ return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-3.5 w-3.5", "aria-hidden": true, children: [_jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }), _jsx("path", { d: "M3 3v5h5" })] }));
21
+ }
22
+ function CloseButton({ onClose }) {
23
+ return (_jsx("button", { type: "button", onClick: onClose, "aria-label": "Close settings", className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition hover:bg-white/10", style: { color: textDim, border: "1px solid rgba(255,255,255,0.1)" }, children: _jsx(GlyphClose, {}) }));
24
+ }
25
+ function RowShell({ label, control }) {
26
+ return (_jsxs("div", { className: "flex items-center justify-between gap-4 rounded-xl px-3.5 py-3", style: rowStyle, children: [_jsx("span", { className: "text-sm", style: { color: text }, children: label }), control] }));
27
+ }
28
+ function SliderRow({ row }) {
29
+ const value = Number(row.value);
30
+ const readout = row.format?.(value) ?? String(value);
31
+ return (_jsx(RowShell, { label: row.label, control: _jsxs("div", { className: "flex flex-1 items-center gap-3 pl-4", children: [_jsx("input", { type: "range", min: row.min, max: row.max, step: row.step, value: value, "aria-label": row.label, className: "h-1.5 flex-1 cursor-pointer", style: { accentColor: accent }, onChange: (event) => row.set(Number(event.target.value)) }), _jsx("span", { className: "w-12 shrink-0 text-right text-sm tabular-nums", style: { color: text }, children: readout })] }) }));
32
+ }
33
+ function ToggleRow({ row }) {
34
+ const on = Boolean(row.value);
35
+ return (_jsx(RowShell, { label: row.label, control: _jsx("button", { type: "button", role: "switch", "aria-checked": on, "aria-label": row.label, onClick: () => row.set(!on), className: "relative h-7 w-12 shrink-0 rounded-full transition", style: { background: on ? accent : "rgba(255,255,255,0.16)", boxShadow: on ? `0 0 12px ${accentGlow}` : "none" }, children: _jsx("span", { className: `absolute top-1 h-5 w-5 rounded-full bg-white shadow transition-all ${on ? "left-6" : "left-1"}` }) }) }));
36
+ }
37
+ function SelectRow({ row }) {
38
+ const options = row.options ?? [];
39
+ const current = String(row.value);
40
+ return (_jsx(RowShell, { label: row.label, control: _jsx("div", { className: "flex shrink-0 gap-1 rounded-lg p-1", style: { background: surfaceDeep }, children: options.map((option) => {
41
+ const selected = option.value === current;
42
+ return (_jsx("button", { type: "button", onClick: () => row.set(option.value), className: "min-h-[34px] rounded-md px-3 text-sm font-medium transition", style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: option.label }, option.value));
43
+ }) }) }));
44
+ }
45
+ function RowControl({ row }) {
46
+ if (row.kind === "slider")
47
+ return _jsx(SliderRow, { row: row });
48
+ if (row.kind === "toggle")
49
+ return _jsx(ToggleRow, { row: row });
50
+ return _jsx(SelectRow, { row: row });
51
+ }
52
+ function KeybindRowView({ row, capturing, onCapture, }) {
53
+ return (_jsxs("div", { className: "flex items-center justify-between gap-3 rounded-xl px-3.5 py-3", style: rowStyle, children: [_jsx("span", { className: "text-sm", style: { color: text }, children: row.label }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsx("button", { type: "button", onClick: onCapture, disabled: capturing, title: "Click to rebind \u2014 Esc to cancel", className: "inline-flex min-h-[34px] min-w-[3.75rem] items-center justify-center rounded-lg px-3 font-mono text-sm transition hover:brightness-125", style: capturing
54
+ ? { background: accentGlow, color: accent, border: `1px solid ${accent}` }
55
+ : badgeStyle, children: capturing ? "Press…" : row.bindingLabel }), _jsx("button", { type: "button", onClick: () => row.reset(), "aria-label": `Reset ${row.label}`, title: "Reset to default", className: `flex h-8 w-8 items-center justify-center rounded-lg transition hover:bg-white/10 ${row.isDefault ? "invisible" : ""}`, style: { color: textDim }, children: _jsx(GlyphReset, {}) })] })] }));
56
+ }
57
+ function ActionRow({ action, onClose }) {
58
+ const isDanger = action.kind === "danger";
59
+ return (_jsxs("button", { type: "button", onClick: () => {
60
+ action.run();
61
+ onClose();
62
+ }, className: "flex w-full items-center justify-between gap-3 rounded-xl px-4 py-3.5 text-left transition hover:brightness-110", style: {
63
+ background: isDanger ? "rgba(224,87,78,0.12)" : "rgba(255,255,255,0.05)",
64
+ border: `1px solid ${isDanger ? danger : "rgba(255,255,255,0.1)"}`,
65
+ }, children: [_jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-sm font-semibold", style: { color: isDanger ? danger : text }, children: action.label }), action.description !== undefined ? (_jsx("span", { className: "text-xs", style: { color: textDim }, children: action.description })) : null] }), _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-4 w-4 shrink-0", style: { color: isDanger ? danger : textDim }, "aria-hidden": true, children: _jsx("path", { d: "M9 6l6 6-6 6" }) })] }));
66
+ }
67
+ function TabButton({ tab, selected, orientation, onSelect }) {
68
+ return (_jsx("button", { type: "button", onClick: onSelect, className: `min-h-[40px] whitespace-nowrap rounded-lg px-4 text-sm font-medium transition ${orientation === "col" ? "w-full text-left" : ""} ${selected ? "" : "hover:bg-white/5"}`, style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: tab.label }));
69
+ }
70
+ function Body({ tab, actions, capturing, setCapturing, onClose }) {
71
+ return (_jsx("div", { className: "jg-settings-scroll flex-1 space-y-2 overflow-y-auto p-4", children: tab.category === null
72
+ ? actions.map((action) => _jsx(ActionRow, { action: action, onClose: onClose }, action.id))
73
+ : (_jsxs(_Fragment, { children: [tab.category.rows.map((row) => (_jsx(RowControl, { row: row }, row.id))), tab.category.keybinds.map((row) => (_jsx(KeybindRowView, { row: row, capturing: capturing?.action === row.action, onCapture: () => setCapturing(row) }, row.action)))] })) }));
74
+ }
75
+ const panelStyle = {
76
+ background: surface,
77
+ color: text,
78
+ border: `1px solid ${edge}`,
79
+ fontFamily: fontDisplay,
80
+ };
81
+ function Backdrop({ children, onClose, align }) {
82
+ return (_jsx("div", { className: `pointer-events-auto absolute inset-0 z-40 flex justify-center bg-black/55 p-3 backdrop-blur-sm ${align}`, onClick: onClose, children: children }));
83
+ }
84
+ function stop(event) {
85
+ event.stopPropagation();
86
+ }
87
+ export function SettingsMenu({ controller, onClose, initialTab, }) {
88
+ const tabs = [
89
+ ...(controller.actions.length > 0 ? [{ id: "game", label: "Game", category: null }] : []),
90
+ ...controller.categories.map((category) => ({ id: category.id, label: category.label, category })),
91
+ ];
92
+ const [activeId, setActiveId] = useState(initialTab !== undefined && tabs.some((tab) => tab.id === initialTab) ? initialTab : (tabs[0]?.id ?? ""));
93
+ const [capturing, setCapturing] = useState(null);
94
+ const active = tabs.find((tab) => tab.id === activeId) ?? tabs[0] ?? null;
95
+ useEffect(() => {
96
+ if (capturing === null)
97
+ return;
98
+ const onKey = (event) => {
99
+ event.preventDefault();
100
+ event.stopPropagation();
101
+ if (event.code === "Escape") {
102
+ setCapturing(null);
103
+ return;
104
+ }
105
+ capturing.rebind(event.code);
106
+ setCapturing(null);
107
+ };
108
+ const onPointer = (event) => {
109
+ event.preventDefault();
110
+ event.stopPropagation();
111
+ capturing.rebind(`mouse${event.button}`);
112
+ setCapturing(null);
113
+ };
114
+ const onContext = (event) => event.preventDefault();
115
+ window.addEventListener("keydown", onKey, true);
116
+ window.addEventListener("pointerdown", onPointer, true);
117
+ window.addEventListener("contextmenu", onContext, true);
118
+ return () => {
119
+ window.removeEventListener("keydown", onKey, true);
120
+ window.removeEventListener("pointerdown", onPointer, true);
121
+ window.removeEventListener("contextmenu", onContext, true);
122
+ };
123
+ }, [capturing]);
124
+ useEffect(() => {
125
+ if (capturing !== null)
126
+ return;
127
+ const onKey = (event) => {
128
+ if (event.code === "Escape")
129
+ onClose();
130
+ };
131
+ window.addEventListener("keydown", onKey);
132
+ return () => window.removeEventListener("keydown", onKey);
133
+ }, [capturing, onClose]);
134
+ if (active === null)
135
+ return null;
136
+ const bodyProps = { tab: active, actions: controller.actions, capturing, setCapturing, onClose };
137
+ const title = (_jsx("h2", { className: "text-base font-semibold tracking-wide", style: { color: text }, children: "Settings" }));
138
+ if (controller.variant === "fullscreen") {
139
+ return (_jsx("div", { className: "pointer-events-auto absolute inset-0 z-40 flex flex-col", style: { background: surface, color: text, fontFamily: fontDisplay }, children: _jsxs("div", { className: "mx-auto flex h-full w-full max-w-2xl flex-col px-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 pb-4 pt-6", children: [_jsx("h2", { className: "text-2xl font-bold tracking-wide", style: { color: text }, children: "Settings" }), _jsx(CloseButton, { onClose: onClose })] }), _jsx("div", { className: "flex gap-1.5 overflow-x-auto pb-4", children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx(Body, { ...bodyProps })] }) }));
140
+ }
141
+ if (controller.variant === "sidebar") {
142
+ return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { onClick: stop, className: "flex overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(94vw, 760px)", height: "min(84vh, 560px)" }, children: [_jsxs("nav", { className: "flex w-44 shrink-0 flex-col gap-1 p-3", style: { background: surfaceDeep, borderRight: `1px solid ${edge}` }, children: [_jsx("span", { className: "px-3 pb-2 pt-1 text-xs font-semibold uppercase tracking-widest", style: { color: textDim }, children: "Settings" }), tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "col", onSelect: () => setActiveId(tab.id) }, tab.id)))] }), _jsxs("div", { className: "flex flex-1 flex-col", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", style: { borderBottom: `1px solid ${edge}` }, children: [_jsx("span", { className: "text-sm font-semibold", style: { color: text }, children: active.label }), _jsx(CloseButton, { onClose: onClose })] }), _jsx(Body, { ...bodyProps })] })] }) }));
143
+ }
144
+ if (controller.variant === "sheet") {
145
+ return (_jsx(Backdrop, { onClose: onClose, align: "items-end", children: _jsxs("div", { onClick: stop, className: "flex flex-col overflow-hidden rounded-t-3xl shadow-2xl", style: { ...panelStyle, width: "min(100%, 640px)", height: "min(84vh, 620px)" }, children: [_jsx("div", { className: "flex flex-col items-center pt-2.5", children: _jsx("span", { className: "h-1.5 w-10 rounded-full", style: { background: "rgba(255,255,255,0.25)" } }) }), _jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx("div", { className: "flex gap-1.5 overflow-x-auto px-4 pb-3", children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx("div", { style: { borderTop: `1px solid ${edge}` }, className: "flex flex-1 flex-col overflow-hidden", children: _jsx(Body, { ...bodyProps }) })] }) }));
146
+ }
147
+ return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { onClick: stop, className: "flex flex-col overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(92vw, 460px)", height: "min(86vh, 580px)" }, children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3.5", style: { borderBottom: `1px solid ${edge}` }, children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx("div", { className: "flex gap-1.5 overflow-x-auto px-4 py-3", style: { borderBottom: `1px solid ${edge}` }, children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx(Body, { ...bodyProps })] }) }));
148
+ }
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from "react";
2
+ import type { SettingsSurface, SettingsVariant } from "@jgengine/core/settings/settingsModel";
3
+ import { type SettingsActionView } from "@jgengine/react/settings";
4
+ import { type SettingsControllerInput } from "./settingsController.js";
5
+ export interface SettingsRuntimeProps extends SettingsControllerInput {
6
+ variant: SettingsVariant;
7
+ surface: SettingsSurface | false;
8
+ actions: readonly SettingsActionView[];
9
+ children: ReactNode;
10
+ }
11
+ export declare function SettingsRuntime({ variant, surface, actions, children, ...input }: SettingsRuntimeProps): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { SettingsControllerProvider, } from "@jgengine/react/settings";
4
+ import { useSettingsCategories } from "./settingsController.js";
5
+ export function SettingsRuntime({ variant, surface, actions, children, ...input }) {
6
+ const categories = useSettingsCategories(input);
7
+ const [isOpen, setOpen] = useState(false);
8
+ const controller = useMemo(() => ({
9
+ categories,
10
+ actions: [...actions],
11
+ variant,
12
+ surface,
13
+ isOpen,
14
+ open: () => setOpen(true),
15
+ close: () => setOpen(false),
16
+ setOpen,
17
+ }), [categories, actions, variant, surface, isOpen]);
18
+ return _jsx(SettingsControllerProvider, { controller: controller, children: children });
19
+ }
@@ -0,0 +1,14 @@
1
+ import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
2
+ import { type SettingsStore } from "@jgengine/core/settings/settingsModel";
3
+ import type { AudioEngine } from "../audio/audioEngine.js";
4
+ export declare function useSettingsRevision(store: SettingsStore): number;
5
+ export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean): {
6
+ shadows: boolean;
7
+ dpr: number;
8
+ uiScale: number;
9
+ };
10
+ export declare function AudioSettingsBridge({ store, engine, buses, }: {
11
+ store: SettingsStore;
12
+ engine: AudioEngine;
13
+ buses: Record<string, AudioBusDef> | undefined;
14
+ }): null;
@@ -0,0 +1,27 @@
1
+ import { useEffect, useReducer } from "react";
2
+ import { busVolumeSettingId, DEFAULT_GRAPHICS_QUALITY, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_DPR, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
3
+ export function useSettingsRevision(store) {
4
+ const [rev, bump] = useReducer((n) => n + 1, 0);
5
+ useEffect(() => store.subscribe(() => bump()), [store]);
6
+ return rev;
7
+ }
8
+ export function useGraphicsSettings(store, shadowsDefault) {
9
+ useSettingsRevision(store);
10
+ const quality = store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY);
11
+ const rawUiScale = store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE);
12
+ return {
13
+ shadows: store.get(SETTING_IDS.graphicsShadows, shadowsDefault),
14
+ dpr: GRAPHICS_QUALITY_DPR[quality] ?? GRAPHICS_QUALITY_DPR.high,
15
+ uiScale: Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, rawUiScale)),
16
+ };
17
+ }
18
+ export function AudioSettingsBridge({ store, engine, buses, }) {
19
+ const rev = useSettingsRevision(store);
20
+ useEffect(() => {
21
+ engine.setMasterGain(store.get(SETTING_IDS.masterVolume, DEFAULT_MASTER_VOLUME));
22
+ for (const bus of Object.values(buses ?? {})) {
23
+ engine.setBusGain(bus.id, store.get(busVolumeSettingId(bus.id), bus.gain ?? 1));
24
+ }
25
+ }, [store, engine, buses, rev]);
26
+ return null;
27
+ }
@@ -0,0 +1,20 @@
1
+ import { bindingLabel, type ActionCodesMap } from "@jgengine/core/input/actionBindings";
2
+ import type { BindingOverrides } from "@jgengine/core/input/bindingOverrides";
3
+ import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
4
+ import { type GameSettingDef, type SettingCategory, type SettingCategoryDef } from "@jgengine/core/settings/settingsModel";
5
+ import { type SettingsActionView, type SettingsKeybindRow, type SettingsCategoryView, type SettingsController, type SettingsRow } from "@jgengine/react/settings";
6
+ export type { SettingsActionView, SettingsKeybindRow, SettingsCategoryView, SettingsController, SettingsRow };
7
+ export interface SettingsControllerInput {
8
+ input: ActionCodesMap;
9
+ buses: Record<string, AudioBusDef> | undefined;
10
+ extra: readonly GameSettingDef[];
11
+ categories: readonly SettingCategoryDef[];
12
+ hide: readonly SettingCategory[];
13
+ fovEnabled: boolean;
14
+ hideBindings: readonly string[];
15
+ overrides: BindingOverrides;
16
+ rebind: (action: string, code: string) => void;
17
+ resetBinding: (action: string) => void;
18
+ }
19
+ export declare function useSettingsCategories(config: SettingsControllerInput): SettingsCategoryView[];
20
+ export { bindingLabel };