@jgengine/shell 0.7.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 (168) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +37 -1
  3. package/dist/GameHost.d.ts +12 -0
  4. package/dist/GameHost.js +33 -0
  5. package/dist/GamePlayer.d.ts +11 -0
  6. package/dist/GamePlayer.js +24 -0
  7. package/dist/GamePlayerShell.d.ts +36 -1
  8. package/dist/GamePlayerShell.js +751 -80
  9. package/dist/GameUiPreview.js +2 -1
  10. package/dist/audio/AudioComponents.js +25 -8
  11. package/dist/audio/audioEngine.d.ts +1 -0
  12. package/dist/audio/audioEngine.js +4 -0
  13. package/dist/behaviour.d.ts +12 -0
  14. package/dist/behaviour.js +27 -0
  15. package/dist/behaviourAttach.d.ts +9 -0
  16. package/dist/behaviourAttach.js +32 -0
  17. package/dist/behaviourDriver.d.ts +7 -0
  18. package/dist/behaviourDriver.js +21 -0
  19. package/dist/camera/GameCameraRig.d.ts +8 -3
  20. package/dist/camera/GameCameraRig.js +28 -14
  21. package/dist/camera/GameFirstPersonCamera.js +16 -0
  22. package/dist/camera/GameInspectionCamera.d.ts +6 -0
  23. package/dist/camera/GameInspectionCamera.js +24 -0
  24. package/dist/camera/GameOrbitCamera.js +10 -1
  25. package/dist/camera/PlayerFov.d.ts +18 -0
  26. package/dist/camera/PlayerFov.js +48 -0
  27. package/dist/camera/cameraBlendMath.d.ts +13 -0
  28. package/dist/camera/cameraBlendMath.js +36 -0
  29. package/dist/camera/cameraRigs.d.ts +8 -1
  30. package/dist/camera/cameraRigs.js +78 -38
  31. package/dist/camera/fovPreference.d.ts +25 -0
  32. package/dist/camera/fovPreference.js +73 -0
  33. package/dist/camera/index.d.ts +7 -2
  34. package/dist/camera/index.js +7 -2
  35. package/dist/camera/inspectionCameraMath.d.ts +25 -0
  36. package/dist/camera/inspectionCameraMath.js +44 -0
  37. package/dist/camera/rigMath.d.ts +50 -1
  38. package/dist/camera/rigMath.js +91 -1
  39. package/dist/camera/rigResolve.d.ts +15 -0
  40. package/dist/camera/rigResolve.js +52 -0
  41. package/dist/camera/shakeChannel.d.ts +3 -17
  42. package/dist/camera/shakeChannel.js +2 -21
  43. package/dist/camera/shakeChannelMath.d.ts +8 -0
  44. package/dist/camera/shakeChannelMath.js +21 -0
  45. package/dist/cartridge.d.ts +145 -0
  46. package/dist/cartridge.js +245 -0
  47. package/dist/defineGame.d.ts +16 -0
  48. package/dist/defineGame.js +52 -0
  49. package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
  50. package/dist/devtools/CollisionDebugWorld.js +180 -0
  51. package/dist/devtools/DevtoolsOverlay.d.ts +78 -0
  52. package/dist/devtools/DevtoolsOverlay.js +676 -0
  53. package/dist/devtools/collisionDebug.d.ts +57 -0
  54. package/dist/devtools/collisionDebug.js +127 -0
  55. package/dist/devtools/collisionDebugMath.d.ts +102 -0
  56. package/dist/devtools/collisionDebugMath.js +128 -0
  57. package/dist/environment/Daylight.d.ts +56 -0
  58. package/dist/environment/Daylight.js +101 -0
  59. package/dist/environment/EnvironmentScene.js +87 -48
  60. package/dist/environment/GroundPad.d.ts +7 -0
  61. package/dist/environment/GroundPad.js +12 -0
  62. package/dist/environment/daylightCycle.d.ts +26 -0
  63. package/dist/environment/daylightCycle.js +116 -0
  64. package/dist/environment/groundPadMath.d.ts +13 -0
  65. package/dist/environment/groundPadMath.js +10 -0
  66. package/dist/environment/index.d.ts +3 -0
  67. package/dist/environment/index.js +3 -0
  68. package/dist/environment/skyLightingPolicy.d.ts +10 -0
  69. package/dist/environment/skyLightingPolicy.js +6 -0
  70. package/dist/input/mouseLook.d.ts +27 -0
  71. package/dist/input/mouseLook.js +35 -0
  72. package/dist/materialOverride.d.ts +6 -0
  73. package/dist/materialOverride.js +28 -0
  74. package/dist/multiplayer.d.ts +16 -9
  75. package/dist/multiplayer.js +62 -12
  76. package/dist/pointer/PointerProbe.js +20 -3
  77. package/dist/pointer/pointerService.d.ts +5 -0
  78. package/dist/pointer/pointerService.js +51 -24
  79. package/dist/registry.d.ts +4 -1
  80. package/dist/registry.js +3 -1
  81. package/dist/render/modelRender.d.ts +23 -0
  82. package/dist/render/modelRender.js +106 -0
  83. package/dist/render/resolveModel.d.ts +14 -0
  84. package/dist/render/resolveModel.js +24 -0
  85. package/dist/settings/QuickControls.d.ts +4 -0
  86. package/dist/settings/QuickControls.js +42 -0
  87. package/dist/settings/SettingsChrome.d.ts +1 -0
  88. package/dist/settings/SettingsChrome.js +10 -0
  89. package/dist/settings/SettingsMenu.d.ts +6 -0
  90. package/dist/settings/SettingsMenu.js +148 -0
  91. package/dist/settings/SettingsRuntime.d.ts +11 -0
  92. package/dist/settings/SettingsRuntime.js +19 -0
  93. package/dist/settings/appliedSettings.d.ts +14 -0
  94. package/dist/settings/appliedSettings.js +27 -0
  95. package/dist/settings/settingsController.d.ts +20 -0
  96. package/dist/settings/settingsController.js +165 -0
  97. package/dist/structures/GeneratedBuilding.d.ts +10 -0
  98. package/dist/structures/GeneratedBuilding.js +96 -8
  99. package/dist/structures/index.d.ts +1 -1
  100. package/dist/structures/index.js +1 -1
  101. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  102. package/dist/terrain/CarvedTerrain.js +3 -3
  103. package/dist/terrain/GrassField.d.ts +3 -1
  104. package/dist/terrain/GrassField.js +4 -2
  105. package/dist/terrain/grassBudget.d.ts +3 -0
  106. package/dist/terrain/grassBudget.js +6 -0
  107. package/dist/terrain/grassGeometry.js +1 -1
  108. package/dist/terrain/index.d.ts +1 -1
  109. package/dist/terrain/index.js +1 -1
  110. package/dist/terrain/terrainMath.d.ts +9 -0
  111. package/dist/terrain/terrainMath.js +17 -2
  112. package/dist/touch/OrientationHint.d.ts +3 -0
  113. package/dist/touch/OrientationHint.js +13 -0
  114. package/dist/touch/TouchControlsOverlay.d.ts +34 -0
  115. package/dist/touch/TouchControlsOverlay.js +279 -0
  116. package/dist/visibility/CullingProvider.d.ts +21 -0
  117. package/dist/visibility/CullingProvider.js +134 -0
  118. package/dist/vision/FrustumSensorHud.d.ts +1 -6
  119. package/dist/vision/FrustumSensorHud.js +25 -27
  120. package/dist/vision/frustumSampleEqual.d.ts +2 -0
  121. package/dist/vision/frustumSampleEqual.js +10 -0
  122. package/dist/water/Ocean.js +1 -1
  123. package/dist/water/OceanConfig.d.ts +13 -0
  124. package/dist/water/OceanConfig.js +25 -17
  125. package/dist/water/OceanShader.d.ts +1 -1
  126. package/dist/water/OceanShader.js +3 -3
  127. package/dist/water/index.d.ts +1 -1
  128. package/dist/water/index.js +1 -1
  129. package/dist/weather/FireSpreadLayer.js +7 -2
  130. package/dist/weather/RainField.d.ts +3 -1
  131. package/dist/weather/RainField.js +4 -4
  132. package/dist/weather/SnowField.d.ts +3 -1
  133. package/dist/weather/SnowField.js +4 -4
  134. package/dist/weather/fireSpreadPose.d.ts +2 -0
  135. package/dist/weather/fireSpreadPose.js +4 -0
  136. package/dist/weather/weatherMath.d.ts +5 -1
  137. package/dist/weather/weatherMath.js +7 -2
  138. package/dist/world/DataObjects.d.ts +44 -0
  139. package/dist/world/DataObjects.js +77 -0
  140. package/dist/world/GridWorldScene.d.ts +10 -0
  141. package/dist/world/GridWorldScene.js +54 -0
  142. package/dist/world/SpriteBatch.d.ts +44 -0
  143. package/dist/world/SpriteBatch.js +112 -0
  144. package/dist/world/WorldHud.d.ts +8 -1
  145. package/dist/world/WorldHud.js +90 -39
  146. package/dist/world/WorldItems.js +21 -2
  147. package/dist/world/entityPose.d.ts +14 -0
  148. package/dist/world/entityPose.js +10 -0
  149. package/dist/world/telegraphPulse.d.ts +1 -0
  150. package/dist/world/telegraphPulse.js +4 -0
  151. package/dist/world/worldBarSamples.d.ts +30 -0
  152. package/dist/world/worldBarSamples.js +51 -0
  153. package/llms.txt +2643 -0
  154. package/package.json +6 -5
  155. package/dist/demo/builderDemo.d.ts +0 -2
  156. package/dist/demo/builderDemo.js +0 -189
  157. package/dist/demo/demoGame.d.ts +0 -2
  158. package/dist/demo/demoGame.js +0 -208
  159. package/dist/demo/environmentShowcase.d.ts +0 -2
  160. package/dist/demo/environmentShowcase.js +0 -15
  161. package/dist/demo/mapDemo.d.ts +0 -2
  162. package/dist/demo/mapDemo.js +0 -206
  163. package/dist/demo/pointerDemo.d.ts +0 -2
  164. package/dist/demo/pointerDemo.js +0 -151
  165. package/dist/demo/sensorShowcase.d.ts +0 -2
  166. package/dist/demo/sensorShowcase.js +0 -131
  167. package/dist/demo/survivalDemo.d.ts +0 -2
  168. package/dist/demo/survivalDemo.js +0 -291
package/llms.txt ADDED
@@ -0,0 +1,2643 @@
1
+ # @jgengine/shell
2
+ > Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.
3
+
4
+ Version: 0.9.0
5
+ License: AGPL-3.0-only
6
+ Repository: https://github.com/Noisemaker111/jgengine
7
+ Docs: https://jgengine.com
8
+ Imports use deep paths: `@jgengine/shell/<path>`.
9
+
10
+ ## Exported surface
11
+
12
+ ### @jgengine/shell/GameHost
13
+
14
+ - GameHost (function): function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer }: GameHostProps): React.JSX.Element
15
+ - GameHostProps (interface): interface GameHostProps
16
+
17
+ ### @jgengine/shell/GamePlayer
18
+
19
+ - GamePlayer (function): function GamePlayer({ gameId, registry, fallbackGameId, loading = null, multiplayer = null }: GamePlayerProps): React.JSX.Element
20
+ - GamePlayerProps (type): type GamePlayerProps = { gameId: string; registry: GameRegistry; fallbackGameId?: string; loading?: ReactNode; multiplayer?: ShellMultiplayer | null; }
21
+
22
+ ### @jgengine/shell/GamePlayerShell
23
+
24
+ - GamePlayerShell (function): function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }: { playable: PlayableGame; multiplayer?: ShellMultiplayer | null; poster?: boolean; /** Called once per boot after onInit/onNewPlayer with the live GameContext — a staging seam for screenshots,…
25
+ - applyMotionImpulses (function): function applyMotionImpulses(currentVelocity: number, batch: MotionIntentBatch | null): number — Applies a pending `MotionIntentBatch` to a vertical velocity: impulses add, then `verticalVelocity` replaces the result outright (#162.4).
26
+ - dispatchBoundAction (function): function dispatchBoundAction(ctx: GameContext, action: string, yaw: number, pitch: number, aim: Aim): void — Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`).
27
+ - hasEnvironmentTerrain (function): function hasEnvironmentTerrain(world: WorldFeature | undefined): boolean — True when the world is an environment feature with terrain, so the voxel controller should sample its height.
28
+ - heldActionsFor (function): function heldActionsFor(tracker: Pick<ActionStateTracker<string>, "isDown">, actions: readonly string[]): string[] — Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions.
29
+ - nearbyObstacles (function): function nearbyObstacles(objects: readonly SceneObject[], center: readonly [number, number, number], radius: number = OBSTACLE_GATHER_RADIUS): CollisionObstacle[] — Placed scene objects within `radius` of `center`, as `CollisionObstacle`s for `resolveObstacleStep` (#162.1).
30
+ - resolvePhysicsTuning (function): function resolvePhysicsTuning(physics: PhysicsConfig | undefined): MovementTuningOverrides | undefined — Maps the game's declared `physics` onto the movement controllers' tuning overrides. `PhysicsConfig.gravity` is a signed world acceleration (negative points down, matching every game's config and the Y-up convention), but the controllers integrate `velocityY -= gravityAcceleration * dt` and so expect a positive downward magnitude. Negating here is what keeps a down-pointing gravity pulling the player *down*; passing the signed value straight through flipped the sign and launched airborne players upward instead.
31
+ - resolveWorldSky (function): function resolveWorldSky(world: WorldFeature | undefined): SkyEnvironmentDescriptor | undefined — The world's declared sky, when its world feature is an environment with one (#196.1).
32
+ - shouldFireBoundAction (function): function shouldFireBoundAction(tracker: Pick<ActionStateTracker<string>, "isDown" | "wasPressed">, action: string, input: PlayableGame["game"]["input"], repeatFiredAt: ReadonlyMap<string, number>, now: number): boolean — Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`).
33
+
34
+ ### @jgengine/shell/GameUiPreview
35
+
36
+ - GameUiPreview (function): function GameUiPreview({ playable, scenario = defaultUiScenario, }: { playable: PlayableGame; scenario?: UiPreviewScenario; }): React.JSX.Element
37
+ - UiPreviewScenario (type): type UiPreviewScenario = (ctx: GameContext, playable: PlayableGame) => void
38
+ - defaultUiScenario (const): const defaultUiScenario: UiPreviewScenario
39
+
40
+ ### @jgengine/shell/audio/AudioComponents
41
+
42
+ - AudioListener (function): function AudioListener({ engine }: { engine: AudioEngine }): null
43
+ - EntityAudioEmitters (function): function EntityAudioEmitters({ engine, entitySounds, }: { engine: AudioEngine; entitySounds: Record<string, string> | undefined; }): null
44
+ - ObjectAudioEmitters (function): function ObjectAudioEmitters({ engine, objectSounds, }: { engine: AudioEngine; objectSounds: Record<string, string> | undefined; }): null
45
+
46
+ ### @jgengine/shell/audio/audioEngine
47
+
48
+ - AudioEmitterHandle (interface): interface AudioEmitterHandle
49
+ - AudioEngine (interface): interface AudioEngine
50
+ - AudioSceneConfig (interface): interface AudioSceneConfig
51
+ - Vec3 (interface): interface Vec3
52
+ - createAudioEngine (function): function createAudioEngine(config: AudioSceneConfig = {}): AudioEngine
53
+
54
+ ### @jgengine/shell/behaviour
55
+
56
+ - Object3DBehaviour (class): class Object3DBehaviour extends Behaviour
57
+ - attachObject3D (function): function attachObject3D<T extends Object3DBehaviour>(world: BehaviourWorld, object: Object3D, behaviour: T, nodeId: string = object.uuid): T
58
+ - createBehaviourWorldDriver (function): function createBehaviourWorldDriver(world: BehaviourWorld): { start(): void; stop(): void; isRunning(): boolean; step(dt: number): void; }
59
+ - useBehaviourWorld (function): function useBehaviourWorld(world: BehaviourWorld, scaleDt?: (rawDt: number) => number): void — Bootstraps `world` on mount and dispatches `world.update` every frame at simulation priority. Stops driving updates on unmount so remount does not double-step. `scaleDt` maps the raw frame delta to the dt behaviours receive — pass the game clock's scaling to keep behaviours on game time, or omit for real-time seconds. Games driving `world.update` themselves from `loop.onTick` should not also render this.
60
+
61
+ ### @jgengine/shell/behaviourAttach
62
+
63
+ - Object3DBehaviour (class): class Object3DBehaviour extends Behaviour
64
+ - attachObject3D (function): function attachObject3D<T extends Object3DBehaviour>(world: BehaviourWorld, object: Object3D, behaviour: T, nodeId: string = object.uuid): T
65
+
66
+ ### @jgengine/shell/behaviourDriver
67
+
68
+ - createBehaviourWorldDriver (function): function createBehaviourWorldDriver(world: BehaviourWorld): { start(): void; stop(): void; isRunning(): boolean; step(dt: number): void; }
69
+
70
+ ### @jgengine/shell/camera/GameCameraRig
71
+
72
+ - GameCameraRig (function): function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): React.JSX.Element
73
+ - GameCameraRigProps (interface): interface GameCameraRigProps
74
+ - resolveRigKind (function): function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind — Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least specific: an explicit `rig` field always wins; then `perspective: "first"` (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence of a rig's own config block selects that rig, checked in the fixed order below (#207.8) so a config carrying more than one block resolves deterministically instead of depending on object key order. Set `rig` explicitly to break a tie.
75
+
76
+ ### @jgengine/shell/camera/GameFirstPersonCamera
77
+
78
+ - GameFirstPersonCamera (function): function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): React.JSX.Element | null
79
+ - GameFirstPersonCameraProps (interface): interface GameFirstPersonCameraProps
80
+
81
+ ### @jgengine/shell/camera/GameInspectionCamera
82
+
83
+ - GameInspectionCamera (function): function GameInspectionCamera({ config: configPatch }: GameInspectionCameraProps): React.JSX.Element — Model-viewer style rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state.
84
+ - GameInspectionCameraProps (interface): interface GameInspectionCameraProps
85
+
86
+ ### @jgengine/shell/camera/GameOrbitCamera
87
+
88
+ - CameraFollowListener (type): type CameraFollowListener = (state: CameraFollowState) => void
89
+ - GameOrbitCamera (function): function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls = false, }: GameOrbitCameraProps): React.JSX.Element
90
+ - GameOrbitCameraProps (interface): interface GameOrbitCameraProps
91
+ - seedOrbitCameraTarget (function): function seedOrbitCameraTarget(camera: Camera, target: Vector3, distance: number, height: number): void — Seed orbit target before controls mount (demo spawn at origin).
92
+
93
+ ### @jgengine/shell/camera/PlayerFov
94
+
95
+ - PlayerFovProvider (function): function PlayerFovProvider({ config, orthographic, children, }: { config?: GameCameraConfig; orthographic: boolean; children: ReactNode; }): React.JSX.Element
96
+ - PlayerFovSlider (function): function PlayerFovSlider(): React.JSX.Element | null
97
+ - PlayerFovState (interface): interface PlayerFovState
98
+ - usePlayerFov (function): function usePlayerFov(): PlayerFovState
99
+
100
+ ### @jgengine/shell/camera/cameraBlendMath
101
+
102
+ - CameraBlendScratch (interface): interface CameraBlendScratch
103
+ - applyCameraBlendStep (function): function applyCameraBlendStep(scratch: CameraBlendScratch, camera: Camera, targetFov: number, dt: number): boolean
104
+ - captureCameraBlendFrom (function): function captureCameraBlendFrom(scratch: CameraBlendScratch, position: Vector3, quaternion: Quaternion, fov: number, duration: number): void
105
+ - createCameraBlendScratch (function): function createCameraBlendScratch(Vector3Ctor: new () => Vector3, QuaternionCtor: new () => Quaternion): CameraBlendScratch
106
+
107
+ ### @jgengine/shell/camera/cameraRigs
108
+
109
+ - CAMERA_POST_FRAME_PRIORITY (const): const CAMERA_POST_FRAME_PRIORITY: number
110
+ - CAMERA_RIG_FRAME_PRIORITY (const): const CAMERA_RIG_FRAME_PRIORITY: -1
111
+ - CameraBlendScratch (interface): interface CameraBlendScratch
112
+ - ChaseRig (function): function ChaseRig(props: RigProps): null
113
+ - CinematicRig (function): function CinematicRig(props: RigProps): null
114
+ - LockOnRig (function): function LockOnRig(props: RigProps): null
115
+ - ObserverRig (function): function ObserverRig(props: RigProps): null — Detached spectator/photo cam (#120): binds to any entity or fixed point and auto-orbits it, reading no player input at all — the van CCTV / photo-mode / kill-cam rig. Distinct from every other rig, which drives from mouse/keys.
116
+ - RigProps (interface): interface RigProps
117
+ - RtsRig (function): function RtsRig(props: RigProps & { panKeysEnabled?: boolean }): null
118
+ - ShoulderRig (function): function ShoulderRig(props: RigProps): null
119
+ - SideScrollRig (function): function SideScrollRig(props: RigProps): null — Fixed side-on 2.5D follow rig: watches the followed entity from the perpendicular axis, never reading WASD/mouse-look.
120
+ - TopDownRig (function): function TopDownRig(props: RigProps): null
121
+ - applyCameraBlendStep (function): function applyCameraBlendStep(scratch: CameraBlendScratch, camera: Camera, targetFov: number, dt: number): boolean
122
+ - captureCameraBlendFrom (function): function captureCameraBlendFrom(scratch: CameraBlendScratch, position: Vector3, quaternion: Quaternion, fov: number, duration: number): void
123
+ - createCameraBlendScratch (function): function createCameraBlendScratch(Vector3Ctor: new () => Vector3, QuaternionCtor: new () => Quaternion): CameraBlendScratch
124
+
125
+ ### @jgengine/shell/camera/fovPreference
126
+
127
+ - PLAYER_FOV_DEFAULT (const): const PLAYER_FOV_DEFAULT: any
128
+ - PLAYER_FOV_MAX (const): const PLAYER_FOV_MAX: 120
129
+ - PLAYER_FOV_MIN (const): const PLAYER_FOV_MIN: 40
130
+ - PLAYER_FOV_STORAGE_KEY (const): const PLAYER_FOV_STORAGE_KEY: "jgengine:player-fov"
131
+ - PlayerFovBounds (interface): interface PlayerFovBounds
132
+ - clampPlayerFov (function): function clampPlayerFov(value: unknown, min: number = PLAYER_FOV_MIN, max: number = PLAYER_FOV_MAX): number
133
+ - composePlayerFov (function): function composePlayerFov(preference: number, poseFov: number, mode: "relative" | "absolute" = "relative", bounds: PlayerFovBounds = resolvePlayerFovBounds()): number — Composition model for perspective rigs: - preference is the player base FOV - poseFov is the rig's authored FOV (includes chase-speed modulation, ADS zoom, transitions) - relative mode shifts the authored FOV by (preference − default) so mods still stack - absolute mode (cinematic keyframes) keeps the authored FOV as-is
134
+ - loadPlayerFov (function): function loadPlayerFov(bounds: PlayerFovBounds = resolvePlayerFovBounds(), storage: Pick<Storage, "getItem"> | null | undefined = defaultStorage()): number
135
+ - resolvePlayerFovBounds (function): function resolvePlayerFovBounds(options?: { min?: number; max?: number; default?: number; }): PlayerFovBounds
136
+ - savePlayerFov (function): function savePlayerFov(value: number, bounds: PlayerFovBounds = resolvePlayerFovBounds(), storage: Pick<Storage, "setItem"> | null | undefined = defaultStorage()): number
137
+
138
+ ### @jgengine/shell/camera/index
139
+
140
+ - CAMERA_POST_FRAME_PRIORITY (const): const CAMERA_POST_FRAME_PRIORITY: number
141
+ - CAMERA_RIG_FRAME_PRIORITY (const): const CAMERA_RIG_FRAME_PRIORITY: -1
142
+ - CameraBlendScratch (interface): interface CameraBlendScratch
143
+ - CameraFollowListener (type): type CameraFollowListener = (state: CameraFollowState) => void
144
+ - CameraFollowState (interface): interface CameraFollowState
145
+ - CameraPose (interface): interface CameraPose
146
+ - CameraShakeChannel (interface): interface CameraShakeChannel
147
+ - CameraShakeContext (const): const CameraShakeContext: React.Context<CameraShakeChannel>
148
+ - ChaseRig (function): function ChaseRig(props: RigProps): null
149
+ - CinematicRig (function): function CinematicRig(props: RigProps): null
150
+ - DEFAULT_ORBIT_CAMERA (const): const DEFAULT_ORBIT_CAMERA: ResolvedOrbitCameraConfig
151
+ - DirectorCameraValues (interface): interface DirectorCameraValues
152
+ - GAME_SIM_FRAME_PRIORITY (const): const GAME_SIM_FRAME_PRIORITY: 0 — Run simulation/movement before orbit follow so poses are current.
153
+ - GameCameraRig (function): function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): React.JSX.Element
154
+ - GameCameraRigProps (interface): interface GameCameraRigProps
155
+ - GameFirstPersonCamera (function): function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): React.JSX.Element | null
156
+ - GameFirstPersonCameraProps (interface): interface GameFirstPersonCameraProps
157
+ - GameInspectionCamera (function): function GameInspectionCamera({ config: configPatch }: GameInspectionCameraProps): React.JSX.Element — Model-viewer style rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state.
158
+ - GameInspectionCameraProps (interface): interface GameInspectionCameraProps
159
+ - GameOrbitCamera (function): function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls = false, }: GameOrbitCameraProps): React.JSX.Element
160
+ - GameOrbitCameraProps (interface): interface GameOrbitCameraProps
161
+ - LockOnRig (function): function LockOnRig(props: RigProps): null
162
+ - ORBIT_CAMERA_FRAME_PRIORITY (const): const ORBIT_CAMERA_FRAME_PRIORITY: -1 — Orbit follow reads the latest entity pose after GAME_SIM_FRAME_PRIORITY.
163
+ - ObserverRig (function): function ObserverRig(props: RigProps): null — Detached spectator/photo cam (#120): binds to any entity or fixed point and auto-orbits it, reading no player input at all — the van CCTV / photo-mode / kill-cam rig. Distinct from every other rig, which drives from mouse/keys.
164
+ - OrbitCameraConfig (interface): interface OrbitCameraConfig
165
+ - OrbitFollowRuntimeState (interface): interface OrbitFollowRuntimeState
166
+ - PLAYER_FOV_DEFAULT (const): const PLAYER_FOV_DEFAULT: any
167
+ - PLAYER_FOV_MAX (const): const PLAYER_FOV_MAX: 120
168
+ - PLAYER_FOV_MIN (const): const PLAYER_FOV_MIN: 40
169
+ - PLAYER_FOV_STORAGE_KEY (const): const PLAYER_FOV_STORAGE_KEY: "jgengine:player-fov"
170
+ - PlayerFovBounds (interface): interface PlayerFovBounds
171
+ - PlayerFovProvider (function): function PlayerFovProvider({ config, orthographic, children, }: { config?: GameCameraConfig; orthographic: boolean; children: ReactNode; }): React.JSX.Element
172
+ - PlayerFovSlider (function): function PlayerFovSlider(): React.JSX.Element | null
173
+ - PlayerFovState (interface): interface PlayerFovState
174
+ - ResolvedChase (interface): interface ResolvedChase
175
+ - ResolvedDirectedCamera (interface): interface ResolvedDirectedCamera
176
+ - ResolvedInspectionCameraConfig (interface): interface ResolvedInspectionCameraConfig
177
+ - ResolvedObserver (interface): interface ResolvedObserver
178
+ - ResolvedOrbitCameraConfig (interface): interface ResolvedOrbitCameraConfig — Fully resolved shell config after merging with DEFAULT_ORBIT_CAMERA.
179
+ - ResolvedShoulder (interface): interface ResolvedShoulder
180
+ - ResolvedSideScroll (interface): interface ResolvedSideScroll
181
+ - ResolvedTopDown (interface): interface ResolvedTopDown
182
+ - RigProps (interface): interface RigProps
183
+ - RtsRig (function): function RtsRig(props: RigProps & { panKeysEnabled?: boolean }): null
184
+ - ShakeOffset (interface): interface ShakeOffset
185
+ - ShoulderRig (function): function ShoulderRig(props: RigProps): null
186
+ - SideScrollRig (function): function SideScrollRig(props: RigProps): null — Fixed side-on 2.5D follow rig: watches the followed entity from the perpendicular axis, never reading WASD/mouse-look.
187
+ - StaticCameraValues (interface): interface StaticCameraValues
188
+ - TopDownRig (function): function TopDownRig(props: RigProps): null
189
+ - TraumaState (interface): interface TraumaState
190
+ - Vec3 (interface): interface Vec3
191
+ - addTrauma (function): function addTrauma(state: TraumaState, amount: number): void — Add trauma (0..1) and clamp; larger hits raise the ceiling toward 1.
192
+ - angleDelta (function): function angleDelta(a: number, b: number): number — Shortest signed angular delta from `a` to `b`, wrapped to (-PI, PI].
193
+ - applyCameraBlendStep (function): function applyCameraBlendStep(scratch: CameraBlendScratch, camera: Camera, targetFov: number, dt: number): boolean
194
+ - blendShoulder (function): function blendShoulder(hip: ResolvedShoulder, ads: ResolvedShoulder, t: number): ResolvedShoulder — Blend two shoulder framings by an ADS factor in [0,1].
195
+ - cameraFollowStep (function): function cameraFollowStep(input: { camera: Vec3; target: Vec3; previousTarget: Vec3 | null; lockedDistance: number | null; }): CameraFollowState
196
+ - cameraLookPitch (function): function cameraLookPitch(camera: Vec3, target: Vec3): number — Elevation the camera looks along (radians): negative = aiming down, positive = aiming up, 0 = level. Feeds aim.pitch so vertical aim tracks the camera.
197
+ - cameraShake (function): function cameraShake(amplitude: number, decayPerSecond?: number): void — Feed the default camera-shake channel from anywhere (see G7 hitstop cross-cut).
198
+ - captureCameraBlendFrom (function): function captureCameraBlendFrom(scratch: CameraBlendScratch, position: Vector3, quaternion: Quaternion, fov: number, duration: number): void
199
+ - chaseDesiredPosition (function): function chaseDesiredPosition(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
200
+ - chaseLookAt (function): function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Look point ahead of / above a chased vehicle.
201
+ - cinematicSample (function): function cinematicSample(keyframes: readonly CameraKeyframe[], elapsed: number, loop: boolean, fallbackFov: number): CinematicSample — Sample a keyframe path at `elapsed` seconds. Each keyframe's `duration` is the travel time from the previous keyframe into it; segment easing is per the destination keyframe. Reports `done` once past the final keyframe (unless looping, which wraps `elapsed` into the total path duration).
202
+ - clamp (function): function clamp(value: number, min: number, max: number): number
203
+ - clampPlayerFov (function): function clampPlayerFov(value: unknown, min: number = PLAYER_FOV_MIN, max: number = PLAYER_FOV_MAX): number
204
+ - composePlayerFov (function): function composePlayerFov(preference: number, poseFov: number, mode: "relative" | "absolute" = "relative", bounds: PlayerFovBounds = resolvePlayerFovBounds()): number — Composition model for perspective rigs: - preference is the player base FOV - poseFov is the rig's authored FOV (includes chase-speed modulation, ADS zoom, transitions) - relative mode shifts the authored FOV by (preference − default) so mods still stack - absolute mode (cinematic keyframes) keeps the authored FOV as-is
205
+ - createCameraBlendScratch (function): function createCameraBlendScratch(Vector3Ctor: new () => Vector3, QuaternionCtor: new () => Quaternion): CameraBlendScratch
206
+ - createCameraShakeChannel (function): function createCameraShakeChannel(defaultDecayPerSecond = 1.6): CameraShakeChannel
207
+ - createTrauma (function): function createTrauma(): TraumaState
208
+ - crossfadePose (function): function crossfadePose(from: CameraPose, to: CameraPose, t: number): CameraPose — Linear cross-fade between two full camera poses (position, lookAt, fov).
209
+ - defaultCameraShakeChannel (const): const defaultCameraShakeChannel: CameraShakeChannel — Process-wide default channel. A shell mounts its own channel via `CameraShakeContext`, but game systems that have no React context (e.g. a `loop.onTick` reacting to `entity.died`) can import `cameraShake` and feed the default channel directly.
210
+ - distanceBetween (function): function distanceBetween(a: Vec3, b: Vec3): number
211
+ - forwardVector (function): function forwardVector(yaw: number): Vec3
212
+ - lerp (function): function lerp(from: number, to: number, blend: number): number
213
+ - lerpVec3 (function): function lerpVec3(from: Vec3, to: Vec3, blend: number): Vec3
214
+ - loadPlayerFov (function): function loadPlayerFov(bounds: PlayerFovBounds = resolvePlayerFovBounds(), storage: Pick<Storage, "getItem"> | null | undefined = defaultStorage()): number
215
+ - lockOnPose (function): function lockOnPose(player: Vec3, target: Vec3, config: LockOnCameraConfig | undefined, fov: number): { pose: CameraPose; yaw: number } — Lock-on pose: camera sits behind the player along the player→target vector so the target stays framed. The look point is biased between player and target by `framingBias`.
216
+ - observerPose (function): function observerPose(subject: Vec3, angle: number, resolved: ResolvedObserver, fov: number): CameraPose — Detached spectator pose: orbits `subject` at a fixed distance/height, never reading player input.
217
+ - orbitFollowStep (function): function orbitFollowStep(input: { state: OrbitFollowRuntimeState; desiredTarget: Vec3; deltaSeconds: number; config: ResolvedOrbitCameraConfig; dragging: boolean; }): OrbitFollowRuntimeState & { distance: number } — Pure orbit follow step — smooth target tracking, camera carries target delta (OrbitControls alone keeps camera fixed when only target moves), optional distance lock. Call each frame before OrbitControls.update() in the shell.
218
+ - orbitYawFromCamera (function): function orbitYawFromCamera(cameraX: number, cameraZ: number, targetX: number, targetZ: number): number — Horizontal facing derived from an orbit camera orbiting a target on the XZ plane.
219
+ - resolveChase (function): function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase
220
+ - resolveDirectedCamera (function): function resolveDirectedCamera(director: DirectorCameraValues | undefined, staticConfig: StaticCameraValues): ResolvedDirectedCamera — Merges a `CameraDirector` runtime snapshot over the static `GameCameraConfig` (#196.2). `director` omitted, or its fields `undefined`/`null`, is a pure passthrough to `staticConfig` so mounting a director with no active override changes nothing.
221
+ - resolveFollowTargetFromPosition (function): function resolveFollowTargetFromPosition(position: readonly [number, number, number], config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset">): Vec3
222
+ - resolveInspectionCameraConfig (function): function resolveInspectionCameraConfig(config?: InspectionCameraConfig): ResolvedInspectionCameraConfig
223
+ - resolveInspectionZoomToCursor (function): function resolveInspectionZoomToCursor(anchor: InspectionZoomAnchor): boolean — Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag.
224
+ - resolveObserver (function): function resolveObserver(config: ObserverCameraConfig | undefined): ResolvedObserver
225
+ - resolveOrbitCameraConfig (function): function resolveOrbitCameraConfig(patch?: OrbitCameraConfig): ResolvedOrbitCameraConfig
226
+ - resolvePlayerFovBounds (function): function resolvePlayerFovBounds(options?: { min?: number; max?: number; default?: number; }): PlayerFovBounds
227
+ - resolveRigKind (function): function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind — Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least specific: an explicit `rig` field always wins; then `perspective: "first"` (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence of a rig's own config block selects that rig, checked in the fixed order below (#207.8) so a config carrying more than one block resolves deterministically instead of depending on object key order. Set `rig` explicitly to break a tie.
228
+ - resolveShoulder (function): function resolveShoulder(config: ShoulderCameraConfig | undefined, aiming: boolean): ResolvedShoulder
229
+ - resolveSideScroll (function): function resolveSideScroll(config: SideScrollCameraConfig | undefined): ResolvedSideScroll
230
+ - resolveSideScrollPose (function): function resolveSideScrollPose(entityPos: Vec3, resolved: ResolvedSideScroll, fov: number): CameraPose — Fixed lateral 2.5D follow pose: the camera sits perpendicular to the travel axis at `distance`, above the entity by `height`, and looks at the entity raised by `lookHeight`. Axis "x" watches from +z; axis "z" watches from +x.
231
+ - resolveTargetSmoothing (function): function resolveTargetSmoothing(config: ResolvedOrbitCameraConfig, dragging: boolean): number
232
+ - resolveTopDown (function): function resolveTopDown(config: TopDownCameraConfig | undefined): ResolvedTopDown
233
+ - rightVector (function): function rightVector(yaw: number): Vec3 — Screen-right of a yaw: `forward × up` with up = +Y.
234
+ - rtsPanKeysConflict (function): function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean
235
+ - savePlayerFov (function): function savePlayerFov(value: number, bounds: PlayerFovBounds = resolvePlayerFovBounds(), storage: Pick<Storage, "setItem"> | null | undefined = defaultStorage()): number
236
+ - seatPose (function): function seatPose(follow: Vec3, yaw: number, local: { x?: number; y?: number; z?: number }, fov: number): CameraPose — World-space seat pose (cockpit/hood/rear) rigidly attached to the vehicle.
237
+ - seedInspectionCamera (function): function seedInspectionCamera(config: ResolvedInspectionCameraConfig): { camera: Vec3; target: Vec3 } — Seeds the camera/target world position before OrbitControls mounts. Falls back to `initialDistance` behind `target` on the -Z axis, raised by 40% of that distance, when `initialPosition` is unset.
238
+ - seedOrbitFollowState (function): function seedOrbitFollowState(input: { entityPosition: readonly [number, number, number]; config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset" | "initialDistance" | "initialHeight">; }): OrbitFollowRuntimeState
239
+ - shakeOffset (function): function shakeOffset(state: TraumaState, config: { maxOffset?: number; maxRoll?: number; exponent?: number; frequency?: number; } | undefined): ShakeOffset — Positional + roll shake for the current trauma. Shake magnitude is `trauma^exponent` so small hits stay subtle; the oscillation is deterministic per-seed pseudo-noise (no per-frame RNG, so identical inputs reproduce).
240
+ - shoulderPose (function): function shoulderPose(follow: Vec3, yaw: number, pitch: number, sideSign: number, shoulder: ResolvedShoulder): CameraPose — Over-the-shoulder camera pose. The boom sits behind the follow point along `yaw`, raised by `heightOffset`, and pushed sideways by `shoulderOffset` (sign set by `sideSign`: +1 right, -1 left). `pitch` tilts the look point.
241
+ - sideScrollFollowBlend (function): function sideScrollFollowBlend(followSmoothing: number, dt: number): number — Frame-rate independent follow blend for the side-scroll rig; `followSmoothing <= 0` hard-locks (blend 1) instead of freezing.
242
+ - smoothBlend (function): function smoothBlend(deltaSeconds: number, speed: number): number — Frame-rate independent exponential smoothing factor in [0, 1].
243
+ - smoothYaw (function): function smoothYaw(current: number, desired: number, speed: number, dt: number): number — Exponential yaw smoothing that respects wrap-around.
244
+ - smoothstep (function): function smoothstep(t: number): number
245
+ - speedToFov (function): function speedToFov(speed: number, curve: { base?: number; max?: number; speedForMax?: number } | undefined): number — Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`.
246
+ - springArmStep (function): function springArmStep(current: Vec3, desired: Vec3, damping: number, dt: number): Vec3 — Exponential spring-arm approach toward a desired point (frame-rate independent).
247
+ - stepTrauma (function): function stepTrauma(state: TraumaState, decayPerSecond: number, dt: number): void — Decay trauma linearly toward 0 and advance the shake clock.
248
+ - topDownPose (function): function topDownPose(follow: Vec3, resolved: ResolvedTopDown, fov: number): CameraPose — Camera pose for a fixed top-down / isometric rig. `pitch` is the elevation of the camera→target ray above the ground (PI/2 = straight down); `yaw` is the fixed azimuth (PI/4 reads isometric). Height sets zoom; horizontal boom is derived so the look angle stays constant as height changes.
249
+ - useCameraShake (function): function useCameraShake(): CameraShakeChannel — The active rig's shake channel — call `.shake(...)` to add trauma from React UI.
250
+ - usePlayerFov (function): function usePlayerFov(): PlayerFovState
251
+ - yawTo (function): function yawTo(from: Vec3, to: Vec3): number — Yaw that points from `from` toward `to` on the XZ plane (matches shell forward = (sin, cos)).
252
+
253
+ ### @jgengine/shell/camera/inspectionCameraMath
254
+
255
+ - ResolvedInspectionCameraConfig (interface): interface ResolvedInspectionCameraConfig
256
+ - resolveInspectionCameraConfig (function): function resolveInspectionCameraConfig(config?: InspectionCameraConfig): ResolvedInspectionCameraConfig
257
+ - resolveInspectionZoomToCursor (function): function resolveInspectionZoomToCursor(anchor: InspectionZoomAnchor): boolean — Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag.
258
+ - seedInspectionCamera (function): function seedInspectionCamera(config: ResolvedInspectionCameraConfig): { camera: Vec3; target: Vec3 } — Seeds the camera/target world position before OrbitControls mounts. Falls back to `initialDistance` behind `target` on the -Z axis, raised by 40% of that distance, when `initialPosition` is unset.
259
+
260
+ ### @jgengine/shell/camera/orbitCameraMath
261
+
262
+ - CameraFollowState (interface): interface CameraFollowState
263
+ - DEFAULT_ORBIT_CAMERA (const): const DEFAULT_ORBIT_CAMERA: ResolvedOrbitCameraConfig
264
+ - GAME_SIM_FRAME_PRIORITY (const): const GAME_SIM_FRAME_PRIORITY: 0 — Run simulation/movement before orbit follow so poses are current.
265
+ - ORBIT_CAMERA_FRAME_PRIORITY (const): const ORBIT_CAMERA_FRAME_PRIORITY: -1 — Orbit follow reads the latest entity pose after GAME_SIM_FRAME_PRIORITY.
266
+ - OrbitCameraConfig (interface): interface OrbitCameraConfig
267
+ - OrbitFollowRuntimeState (interface): interface OrbitFollowRuntimeState
268
+ - ResolvedOrbitCameraConfig (interface): interface ResolvedOrbitCameraConfig — Fully resolved shell config after merging with DEFAULT_ORBIT_CAMERA.
269
+ - Vec3 (interface): interface Vec3
270
+ - cameraFollowStep (function): function cameraFollowStep(input: { camera: Vec3; target: Vec3; previousTarget: Vec3 | null; lockedDistance: number | null; }): CameraFollowState
271
+ - cameraLookPitch (function): function cameraLookPitch(camera: Vec3, target: Vec3): number — Elevation the camera looks along (radians): negative = aiming down, positive = aiming up, 0 = level. Feeds aim.pitch so vertical aim tracks the camera.
272
+ - distanceBetween (function): function distanceBetween(a: Vec3, b: Vec3): number
273
+ - lerpVec3 (function): function lerpVec3(from: Vec3, to: Vec3, blend: number): Vec3
274
+ - orbitFollowStep (function): function orbitFollowStep(input: { state: OrbitFollowRuntimeState; desiredTarget: Vec3; deltaSeconds: number; config: ResolvedOrbitCameraConfig; dragging: boolean; }): OrbitFollowRuntimeState & { distance: number } — Pure orbit follow step — smooth target tracking, camera carries target delta (OrbitControls alone keeps camera fixed when only target moves), optional distance lock. Call each frame before OrbitControls.update() in the shell.
275
+ - orbitYawFromCamera (function): function orbitYawFromCamera(cameraX: number, cameraZ: number, targetX: number, targetZ: number): number — Horizontal facing derived from an orbit camera orbiting a target on the XZ plane.
276
+ - resolveFollowTargetFromPosition (function): function resolveFollowTargetFromPosition(position: readonly [number, number, number], config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset">): Vec3
277
+ - resolveOrbitCameraConfig (function): function resolveOrbitCameraConfig(patch?: OrbitCameraConfig): ResolvedOrbitCameraConfig
278
+ - resolveTargetSmoothing (function): function resolveTargetSmoothing(config: ResolvedOrbitCameraConfig, dragging: boolean): number
279
+ - seedOrbitFollowState (function): function seedOrbitFollowState(input: { entityPosition: readonly [number, number, number]; config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset" | "initialDistance" | "initialHeight">; }): OrbitFollowRuntimeState
280
+ - smoothBlend (function): function smoothBlend(deltaSeconds: number, speed: number): number — Frame-rate independent exponential smoothing factor in [0, 1].
281
+
282
+ ### @jgengine/shell/camera/rigMath
283
+
284
+ - CameraPose (interface): interface CameraPose
285
+ - CinematicSample (interface): interface CinematicSample
286
+ - DirectorCameraValues (interface): interface DirectorCameraValues
287
+ - ResolvedChase (interface): interface ResolvedChase
288
+ - ResolvedDirectedCamera (interface): interface ResolvedDirectedCamera
289
+ - ResolvedObserver (interface): interface ResolvedObserver
290
+ - ResolvedShoulder (interface): interface ResolvedShoulder
291
+ - ResolvedSideScroll (interface): interface ResolvedSideScroll
292
+ - ResolvedTopDown (interface): interface ResolvedTopDown
293
+ - ShakeOffset (interface): interface ShakeOffset
294
+ - StaticCameraValues (interface): interface StaticCameraValues
295
+ - TraumaState (interface): interface TraumaState
296
+ - addTrauma (function): function addTrauma(state: TraumaState, amount: number): void — Add trauma (0..1) and clamp; larger hits raise the ceiling toward 1.
297
+ - angleDelta (function): function angleDelta(a: number, b: number): number — Shortest signed angular delta from `a` to `b`, wrapped to (-PI, PI].
298
+ - bankRollStep (function): function bankRollStep(currentRoll: number, yaw: number, previousYaw: number, dt: number, resolved: ResolvedChase): number — One smoothing step of the chase rig's turn-bank roll (#286.10): roll opposes yaw rate, clamped and exponentially damped.
299
+ - blendShoulder (function): function blendShoulder(hip: ResolvedShoulder, ads: ResolvedShoulder, t: number): ResolvedShoulder — Blend two shoulder framings by an ADS factor in [0,1].
300
+ - chaseDesiredPosition (function): function chaseDesiredPosition(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
301
+ - chaseLookAt (function): function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Look point ahead of / above a chased vehicle.
302
+ - cinematicSample (function): function cinematicSample(keyframes: readonly CameraKeyframe[], elapsed: number, loop: boolean, fallbackFov: number): CinematicSample — Sample a keyframe path at `elapsed` seconds. Each keyframe's `duration` is the travel time from the previous keyframe into it; segment easing is per the destination keyframe. Reports `done` once past the final keyframe (unless looping, which wraps `elapsed` into the total path duration).
303
+ - clamp (function): function clamp(value: number, min: number, max: number): number
304
+ - createTrauma (function): function createTrauma(): TraumaState
305
+ - crossfadePose (function): function crossfadePose(from: CameraPose, to: CameraPose, t: number): CameraPose — Linear cross-fade between two full camera poses (position, lookAt, fov).
306
+ - forwardVector (function): function forwardVector(yaw: number): Vec3
307
+ - leadFollowPoint (function): function leadFollowPoint(follow: Vec3, previous: Vec3, dt: number, resolved: ResolvedChase): Vec3 — Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`.
308
+ - lerp (function): function lerp(from: number, to: number, blend: number): number
309
+ - lockOnPose (function): function lockOnPose(player: Vec3, target: Vec3, config: LockOnCameraConfig | undefined, fov: number): { pose: CameraPose; yaw: number } — Lock-on pose: camera sits behind the player along the player→target vector so the target stays framed. The look point is biased between player and target by `framingBias`.
310
+ - observerPose (function): function observerPose(subject: Vec3, angle: number, resolved: ResolvedObserver, fov: number): CameraPose — Detached spectator pose: orbits `subject` at a fixed distance/height, never reading player input.
311
+ - resolveChase (function): function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase
312
+ - resolveDirectedCamera (function): function resolveDirectedCamera(director: DirectorCameraValues | undefined, staticConfig: StaticCameraValues): ResolvedDirectedCamera — Merges a `CameraDirector` runtime snapshot over the static `GameCameraConfig` (#196.2). `director` omitted, or its fields `undefined`/`null`, is a pure passthrough to `staticConfig` so mounting a director with no active override changes nothing.
313
+ - resolveObserver (function): function resolveObserver(config: ObserverCameraConfig | undefined): ResolvedObserver
314
+ - resolveShoulder (function): function resolveShoulder(config: ShoulderCameraConfig | undefined, aiming: boolean): ResolvedShoulder
315
+ - resolveSideScroll (function): function resolveSideScroll(config: SideScrollCameraConfig | undefined): ResolvedSideScroll
316
+ - resolveSideScrollPose (function): function resolveSideScrollPose(entityPos: Vec3, resolved: ResolvedSideScroll, fov: number): CameraPose — Fixed lateral 2.5D follow pose: the camera sits perpendicular to the travel axis at `distance`, above the entity by `height`, and looks at the entity raised by `lookHeight`. Axis "x" watches from +z; axis "z" watches from +x.
317
+ - resolveTopDown (function): function resolveTopDown(config: TopDownCameraConfig | undefined): ResolvedTopDown
318
+ - rightVector (function): function rightVector(yaw: number): Vec3 — Screen-right of a yaw: `forward × up` with up = +Y.
319
+ - rtsPanKeysConflict (function): function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean
320
+ - seatPose (function): function seatPose(follow: Vec3, yaw: number, local: { x?: number; y?: number; z?: number }, fov: number): CameraPose — World-space seat pose (cockpit/hood/rear) rigidly attached to the vehicle.
321
+ - shakeOffset (function): function shakeOffset(state: TraumaState, config: { maxOffset?: number; maxRoll?: number; exponent?: number; frequency?: number; } | undefined): ShakeOffset — Positional + roll shake for the current trauma. Shake magnitude is `trauma^exponent` so small hits stay subtle; the oscillation is deterministic per-seed pseudo-noise (no per-frame RNG, so identical inputs reproduce).
322
+ - shoulderPose (function): function shoulderPose(follow: Vec3, yaw: number, pitch: number, sideSign: number, shoulder: ResolvedShoulder): CameraPose — Over-the-shoulder camera pose. The boom sits behind the follow point along `yaw`, raised by `heightOffset`, and pushed sideways by `shoulderOffset` (sign set by `sideSign`: +1 right, -1 left). `pitch` tilts the look point.
323
+ - sideScrollFollowBlend (function): function sideScrollFollowBlend(followSmoothing: number, dt: number): number — Frame-rate independent follow blend for the side-scroll rig; `followSmoothing <= 0` hard-locks (blend 1) instead of freezing.
324
+ - smoothYaw (function): function smoothYaw(current: number, desired: number, speed: number, dt: number): number — Exponential yaw smoothing that respects wrap-around.
325
+ - smoothstep (function): function smoothstep(t: number): number
326
+ - speedToFov (function): function speedToFov(speed: number, curve: { base?: number; max?: number; speedForMax?: number } | undefined): number — Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`.
327
+ - springArmStep (function): function springArmStep(current: Vec3, desired: Vec3, damping: number, dt: number): Vec3 — Exponential spring-arm approach toward a desired point (frame-rate independent).
328
+ - stepTrauma (function): function stepTrauma(state: TraumaState, decayPerSecond: number, dt: number): void — Decay trauma linearly toward 0 and advance the shake clock.
329
+ - topDownPose (function): function topDownPose(follow: Vec3, resolved: ResolvedTopDown, fov: number): CameraPose — Camera pose for a fixed top-down / isometric rig. `pitch` is the elevation of the camera→target ray above the ground (PI/2 = straight down); `yaw` is the fixed azimuth (PI/4 reads isometric). Height sets zoom; horizontal boom is derived so the look angle stays constant as height changes.
330
+ - yawTo (function): function yawTo(from: Vec3, to: Vec3): number — Yaw that points from `from` toward `to` on the XZ plane (matches shell forward = (sin, cos)).
331
+
332
+ ### @jgengine/shell/camera/rigResolve
333
+
334
+ - resolveRigKind (function): function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind — Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least specific: an explicit `rig` field always wins; then `perspective: "first"` (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence of a rig's own config block selects that rig, checked in the fixed order below (#207.8) so a config carrying more than one block resolves deterministically instead of depending on object key order. Set `rig` explicitly to break a tie.
335
+ - turntableAsObserver (function): function turntableAsObserver(config: GameCameraConfig | undefined): GameCameraConfig — The turntable rig is a flat facade over the observer's point-orbit mode: map its `target`/`distance`/… onto an observer block so ObserverRig runs unchanged.
336
+
337
+ ### @jgengine/shell/camera/shakeChannel
338
+
339
+ - CameraShakeChannel (interface): interface CameraShakeChannel
340
+ - CameraShakeContext (const): const CameraShakeContext: React.Context<CameraShakeChannel>
341
+ - cameraShake (function): function cameraShake(amplitude: number, decayPerSecond?: number): void — Feed the default camera-shake channel from anywhere (see G7 hitstop cross-cut).
342
+ - createCameraShakeChannel (function): function createCameraShakeChannel(defaultDecayPerSecond = 1.6): CameraShakeChannel
343
+ - defaultCameraShakeChannel (const): const defaultCameraShakeChannel: CameraShakeChannel — Process-wide default channel. A shell mounts its own channel via `CameraShakeContext`, but game systems that have no React context (e.g. a `loop.onTick` reacting to `entity.died`) can import `cameraShake` and feed the default channel directly.
344
+ - useCameraShake (function): function useCameraShake(): CameraShakeChannel — The active rig's shake channel — call `.shake(...)` to add trauma from React UI.
345
+
346
+ ### @jgengine/shell/camera/shakeChannelMath
347
+
348
+ - CameraShakeChannel (interface): interface CameraShakeChannel
349
+ - createCameraShakeChannel (function): function createCameraShakeChannel(defaultDecayPerSecond = 1.6): CameraShakeChannel
350
+
351
+ ### @jgengine/shell/cartridge
352
+
353
+ - CartridgeAbilitySlot (interface): interface CartridgeAbilitySlot
354
+ - CartridgeConfig (type): type CartridgeConfig = CartridgeSpec & { name: string; panels: CartridgePanels; hud: { storageKey?: string; panels: readonly CartridgeHudPanelSpec[] }; screens: CartridgeScreens; theme?: Record<string, string>; world?: GameConfig["world"]; physics?: GameConfig["physics"]; assets?: GameConfig["assets…
355
+ - CartridgeHudPanelSpec (interface): interface CartridgeHudPanelSpec
356
+ - CartridgePanelItem (type): type CartridgePanelItem = | { kind: "vital"; stat: string; label: string; tone?: string; width?: number } | { kind: "xp"; width?: number } | { kind: "timer"; label: string } | { kind: "score"; source: "kills"; label: string; digits?: number } | { kind: "abilityBar"; icons: Record<string, string> } |…
357
+ - CartridgePanels (interface): interface CartridgePanels
358
+ - CartridgeResultLine (type): type CartridgeResultLine = { label: string; accent?: boolean } & ( | { source: "kills" | "level" } | { value: string | number } )
359
+ - CartridgeScreens (interface): interface CartridgeScreens
360
+ - cartridge (function): function cartridge(config: CartridgeConfig): PlayableGame
361
+
362
+ ### @jgengine/shell/defineGame
363
+
364
+ - GameConfig (type): type GameConfig<TAssetRef extends ModelAssetRef = ModelAssetRef> = EngineFields<TAssetRef> & PresentationFields
365
+ - defineGame (function): function defineGame<TAssetRef extends ModelAssetRef = ModelAssetRef>(config: GameConfig<TAssetRef>): PlayableGame
366
+
367
+ ### @jgengine/shell/devtools/CollisionDebugWorld
368
+
369
+ - CollisionDebugWorld (function): function CollisionDebugWorld(): React.JSX.Element | null — World-space collision debugger. Performs zero scene scans and zero raycasts when every layer is off. Mount only when shell devtools are enabled.
370
+
371
+ ### @jgengine/shell/devtools/DevtoolsOverlay
372
+
373
+ - DevtoolsOverlay (function): function DevtoolsOverlay({ open, ctx, playable, multiplayer, }: { open: boolean; ctx: GameContext; playable: PlayableGame; multiplayer: ShellMultiplayer | null; }): React.JSX.Element | null
374
+ - DevtoolsRendererProbe (function): function DevtoolsRendererProbe(): null
375
+ - applyStoredDevtoolsOverrides (function): function applyStoredDevtoolsOverrides(gameName: string): void
376
+ - buildFullReport (function): function buildFullReport(playable: PlayableGame): DevtoolsSnapshot & { game: string }
377
+ - buildLeanReport (function): function buildLeanReport(playable: PlayableGame): { game: any; at: any; why: string | null; frame: { fps: number; avgFrameMs: number; p95FrameMs: number; maxFrameMs: number; avgSimMs: number; maxSimMs: number; avgOutsideMs: number; maxOutsideMs: number; longFrames: any; samples: any; phases: any; } …
378
+ - persistDevtoolsOverrides (function): function persistDevtoolsOverrides(gameName: string): DevtoolsOverrides
379
+ - withDevtoolsLatency (function): function withDevtoolsLatency(multiplayer: ShellMultiplayer): ShellMultiplayer
380
+
381
+ ### @jgengine/shell/devtools/collisionDebug
382
+
383
+ - AimProbeConfig (interface): interface AimProbeConfig
384
+ - COLLISION_DEBUG_LAYERS (const): const COLLISION_DEBUG_LAYERS: readonly CollisionDebugLayer[]
385
+ - CollisionDebugController (interface): interface CollisionDebugController
386
+ - CollisionDebugLayer (type): type CollisionDebugLayer = | "hitboxes" | "bodies" | "projectiles" | "muzzles" | "aimLaser"
387
+ - CollisionDebugLayers (type): type CollisionDebugLayers = Record<CollisionDebugLayer, boolean>
388
+ - CollisionDebugListener (type): type CollisionDebugListener = () => void
389
+ - CollisionDebugState (interface): interface CollisionDebugState
390
+ - ProjectileDebugTrace (interface): interface ProjectileDebugTrace
391
+ - aimProbeNeeded (function): function aimProbeNeeded(layers: CollisionDebugLayers): boolean
392
+ - anyCollisionLayerOn (function): function anyCollisionLayerOn(layers: CollisionDebugLayers): boolean
393
+ - colliderScanNeeded (function): function colliderScanNeeded(layers: CollisionDebugLayers): boolean
394
+ - collisionDebug (const): const collisionDebug: CollisionDebugController
395
+ - createCollisionDebugController (function): function createCollisionDebugController(initial: CollisionDebugState = createDefaultCollisionDebugState()): CollisionDebugController
396
+ - createDefaultCollisionDebugState (function): function createDefaultCollisionDebugState(): CollisionDebugState
397
+ - projectileListenNeeded (function): function projectileListenNeeded(layers: CollisionDebugLayers): boolean
398
+
399
+ ### @jgengine/shell/devtools/collisionDebugMath
400
+
401
+ - AIM_DAMAGE_COLOR (const): const AIM_DAMAGE_COLOR: "#f87171"
402
+ - AIM_LASER_COLOR (const): const AIM_LASER_COLOR: "#a3e635"
403
+ - AIM_MISS_COLOR (const): const AIM_MISS_COLOR: "#94a3b8"
404
+ - AIM_SOLID_COLOR (const): const AIM_SOLID_COLOR: "#fbbf24"
405
+ - AimEndpointKind (type): type AimEndpointKind = "damage" | "solid" | "miss"
406
+ - AimLaserDebug (interface): interface AimLaserDebug
407
+ - BODY_WIRE_COLOR (const): const BODY_WIRE_COLOR: "#38bdf8"
408
+ - CollectDebugShapesInput (interface): interface CollectDebugShapesInput
409
+ - ComputeAimLaserInput (interface): interface ComputeAimLaserInput
410
+ - DebugShapeEntry (interface): interface DebugShapeEntry
411
+ - HITBOX_WIRE_COLOR (const): const HITBOX_WIRE_COLOR: "#f472b6"
412
+ - PROJECTILE_PATH_COLOR (const): const PROJECTILE_PATH_COLOR: "#fde68a"
413
+ - classifyAimEndpoint (function): function classifyAimEndpoint(hit: Pick<SceneRaycastHit, "damageEligible"> | null | undefined): AimEndpointKind
414
+ - collectDebugShapes (function): function collectDebugShapes(input: CollectDebugShapesInput): DebugShapeEntry[] — Collects world-space collider wireframe entries for enabled layers. Returns [] and performs no entity/object iteration when both hitbox/body layers are off.
415
+ - computeAimLaser (function): function computeAimLaser(input: ComputeAimLaserInput): AimLaserDebug | null — Builds the authoritative aim-laser segment using resolveShot + scene raycast (same seam as projectile prediction/settlement). Zero queries when aimLaser is off.
416
+ - muzzleMarkerFromOrigin (function): function muzzleMarkerFromOrigin(origin: EntityPosition): { center: EntityPosition; radius: number; color: string; }
417
+ - pointAlongRay (function): function pointAlongRay(origin: EntityPosition, direction: EntityPosition, distance: number): EntityPosition
418
+ - shapeWorldCenter (function): function shapeWorldCenter(collider: ResolvedCollider, position: EntityPosition, rotationY: number): EntityPosition
419
+
420
+ ### @jgengine/shell/environment
421
+
422
+ - Daylight (function): function Daylight({ sky, fog, sun, ambient, lights = true }: DaylightProps = {}): React.JSX.Element
423
+ - DaylightCycleConfig (interface): interface DaylightCycleConfig
424
+ - DaylightProps (interface): interface DaylightProps
425
+ - DaylightState (interface): interface DaylightState
426
+ - EnvironmentScene (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element
427
+ - EnvironmentSceneProps (interface): interface EnvironmentSceneProps
428
+ - SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
429
+ - SkyDaylight (function): function SkyDaylight({ sky, lights = true }: SkyDaylightProps): React.JSX.Element — Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
430
+ - SkyDaylightProps (interface): interface SkyDaylightProps
431
+ - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
432
+ - SkyDomeProps (interface): interface SkyDomeProps
433
+ - SkyLightOwnership (type): type SkyLightOwnership = "authored" | "sky-default" — Policy for composing sky backdrops with `PlayableGame.lighting`: - authored lighting present → sky renders dome + fog only; lights stay game-owned - no authored lighting → sky may emit its default sun/hemisphere with the dome Time-of-day never rewrites configured lights; it only drives sky colors/fog (and sky-owned lights when the game did not author lighting).
434
+ - TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock, lights = true }: TimeOfDayDaylightProps): React.JSX.Element — Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock` are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so only dome colors and fog track the day fraction.
435
+ - TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
436
+ - daylightStateAt (function): function daylightStateAt(dayFraction: number, config: DaylightCycleConfig = {}): DaylightState — Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk), lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
437
+ - lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
438
+ - resolveSkyLightOwnership (function): function resolveSkyLightOwnership(hasAuthoredLighting: boolean): SkyLightOwnership
439
+ - skyEmitsLights (function): function skyEmitsLights(ownership: SkyLightOwnership): boolean
440
+
441
+ ### @jgengine/shell/environment/Daylight
442
+
443
+ - Daylight (function): function Daylight({ sky, fog, sun, ambient, lights = true }: DaylightProps = {}): React.JSX.Element
444
+ - DaylightProps (interface): interface DaylightProps
445
+ - SkyDaylight (function): function SkyDaylight({ sky, lights = true }: SkyDaylightProps): React.JSX.Element — Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
446
+ - SkyDaylightProps (interface): interface SkyDaylightProps
447
+ - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
448
+ - SkyDomeProps (interface): interface SkyDomeProps
449
+ - TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock, lights = true }: TimeOfDayDaylightProps): React.JSX.Element — Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock` are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so only dome colors and fog track the day fraction.
450
+ - TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
451
+
452
+ ### @jgengine/shell/environment/EnvironmentScene
453
+
454
+ - EnvironmentScene (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element
455
+ - EnvironmentSceneProps (interface): interface EnvironmentSceneProps
456
+
457
+ ### @jgengine/shell/environment/GroundPad
458
+
459
+ - GroundPad (function): function GroundPad({ pad, field }: GroundPadProps): React.JSX.Element
460
+ - GroundPadProps (interface): interface GroundPadProps
461
+
462
+ ### @jgengine/shell/environment/daylightCycle
463
+
464
+ - DEFAULT_DAY_AMBIENT_INTENSITY (const): const DEFAULT_DAY_AMBIENT_INTENSITY: 0.6
465
+ - DEFAULT_DAY_SKY_BOTTOM (const): const DEFAULT_DAY_SKY_BOTTOM: "#e3f4ff"
466
+ - DEFAULT_DAY_SKY_TOP (const): const DEFAULT_DAY_SKY_TOP: "#3fa4f2"
467
+ - DEFAULT_DAY_SUN_INTENSITY (const): const DEFAULT_DAY_SUN_INTENSITY: 1
468
+ - DaylightCycleConfig (interface): interface DaylightCycleConfig
469
+ - DaylightState (interface): interface DaylightState
470
+ - SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
471
+ - daylightStateAt (function): function daylightStateAt(dayFraction: number, config: DaylightCycleConfig = {}): DaylightState — Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk), lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
472
+ - lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
473
+
474
+ ### @jgengine/shell/environment/groundPadMath
475
+
476
+ - PAD_THICKNESS (const): const PAD_THICKNESS: 0.1
477
+ - PadShape (type): type PadShape = { circular: true; radius: number } | { circular: false; width: number; depth: number }
478
+ - resolvePadMeshY (function): function resolvePadMeshY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number
479
+ - resolvePadShape (function): function resolvePadShape(size: PadSize): PadShape
480
+ - resolvePadSurfaceY (function): function resolvePadSurfaceY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number
481
+
482
+ ### @jgengine/shell/environment/index
483
+
484
+ - Daylight (function): function Daylight({ sky, fog, sun, ambient, lights = true }: DaylightProps = {}): React.JSX.Element
485
+ - DaylightCycleConfig (interface): interface DaylightCycleConfig
486
+ - DaylightProps (interface): interface DaylightProps
487
+ - DaylightState (interface): interface DaylightState
488
+ - EnvironmentScene (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element
489
+ - EnvironmentSceneProps (interface): interface EnvironmentSceneProps
490
+ - SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
491
+ - SkyDaylight (function): function SkyDaylight({ sky, lights = true }: SkyDaylightProps): React.JSX.Element — Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
492
+ - SkyDaylightProps (interface): interface SkyDaylightProps
493
+ - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
494
+ - SkyDomeProps (interface): interface SkyDomeProps
495
+ - SkyLightOwnership (type): type SkyLightOwnership = "authored" | "sky-default" — Policy for composing sky backdrops with `PlayableGame.lighting`: - authored lighting present → sky renders dome + fog only; lights stay game-owned - no authored lighting → sky may emit its default sun/hemisphere with the dome Time-of-day never rewrites configured lights; it only drives sky colors/fog (and sky-owned lights when the game did not author lighting).
496
+ - TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock, lights = true }: TimeOfDayDaylightProps): React.JSX.Element — Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock` are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so only dome colors and fog track the day fraction.
497
+ - TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
498
+ - daylightStateAt (function): function daylightStateAt(dayFraction: number, config: DaylightCycleConfig = {}): DaylightState — Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk), lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
499
+ - lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
500
+ - resolveSkyLightOwnership (function): function resolveSkyLightOwnership(hasAuthoredLighting: boolean): SkyLightOwnership
501
+ - skyEmitsLights (function): function skyEmitsLights(ownership: SkyLightOwnership): boolean
502
+
503
+ ### @jgengine/shell/environment/skyLightingPolicy
504
+
505
+ - SkyLightOwnership (type): type SkyLightOwnership = "authored" | "sky-default" — Policy for composing sky backdrops with `PlayableGame.lighting`: - authored lighting present → sky renders dome + fog only; lights stay game-owned - no authored lighting → sky may emit its default sun/hemisphere with the dome Time-of-day never rewrites configured lights; it only drives sky colors/fog (and sky-owned lights when the game did not author lighting).
506
+ - resolveSkyLightOwnership (function): function resolveSkyLightOwnership(hasAuthoredLighting: boolean): SkyLightOwnership
507
+ - skyEmitsLights (function): function skyEmitsLights(ownership: SkyLightOwnership): boolean
508
+
509
+ ### @jgengine/shell/input/mouseLook
510
+
511
+ - MouseLookAim (interface): interface MouseLookAim
512
+ - MouseLookOptions (interface): interface MouseLookOptions
513
+ - MouseLookTracker (interface): interface MouseLookTracker — The analog mouse-look service chase/orbit-cam games hand-rolled (#282.8) — pointer-lock lifecycle plus delta accumulation into a yaw/pitch aim, decoupled from the first-person rig. Attach it to the canvas, read `aim()` from `onTick`/`useFrame`, dispose on unmount.
514
+ - createMouseLookTracker (function): function createMouseLookTracker(element: HTMLElement, options: MouseLookOptions = {}): MouseLookTracker
515
+
516
+ ### @jgengine/shell/map/MapMarkerBeacons
517
+
518
+ - MapMarkerBeacons (function): function MapMarkerBeacons({ markers, kindStyles, height = 5 }: MapMarkerBeaconsProps): React.JSX.Element — World-space beacons for map markers (the visible in-world side of a ping): a floating diamond over a soft light beam, colored by marker kind. Wire it through `PlayableGame.WorldOverlay`.
519
+ - MapMarkerBeaconsProps (interface): interface MapMarkerBeaconsProps
520
+
521
+ ### @jgengine/shell/map/index
522
+
523
+ - BakeTerrainMapOptions (interface): interface BakeTerrainMapOptions
524
+ - BakedMap (interface): interface BakedMap
525
+ - MapBakeBounds (interface): interface MapBakeBounds
526
+ - MapMarkerBeacons (function): function MapMarkerBeacons({ markers, kindStyles, height = 5 }: MapMarkerBeaconsProps): React.JSX.Element — World-space beacons for map markers (the visible in-world side of a ping): a floating diamond over a soft light beam, colored by marker kind. Wire it through `PlayableGame.WorldOverlay`.
527
+ - MapMarkerBeaconsProps (interface): interface MapMarkerBeaconsProps
528
+ - bakeTerrainMap (function): function bakeTerrainMap(field: TerrainField, bounds: MapBakeBounds, options: BakeTerrainMapOptions = {}): BakedMap | null — Bake a top-down image of a `TerrainField` (or `RegionField`) over `bounds` for the react `Minimap` / `WorldMap` background. Runs in the browser via a 2D canvas; renderer-side, so it lives in the shell.
529
+
530
+ ### @jgengine/shell/map/terrainMap
531
+
532
+ - BakeTerrainMapOptions (interface): interface BakeTerrainMapOptions
533
+ - BakedMap (interface): interface BakedMap
534
+ - MapBakeBounds (interface): interface MapBakeBounds
535
+ - bakeTerrainMap (function): function bakeTerrainMap(field: TerrainField, bounds: MapBakeBounds, options: BakeTerrainMapOptions = {}): BakedMap | null — Bake a top-down image of a `TerrainField` (or `RegionField`) over `bounds` for the react `Minimap` / `WorldMap` background. Runs in the browser via a 2D canvas; renderer-side, so it lives in the shell.
536
+
537
+ ### @jgengine/shell/materialOverride
538
+
539
+ - MaterialOverrideOptions (interface): interface MaterialOverrideOptions
540
+ - applyMaterialOverride (function): function applyMaterialOverride(root: THREE.Object3D, override: ModelMaterialOverride, options?: MaterialOverrideOptions): void
541
+
542
+ ### @jgengine/shell/multiplayer
543
+
544
+ - DEFAULT_FEED_ACTIONS (const): const DEFAULT_FEED_ACTIONS: string[]
545
+ - ResolveShellMultiplayerArgs (type): type ResolveShellMultiplayerArgs = { game: GameDefinition; gameId: string; url?: string; userId?: string; force?: boolean; feedActions?: string[]; }
546
+ - ShellMultiplayer (type): type ShellMultiplayer = MultiplayerSession
547
+ - randomPlayerId (function): function randomPlayerId(): string
548
+ - resolvePeerShellMultiplayer (function): function resolvePeerShellMultiplayer(args: { gameId: string; role: "host" | "join"; room?: string; userId?: string; feedActions?: string[]; }): Promise<ShellMultiplayer & { close: () => void }>
549
+ - resolveShellMultiplayer (function): function resolveShellMultiplayer(args: ResolveShellMultiplayerArgs): ShellMultiplayer | null
550
+
551
+ ### @jgengine/shell/pointer/PointerOverlays
552
+
553
+ - ContextMenuView (function): function ContextMenuView({ menu, x, y, onPick, onClose, }: { menu: ContextMenu; x: number; y: number; onPick: (verb: ContextVerb) => void; onClose: () => void; }): React.JSX.Element
554
+ - MarqueeBox (function): function MarqueeBox({ rect }: { rect: ScreenRect }): React.JSX.Element
555
+
556
+ ### @jgengine/shell/pointer/PointerProbe
557
+
558
+ - PointerProbe (function): function PointerProbe({ service }: { service: PointerService }): null
559
+
560
+ ### @jgengine/shell/pointer/pointerService
561
+
562
+ - POINTER_ENTITY_KEY (const): const POINTER_ENTITY_KEY: "jgEntityId"
563
+ - POINTER_OBJECT_KEY (const): const POINTER_OBJECT_KEY: "jgObjectId"
564
+ - PointerHitFilter (type): type PointerHitFilter = (object: THREE.Object3D) => boolean
565
+ - PointerService (interface): interface PointerService
566
+ - createPointerService (function): function createPointerService(): PointerService
567
+
568
+ ### @jgengine/shell/registry
569
+
570
+ - GameRegistry (type): type GameRegistry = Record<string, () => Promise<PlayableGame>>
571
+ - PlayableGame (type): type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity, RenderObject>
572
+ - RenderEntity (type): type RenderEntity = (entity: SceneEntity) => ReactNode
573
+ - RenderObject (type): type RenderObject = (object: SceneObject) => ReactNode
574
+ - resolveGameLoader (function): function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined
575
+
576
+ ### @jgengine/shell/render/modelRender
577
+
578
+ - MaterialCache (interface): interface MaterialCache
579
+ - PAINT_TEXTURE_SIZE (const): const PAINT_TEXTURE_SIZE: 512
580
+ - PaintCanvas (interface): interface PaintCanvas
581
+ - applyPaintTexture (function): function applyPaintTexture(root: THREE.Object3D, paint: PaintCanvas): void
582
+ - applyPaintTextureToMaterials (function): function applyPaintTextureToMaterials(materials: readonly THREE.MeshStandardMaterial[], paint: PaintCanvas): void
583
+ - cacheStandardMaterials (function): function cacheStandardMaterials(root: THREE.Object3D, into?: MaterialCache | null): MaterialCache
584
+ - cloneModelScene (function): function cloneModelScene(source: THREE.Object3D, options?: { cloneMaterials?: boolean }): THREE.Object3D
585
+ - createPaintCanvas (function): function createPaintCanvas(seed: THREE.MeshStandardMaterial, size = PAINT_TEXTURE_SIZE): PaintCanvas
586
+ - disposeClonedMaterials (function): function disposeClonedMaterials(root: THREE.Object3D): void
587
+ - drawPaintStrokes (function): function drawPaintStrokes(paint: PaintCanvas, strokes: readonly PaintStroke[]): void
588
+ - standardMaterialsOf (function): function standardMaterialsOf(root: THREE.Object3D): THREE.MeshStandardMaterial[]
589
+ - syncPaintCanvas (function): function syncPaintCanvas(paint: PaintCanvas, seedColor: THREE.Color, strokes: readonly PaintStroke[], drawnCount: number): number
590
+
591
+ ### @jgengine/shell/render/resolveModel
592
+
593
+ - ModelResolveContext (interface): interface ModelResolveContext
594
+ - resolveModel (function): function resolveModel(value: string | ModelConfig | undefined, assets: AssetCatalog, context?: ModelResolveContext): ModelConfig | undefined — Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog ids throw — silent generic-primitive fallback only happens when the mapping omits the key entirely (or uses tryResolveCatalogModel for optional ids).
595
+ - tryResolveCatalogModel (function): function tryResolveCatalogModel(id: string, assets: AssetCatalog): ModelConfig | undefined — Soft lookup used when an object catalog id may double as a model asset id.
596
+
597
+ ### @jgengine/shell/replay/useSessionRecorder
598
+
599
+ - RecordedPose (interface): interface RecordedPose
600
+ - useSessionRecorder (function): function useSessionRecorder(entityId: string, options?: RecordingBufferOptions): RecordingBuffer<RecordedPose> — Session-recording buffer (#120) for replay / photo mode / kill-cam: records an entity's pose on game-time every frame into a `RecordingBuffer`, which a game can then `seek()` to scrub, drive an observer cam ghost, or export a kill-cam clip. Recording rides on `ctx.time.now()`, so pause/fast-forward scrub the recording exactly like the live sim.
601
+
602
+ ### @jgengine/shell/settings/QuickControls
603
+
604
+ - QuickControls (function): function QuickControls({ controller }: { controller: SettingsController }): React.JSX.Element | null
605
+
606
+ ### @jgengine/shell/settings/SettingsChrome
607
+
608
+ - SettingsChrome (function): function SettingsChrome(): React.JSX.Element | null
609
+
610
+ ### @jgengine/shell/settings/SettingsMenu
611
+
612
+ - SettingsMenu (function): function SettingsMenu({ controller, onClose, initialTab, }: { controller: SettingsController; onClose: () => void; initialTab?: string; }): React.JSX.Element | null
613
+
614
+ ### @jgengine/shell/settings/SettingsRuntime
615
+
616
+ - SettingsRuntime (function): function SettingsRuntime({ variant, surface, actions, children, ...input }: SettingsRuntimeProps): React.JSX.Element
617
+ - SettingsRuntimeProps (interface): interface SettingsRuntimeProps extends SettingsControllerInput
618
+
619
+ ### @jgengine/shell/settings/appliedSettings
620
+
621
+ - AudioSettingsBridge (function): function AudioSettingsBridge({ store, engine, buses, }: { store: SettingsStore; engine: AudioEngine; buses: Record<string, AudioBusDef> | undefined; }): null
622
+ - useGraphicsSettings (function): function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean): { shadows: boolean; dpr: number; uiScale: number }
623
+ - useSettingsRevision (function): function useSettingsRevision(store: SettingsStore): number
624
+
625
+ ### @jgengine/shell/settings/settingsController
626
+
627
+ - SettingsControllerInput (interface): interface SettingsControllerInput
628
+ - useSettingsCategories (function): function useSettingsCategories(config: SettingsControllerInput): SettingsCategoryView[]
629
+
630
+ ### @jgengine/shell/structures
631
+
632
+ - BuildingBlock (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element
633
+ - BuildingBlockProps (interface): interface BuildingBlockProps
634
+ - GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
635
+ - GeneratedBuildingProps (interface): interface GeneratedBuildingProps
636
+ - InstancedBuildingPlacement (interface): interface InstancedBuildingPlacement
637
+ - InstancedBuildings (function): function InstancedBuildings({ buildings, palette, visibleKinds }: InstancedBuildingsProps): React.JSX.Element | null
638
+ - InstancedBuildingsProps (interface): interface InstancedBuildingsProps
639
+ - PlacementGhost (function): function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }: PlacementGhostProps): React.JSX.Element | null
640
+ - PlacementGhostProps (interface): interface PlacementGhostProps
641
+
642
+ ### @jgengine/shell/structures/GeneratedBuilding
643
+
644
+ - BuildingBlock (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element
645
+ - BuildingBlockProps (interface): interface BuildingBlockProps
646
+ - BuildingFacade (type): type BuildingFacade = "front" | "back" | "left" | "right" | "roof"
647
+ - BuildingKitRenderer (interface): interface BuildingKitRenderer
648
+ - BuildingMaterialPalette (interface): interface BuildingMaterialPalette
649
+ - BuildingPartKind (type): type BuildingPartKind = | "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner"
650
+ - BuildingPartPlacement (interface): interface BuildingPartPlacement
651
+ - GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
652
+ - GeneratedBuildingData (interface): interface GeneratedBuildingData
653
+ - GeneratedBuildingProps (interface): interface GeneratedBuildingProps
654
+ - InstancedBuildingPlacement (interface): interface InstancedBuildingPlacement
655
+ - InstancedBuildings (function): function InstancedBuildings({ buildings, palette, visibleKinds }: InstancedBuildingsProps): React.JSX.Element | null
656
+ - InstancedBuildingsProps (interface): interface InstancedBuildingsProps
657
+
658
+ ### @jgengine/shell/structures/PlacementGhost
659
+
660
+ - PlacementGhost (function): function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }: PlacementGhostProps): React.JSX.Element | null
661
+ - PlacementGhostProps (interface): interface PlacementGhostProps
662
+
663
+ ### @jgengine/shell/structures/index
664
+
665
+ - BuildingBlock (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element
666
+ - BuildingBlockProps (interface): interface BuildingBlockProps
667
+ - GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
668
+ - GeneratedBuildingProps (interface): interface GeneratedBuildingProps
669
+ - InstancedBuildingPlacement (interface): interface InstancedBuildingPlacement
670
+ - InstancedBuildings (function): function InstancedBuildings({ buildings, palette, visibleKinds }: InstancedBuildingsProps): React.JSX.Element | null
671
+ - InstancedBuildingsProps (interface): interface InstancedBuildingsProps
672
+ - PlacementGhost (function): function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }: PlacementGhostProps): React.JSX.Element | null
673
+ - PlacementGhostProps (interface): interface PlacementGhostProps
674
+
675
+ ### @jgengine/shell/terrain
676
+
677
+ - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element — Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
678
+ - CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
679
+ - DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
680
+ - EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
681
+ - EditableGroundProps (interface): interface EditableGroundProps
682
+ - FieldGroundOptions (interface): interface FieldGroundOptions
683
+ - GrassBladeGeometryOptions (interface): interface GrassBladeGeometryOptions
684
+ - GrassField (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, ...meshPr…
685
+ - GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
686
+ - GrassMaterialHandle (interface): interface GrassMaterialHandle
687
+ - GrassMaterialOptions (interface): interface GrassMaterialOptions
688
+ - GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
689
+ - GrassShaderUniforms (interface): interface GrassShaderUniforms
690
+ - GrassWindOptions (interface): interface GrassWindOptions
691
+ - ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
692
+ - ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
693
+ - ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
694
+ - ResolvedGrassBladeGeometryOptions (interface): interface ResolvedGrassBladeGeometryOptions
695
+ - ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
696
+ - ResolvedTerrainSize (interface): interface ResolvedTerrainSize
697
+ - TerraformBrushCursor (function): function TerraformBrushCursor({ center, y = 0.05, radius, mode }: TerraformBrushCursorProps): React.JSX.Element | null
698
+ - TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
699
+ - TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
700
+ - TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
701
+ - TerrainSeed (type): type TerrainSeed = number | string
702
+ - TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
703
+ - createFieldGroundGeometry (function): function createFieldGroundGeometry(field: TerrainField, options: FieldGroundOptions = {}): THREE.BufferGeometry — Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
704
+ - createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
705
+ - createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
706
+ - createProceduralGroundGeometry (function): function createProceduralGroundGeometry(config: ProceduralTerrainConfig = {}, colors: TerrainVertexColorOptions = {}): THREE.BufferGeometry
707
+ - createProceduralTerrainSampler (function): function createProceduralTerrainSampler(config: ProceduralTerrainConfig = {}): TerrainHeightSampler
708
+ - createSeededRandom (function): function createSeededRandom(seed: TerrainSeed = 1): () => number
709
+ - hashNoise2 (function): function hashNoise2(x: number, z: number, seed: TerrainSeed = 1): number
710
+ - normalizeHeightBlend (function): function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number
711
+ - resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
712
+ - resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
713
+ - resolveGrassWind (function): function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>
714
+ - resolveTerrainSegments (function): function resolveTerrainSegments(segments: ProceduralTerrainConfig["segments"] = 96): ResolvedTerrainSegments
715
+ - resolveTerrainSize (function): function resolveTerrainSize(size: TerrainArea = 40): ResolvedTerrainSize
716
+ - seedToUint32 (function): function seedToUint32(seed: TerrainSeed = 1): number
717
+ - toNoiseFieldConfig (function): function toNoiseFieldConfig(config: ProceduralTerrainConfig = {}): NoiseFieldConfig
718
+
719
+ ### @jgengine/shell/terrain/CarvedTerrain
720
+
721
+ - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element — Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
722
+ - CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
723
+
724
+ ### @jgengine/shell/terrain/EditableGround
725
+
726
+ - EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
727
+ - EditableGroundProps (interface): interface EditableGroundProps
728
+
729
+ ### @jgengine/shell/terrain/GrassField
730
+
731
+ - DEFAULT_GRASS_COUNT (const): const DEFAULT_GRASS_COUNT: 1500
732
+ - DEFAULT_GRASS_DENSITY (const): const DEFAULT_GRASS_DENSITY: 1
733
+ - GrassField (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, ...meshPr…
734
+ - GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
735
+ - resolveGrassInstanceBudget (function): function resolveGrassInstanceBudget(count: number, density: number, budget?: number): number
736
+
737
+ ### @jgengine/shell/terrain/ProceduralGround
738
+
739
+ - ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
740
+ - ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
741
+
742
+ ### @jgengine/shell/terrain/TerraformBrushCursor
743
+
744
+ - TerraformBrushCursor (function): function TerraformBrushCursor({ center, y = 0.05, radius, mode }: TerraformBrushCursorProps): React.JSX.Element | null
745
+ - TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
746
+
747
+ ### @jgengine/shell/terrain/grassBudget
748
+
749
+ - DEFAULT_GRASS_COUNT (const): const DEFAULT_GRASS_COUNT: 1500
750
+ - DEFAULT_GRASS_DENSITY (const): const DEFAULT_GRASS_DENSITY: 1
751
+ - resolveGrassInstanceBudget (function): function resolveGrassInstanceBudget(count: number, density: number, budget?: number): number
752
+
753
+ ### @jgengine/shell/terrain/grassGeometry
754
+
755
+ - GrassBladeGeometryOptions (interface): interface GrassBladeGeometryOptions
756
+ - GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
757
+ - ResolvedGrassBladeGeometryOptions (interface): interface ResolvedGrassBladeGeometryOptions
758
+ - createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
759
+ - resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
760
+ - resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
761
+
762
+ ### @jgengine/shell/terrain/grassMaterial
763
+
764
+ - DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
765
+ - GrassMaterialHandle (interface): interface GrassMaterialHandle
766
+ - GrassMaterialOptions (interface): interface GrassMaterialOptions
767
+ - GrassShaderUniforms (interface): interface GrassShaderUniforms
768
+ - GrassWindOptions (interface): interface GrassWindOptions
769
+ - createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
770
+ - resolveGrassWind (function): function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>
771
+
772
+ ### @jgengine/shell/terrain/index
773
+
774
+ - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element — Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
775
+ - CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
776
+ - DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
777
+ - EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
778
+ - EditableGroundProps (interface): interface EditableGroundProps
779
+ - FieldGroundOptions (interface): interface FieldGroundOptions
780
+ - GrassBladeGeometryOptions (interface): interface GrassBladeGeometryOptions
781
+ - GrassField (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, ...meshPr…
782
+ - GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
783
+ - GrassMaterialHandle (interface): interface GrassMaterialHandle
784
+ - GrassMaterialOptions (interface): interface GrassMaterialOptions
785
+ - GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
786
+ - GrassShaderUniforms (interface): interface GrassShaderUniforms
787
+ - GrassWindOptions (interface): interface GrassWindOptions
788
+ - ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
789
+ - ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
790
+ - ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
791
+ - ResolvedGrassBladeGeometryOptions (interface): interface ResolvedGrassBladeGeometryOptions
792
+ - ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
793
+ - ResolvedTerrainSize (interface): interface ResolvedTerrainSize
794
+ - TerraformBrushCursor (function): function TerraformBrushCursor({ center, y = 0.05, radius, mode }: TerraformBrushCursorProps): React.JSX.Element | null
795
+ - TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
796
+ - TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
797
+ - TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
798
+ - TerrainSeed (type): type TerrainSeed = number | string
799
+ - TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
800
+ - createFieldGroundGeometry (function): function createFieldGroundGeometry(field: TerrainField, options: FieldGroundOptions = {}): THREE.BufferGeometry — Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
801
+ - createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
802
+ - createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
803
+ - createProceduralGroundGeometry (function): function createProceduralGroundGeometry(config: ProceduralTerrainConfig = {}, colors: TerrainVertexColorOptions = {}): THREE.BufferGeometry
804
+ - createProceduralTerrainSampler (function): function createProceduralTerrainSampler(config: ProceduralTerrainConfig = {}): TerrainHeightSampler
805
+ - createSeededRandom (function): function createSeededRandom(seed: TerrainSeed = 1): () => number
806
+ - hashNoise2 (function): function hashNoise2(x: number, z: number, seed: TerrainSeed = 1): number
807
+ - normalizeHeightBlend (function): function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number
808
+ - resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
809
+ - resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
810
+ - resolveGrassWind (function): function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>
811
+ - resolveTerrainSegments (function): function resolveTerrainSegments(segments: ProceduralTerrainConfig["segments"] = 96): ResolvedTerrainSegments
812
+ - resolveTerrainSize (function): function resolveTerrainSize(size: TerrainArea = 40): ResolvedTerrainSize
813
+ - seedToUint32 (function): function seedToUint32(seed: TerrainSeed = 1): number
814
+ - toNoiseFieldConfig (function): function toNoiseFieldConfig(config: ProceduralTerrainConfig = {}): NoiseFieldConfig
815
+
816
+ ### @jgengine/shell/terrain/random
817
+
818
+ - TerrainSeed (type): type TerrainSeed = number | string
819
+ - createSeededRandom (function): function createSeededRandom(seed: TerrainSeed = 1): () => number
820
+ - hashNoise2 (function): function hashNoise2(x: number, z: number, seed: TerrainSeed = 1): number
821
+ - seedToUint32 (function): function seedToUint32(seed: TerrainSeed = 1): number
822
+
823
+ ### @jgengine/shell/terrain/terrainMath
824
+
825
+ - FieldGroundOptions (interface): interface FieldGroundOptions
826
+ - ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
827
+ - ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
828
+ - ResolvedTerrainSize (interface): interface ResolvedTerrainSize
829
+ - TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
830
+ - TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
831
+ - TerrainPaletteSampler (type): type TerrainPaletteSampler = (x: number, z: number) => { low: string; high: string; waterline?: string } — Per-position palette override for multi-biome ground coloring — `createTerrainPaletteSampler` from `@jgengine/core/world/terrain` returns exactly this shape.
832
+ - TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
833
+ - createFieldGroundGeometry (function): function createFieldGroundGeometry(field: TerrainField, options: FieldGroundOptions = {}): THREE.BufferGeometry — Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
834
+ - createProceduralGroundGeometry (function): function createProceduralGroundGeometry(config: ProceduralTerrainConfig = {}, colors: TerrainVertexColorOptions = {}): THREE.BufferGeometry
835
+ - createProceduralTerrainSampler (function): function createProceduralTerrainSampler(config: ProceduralTerrainConfig = {}): TerrainHeightSampler
836
+ - normalizeHeightBlend (function): function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number
837
+ - resolveTerrainSegments (function): function resolveTerrainSegments(segments: ProceduralTerrainConfig["segments"] = 96): ResolvedTerrainSegments
838
+ - resolveTerrainSize (function): function resolveTerrainSize(size: TerrainArea = 40): ResolvedTerrainSize
839
+ - toNoiseFieldConfig (function): function toNoiseFieldConfig(config: ProceduralTerrainConfig = {}): NoiseFieldConfig
840
+
841
+ ### @jgengine/shell/touch/OrientationHint
842
+
843
+ - OrientationHint (function): function OrientationHint({ wanted }: { wanted: "landscape" | "portrait" }): React.JSX.Element | null
844
+
845
+ ### @jgengine/shell/touch/TouchControlsOverlay
846
+
847
+ - TouchCodeSink (interface): interface TouchCodeSink
848
+ - TouchControlsDock (function): function TouchControlsDock({ scheme, sink, scale = 1 }: { scheme: TouchScheme; sink: TouchCodeSink; scale?: number }): React.JSX.Element
849
+ - TouchPlaySurface (function): function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }: { scheme: TouchScheme; sink: TouchCodeSink; yawRef: MutableRefObject<number>; pitchRef: MutableRefObject<number>; maxPitch: number; onPrimaryTap: () => void; }): React.JSX.Element
850
+ - primaryButtonOffsets (function): function primaryButtonOffsets(count: number, scale = 1): { right: number; bottom: number }[] | null — Thumb-arc placement for primary buttons around the bottom-right corner: up to three on an inner ring, the rest on an outer ring. Null means too many buttons for an arc — the dock falls back to a wrapping grid.
851
+ - touchDockClearance (function): function touchDockClearance(scheme: TouchScheme | null, scale = 1): number — Vertical space (px, excluding device safe areas) the dock occupies above the bottom edge. The shell publishes it as `--jg-hud-dock-clearance` so `HudCanvas` regions never collide with touch controls.
852
+
853
+ ### @jgengine/shell/visibility/CullingProvider
854
+
855
+ - CullingProvider (function): function CullingProvider({ config, children }: { config: VisibilityConfig | undefined; children: ReactNode }): ReactNode — Drives automatic frustum + distance culling for every entity and placed object. It reads the live render camera each frame, updates the engine VisibilitySystem, and exposes a predicate the entity/object markers consult to toggle `group.visible` — objects fully outside the view (plus a conservative preload margin) are never submitted to the renderer, without unmounting them or touching gameplay. UI, sky, terrain, and environment live outside this subtree and are unaffected.
856
+ - useRenderVisibility (function): function useRenderVisibility(): MutableRefObject<VisiblePredicate> — Read the current render-visibility predicate. Backward compatible: with no CullingProvider mounted (or culling disabled) it returns an always-visible ref, so a marker that consults it behaves exactly as before this feature existed.
857
+
858
+ ### @jgengine/shell/vision/FrustumSensorHud
859
+
860
+ - FrustumSensorProbeOptions (interface): interface FrustumSensorProbeOptions extends FramingConfig
861
+ - FrustumSensorReadout (function): function FrustumSensorReadout(props: FrustumSensorProbeOptions & { wrapperClassName?: string; className?: string }): React.JSX.Element — Renders inside the Canvas (needs the live camera via `useFrame`/`useThree`) but portals a real HTML readout via drei's `Html fullscreen` — a photo-mode "is this subject framed" HUD.
862
+ - frustumSampleDisplayEqual (function): function frustumSampleDisplayEqual(a: FrustumSample | null, b: FrustumSample | null): boolean
863
+ - useFrustumSensor (function): function useFrustumSensor(options: FrustumSensorProbeOptions): FrustumSample | null
864
+
865
+ ### @jgengine/shell/vision/HiddenStateProbeHud
866
+
867
+ - SensorReadoutMeter (function): function SensorReadoutMeter({ label, reading, className }: SensorReadoutMeterProps): React.JSX.Element — A handheld-sensor readout: needle strength bar + the raw reading, or a "no signal" idle state.
868
+ - SensorReadoutMeterProps (interface): interface SensorReadoutMeterProps
869
+ - useHiddenStateProbe (function): function useHiddenStateProbe(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): any — Reads a hidden zone/entity state variable in range (EMF / thermometer / geiger style sensor verb, #116).
870
+
871
+ ### @jgengine/shell/vision/RevealVision
872
+
873
+ - RevealHighlights (function): function RevealHighlights(props: RevealHighlightsProps): React.JSX.Element | null — Screen-space reveal effect (#115) — highlights tagged entities through occluders (Dark Sight / detective-vision / wallhack style). Renders with `depthTest: false` so the highlight draws over any wall standing between the origin and the revealed entity, rather than the usual depth-sorted scene.
874
+ - RevealHighlightsProps (interface): interface RevealHighlightsProps extends RevealVisionOptions
875
+ - RevealScreenTint (function): function RevealScreenTint({ enabled, color = "rgba(56, 189, 248, 0.16)", className }: RevealScreenTintProps): React.JSX.Element | null — Full-screen desaturating tint that reads as "vision mode is on" (Dark Sight / thermal / detective vision).
876
+ - RevealScreenTintProps (interface): interface RevealScreenTintProps
877
+ - RevealVisionOptions (interface): interface RevealVisionOptions
878
+ - useRevealHits (function): function useRevealHits(options: RevealVisionOptions): readonly RevealHit[] — Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene.
879
+
880
+ ### @jgengine/shell/vision/frustumSampleEqual
881
+
882
+ - frustumSampleDisplayEqual (function): function frustumSampleDisplayEqual(a: FrustumSample | null, b: FrustumSample | null): boolean
883
+
884
+ ### @jgengine/shell/water
885
+
886
+ - DEFAULT_OCEAN_CONFIG (const): const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig
887
+ - DEFAULT_OCEAN_WAVE_SCALE (const): const DEFAULT_OCEAN_WAVE_SCALE: 18 — Shared with `@jgengine/core/world/water` — primary wavelength in world units.
888
+ - MAX_OCEAN_WAVES (const): const MAX_OCEAN_WAVES: 6
889
+ - OCEAN_QUALITY_PRESETS (const): const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, { size: number; resolution: number }>
890
+ - Ocean (function): function Ocean({ config, ...meshProps }: OceanProps): React.JSX.Element
891
+ - OceanColorConfig (interface): interface OceanColorConfig
892
+ - OceanConfig (interface): interface OceanConfig
893
+ - OceanDirectionVector (interface): interface OceanDirectionVector
894
+ - OceanFoamConfig (interface): interface OceanFoamConfig
895
+ - OceanMaterialUniforms (interface): interface OceanMaterialUniforms
896
+ - OceanProps (interface): interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
897
+ - OceanQualityPreset (type): type OceanQualityPreset = "low" | "medium" | "high" | "ultra"
898
+ - OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
899
+ - OceanWaveConfig (interface): interface OceanWaveConfig
900
+ - OceanWaveDirection (type): type OceanWaveDirection = number | OceanDirectionVector
901
+ - ResolvedOceanColorConfig (interface): interface ResolvedOceanColorConfig
902
+ - ResolvedOceanConfig (interface): interface ResolvedOceanConfig
903
+ - ResolvedOceanFoamConfig (interface): interface ResolvedOceanFoamConfig
904
+ - ResolvedOceanWaveConfig (interface): interface ResolvedOceanWaveConfig
905
+ - buildOceanWaveUniforms (function): function buildOceanWaveUniforms(config: ResolvedOceanConfig): { directions: THREE.Vector2[]; params: THREE.Vector4[]; }
906
+ - createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
907
+ - createOceanMaterial (function): function createOceanMaterial(config: ResolvedOceanConfig): OceanShaderMaterial
908
+ - syncOceanMaterial (function): function syncOceanMaterial(material: OceanShaderMaterial, config: ResolvedOceanConfig, elapsedSeconds: number): void
909
+
910
+ ### @jgengine/shell/water/Ocean
911
+
912
+ - Ocean (function): function Ocean({ config, ...meshProps }: OceanProps): React.JSX.Element
913
+ - OceanProps (interface): interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
914
+
915
+ ### @jgengine/shell/water/OceanConfig
916
+
917
+ - DEFAULT_OCEAN_CONFIG (const): const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig
918
+ - DEFAULT_OCEAN_WAVE_SCALE (const): const DEFAULT_OCEAN_WAVE_SCALE: 18 — Shared with `@jgengine/core/world/water` — primary wavelength in world units.
919
+ - MAX_OCEAN_WAVES (const): const MAX_OCEAN_WAVES: 6
920
+ - OCEAN_QUALITY_PRESETS (const): const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, { size: number; resolution: number }>
921
+ - OceanColorConfig (interface): interface OceanColorConfig
922
+ - OceanConfig (interface): interface OceanConfig
923
+ - OceanDirectionVector (interface): interface OceanDirectionVector
924
+ - OceanFoamConfig (interface): interface OceanFoamConfig
925
+ - OceanQualityPreset (type): type OceanQualityPreset = "low" | "medium" | "high" | "ultra"
926
+ - OceanWaveConfig (interface): interface OceanWaveConfig
927
+ - OceanWaveDirection (type): type OceanWaveDirection = number | OceanDirectionVector
928
+ - ResolvedOceanColorConfig (interface): interface ResolvedOceanColorConfig
929
+ - ResolvedOceanConfig (interface): interface ResolvedOceanConfig
930
+ - ResolvedOceanFoamConfig (interface): interface ResolvedOceanFoamConfig
931
+ - ResolvedOceanWaveConfig (interface): interface ResolvedOceanWaveConfig
932
+ - buildOceanWaveUniforms (function): function buildOceanWaveUniforms(config: ResolvedOceanConfig): { directions: THREE.Vector2[]; params: THREE.Vector4[]; }
933
+ - createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
934
+
935
+ ### @jgengine/shell/water/OceanMaterial
936
+
937
+ - OceanMaterialUniforms (interface): interface OceanMaterialUniforms
938
+ - OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
939
+ - createOceanMaterial (function): function createOceanMaterial(config: ResolvedOceanConfig): OceanShaderMaterial
940
+ - syncOceanMaterial (function): function syncOceanMaterial(material: OceanShaderMaterial, config: ResolvedOceanConfig, elapsedSeconds: number): void
941
+
942
+ ### @jgengine/shell/water/OceanShader
943
+
944
+ - oceanFragmentShader (const): const oceanFragmentShader: "\nuniform vec3 uShallowColor;\nuniform vec3 uDeepColor;\nuniform vec3 uCrestColor;\nuniform vec3 uFoamColor;\nuniform float uOpacity;\nuniform float uFresnelStrength;\nuniform float uHorizonBlend;\nuniform float uFoamIntensity;\n\nvarying vec3 vWorldPosition;\nvarying vec…
945
+ - oceanVertexShader (const): const oceanVertexShader: "\nuniform float uTime;\nuniform vec2 uWaveDirections[6];\nuniform vec4 uWaveParams[6];\nuniform float uChoppiness;\nuniform float uFoamThreshold;\nuniform float uFoamSoftness;\nuniform float uFoamCoverage;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying floa…
946
+
947
+ ### @jgengine/shell/water/index
948
+
949
+ - DEFAULT_OCEAN_CONFIG (const): const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig
950
+ - DEFAULT_OCEAN_WAVE_SCALE (const): const DEFAULT_OCEAN_WAVE_SCALE: 18 — Shared with `@jgengine/core/world/water` — primary wavelength in world units.
951
+ - MAX_OCEAN_WAVES (const): const MAX_OCEAN_WAVES: 6
952
+ - OCEAN_QUALITY_PRESETS (const): const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, { size: number; resolution: number }>
953
+ - Ocean (function): function Ocean({ config, ...meshProps }: OceanProps): React.JSX.Element
954
+ - OceanColorConfig (interface): interface OceanColorConfig
955
+ - OceanConfig (interface): interface OceanConfig
956
+ - OceanDirectionVector (interface): interface OceanDirectionVector
957
+ - OceanFoamConfig (interface): interface OceanFoamConfig
958
+ - OceanMaterialUniforms (interface): interface OceanMaterialUniforms
959
+ - OceanProps (interface): interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
960
+ - OceanQualityPreset (type): type OceanQualityPreset = "low" | "medium" | "high" | "ultra"
961
+ - OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
962
+ - OceanWaveConfig (interface): interface OceanWaveConfig
963
+ - OceanWaveDirection (type): type OceanWaveDirection = number | OceanDirectionVector
964
+ - ResolvedOceanColorConfig (interface): interface ResolvedOceanColorConfig
965
+ - ResolvedOceanConfig (interface): interface ResolvedOceanConfig
966
+ - ResolvedOceanFoamConfig (interface): interface ResolvedOceanFoamConfig
967
+ - ResolvedOceanWaveConfig (interface): interface ResolvedOceanWaveConfig
968
+ - buildOceanWaveUniforms (function): function buildOceanWaveUniforms(config: ResolvedOceanConfig): { directions: THREE.Vector2[]; params: THREE.Vector4[]; }
969
+ - createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
970
+ - createOceanMaterial (function): function createOceanMaterial(config: ResolvedOceanConfig): OceanShaderMaterial
971
+ - syncOceanMaterial (function): function syncOceanMaterial(material: OceanShaderMaterial, config: ResolvedOceanConfig, elapsedSeconds: number): void
972
+
973
+ ### @jgengine/shell/weather/FireSpreadLayer
974
+
975
+ - FireSpreadLayer (function): function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }: FireSpreadLayerProps): React.JSX.Element
976
+ - FireSpreadLayerProps (interface): interface FireSpreadLayerProps
977
+
978
+ ### @jgengine/shell/weather/LightningStrike
979
+
980
+ - LightningStrike (function): function LightningStrike({ origin, target, strikeKey = 0, seed = 451, visible = true, duration = 0.18, color = DEFAULT_COLOR, glow = 2.4, branches = 5, jaggedness = 0.08, impactLight = 26, renderOrder = 20, }: LightningStrikeProps): React.JSX.Element
981
+ - LightningStrikeProps (interface): interface LightningStrikeProps
982
+
983
+ ### @jgengine/shell/weather/RainField
984
+
985
+ - RainField (function): function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 22, length = 1.35, width = 0.018, opacity = 0.48, color = DEFAULT_RAIN_COLOR, lightning, timeScale, seed = 11939, renderOrder …
986
+ - RainFieldProps (interface): interface RainFieldProps
987
+
988
+ ### @jgengine/shell/weather/SnowField
989
+
990
+ - SnowField (function): function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 3.2, size = 0.11, sway = 0.62, opacity = 0.86, color = DEFAULT_SNOW_COLOR, timeScale, seed = 72931, renderOrder = 11, frustumC…
991
+ - SnowFieldProps (interface): interface SnowFieldProps
992
+
993
+ ### @jgengine/shell/weather/WeatherLayer
994
+
995
+ - WeatherLayer (function): function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, rain, snow, enabled = true, children, }: WeatherLayerProps): React.JSX.Element | null
996
+ - WeatherLayerMode (type): type WeatherLayerMode = "clear" | "rain" | "snow" | "mixed"
997
+ - WeatherLayerProps (interface): interface WeatherLayerProps
998
+
999
+ ### @jgengine/shell/weather/fireSpreadPose
1000
+
1001
+ - setBillboardQuaternion (function): function setBillboardQuaternion(quaternion: Quaternion, euler: Euler, yaw: number): void
1002
+
1003
+ ### @jgengine/shell/weather/index
1004
+
1005
+ - FireSpreadLayer (function): function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }: FireSpreadLayerProps): React.JSX.Element
1006
+ - FireSpreadLayerProps (interface): interface FireSpreadLayerProps
1007
+ - LightningStrike (function): function LightningStrike({ origin, target, strikeKey = 0, seed = 451, visible = true, duration = 0.18, color = DEFAULT_COLOR, glow = 2.4, branches = 5, jaggedness = 0.08, impactLight = 26, renderOrder = 20, }: LightningStrikeProps): React.JSX.Element
1008
+ - LightningStrikeProps (interface): interface LightningStrikeProps
1009
+ - RainField (function): function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 22, length = 1.35, width = 0.018, opacity = 0.48, color = DEFAULT_RAIN_COLOR, lightning, timeScale, seed = 11939, renderOrder …
1010
+ - RainFieldProps (interface): interface RainFieldProps
1011
+ - SnowField (function): function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 3.2, size = 0.11, sway = 0.62, opacity = 0.86, color = DEFAULT_SNOW_COLOR, timeScale, seed = 72931, renderOrder = 11, frustumC…
1012
+ - SnowFieldProps (interface): interface SnowFieldProps
1013
+ - WeatherLayer (function): function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, rain, snow, enabled = true, children, }: WeatherLayerProps): React.JSX.Element | null
1014
+ - WeatherLayerMode (type): type WeatherLayerMode = "clear" | "rain" | "snow" | "mixed"
1015
+ - WeatherLayerProps (interface): interface WeatherLayerProps
1016
+ - WeatherUniformOptions (interface): interface WeatherUniformOptions
1017
+ - WeatherUniformProvider (function): function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & { children: ReactNode }): React.JSX.Element
1018
+ - WeatherUniformSet (interface): interface WeatherUniformSet
1019
+ - WeatherVector (type): type WeatherVector = readonly [number, number, number]
1020
+ - createWeatherUniformSet (function): function createWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
1021
+ - useWeatherUniformSet (function): function useWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
1022
+
1023
+ ### @jgengine/shell/weather/weatherGeometry
1024
+
1025
+ - createWeatherQuadGeometry (function): function createWeatherQuadGeometry(maxCount: number, seed: number): THREE.InstancedBufferGeometry
1026
+
1027
+ ### @jgengine/shell/weather/weatherMath
1028
+
1029
+ - DEFAULT_RAIN_COUNT (const): const DEFAULT_RAIN_COUNT: 2000
1030
+ - DEFAULT_RAIN_DENSITY (const): const DEFAULT_RAIN_DENSITY: 0.45
1031
+ - DEFAULT_SNOW_COUNT (const): const DEFAULT_SNOW_COUNT: 1500
1032
+ - DEFAULT_SNOW_DENSITY (const): const DEFAULT_SNOW_DENSITY: 0.5
1033
+ - WeatherSeedAttributes (interface): interface WeatherSeedAttributes
1034
+ - clampWeatherRatio (function): function clampWeatherRatio(value: number): number
1035
+ - createWeatherSeedAttributes (function): function createWeatherSeedAttributes(maxCount: number, seed: number): WeatherSeedAttributes
1036
+ - resolveWeatherInstanceCount (function): function resolveWeatherInstanceCount(maxCount: number, density: number, budget?: number): number
1037
+
1038
+ ### @jgengine/shell/weather/weatherUniforms
1039
+
1040
+ - WeatherUniformOptions (interface): interface WeatherUniformOptions
1041
+ - WeatherUniformProvider (function): function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & { children: ReactNode }): React.JSX.Element
1042
+ - WeatherUniformSet (interface): interface WeatherUniformSet
1043
+ - WeatherVector (type): type WeatherVector = readonly [number, number, number]
1044
+ - createWeatherUniformSet (function): function createWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
1045
+ - useWeatherUniformSet (function): function useWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
1046
+
1047
+ ### @jgengine/shell/world/DataObjects
1048
+
1049
+ - DataObjects (function): function DataObjects<T>({ data, position, height, color, cellSize = 0.28, hovered = null, hoverColor = "#ffffff", onHover, grow, castShadow = true, receiveShadow = true, renderItem, }: DataObjectsProps<T>): React.JSX.Element | null
1050
+ - DataObjectsProps (interface): interface DataObjectsProps<T> — Renders one placed 3D object per data item — a 3D bar chart, a heatmap, a city of buildings, a crowd. By default each item is an extruded box (all boxes share one `InstancedMesh`, so hundreds cost one draw call), sized and colored from the item; `renderItem` swaps the box for arbitrary content (a sprite, a GLB, a full entity). Genre-agnostic: the caller owns the data `T`, this owns the placement.
1051
+
1052
+ ### @jgengine/shell/world/GridWorldScene
1053
+
1054
+ - GridWorldScene (function): function GridWorldScene({ feature }: GridWorldSceneProps): React.JSX.Element | null — Data-driven renderer for the `biomes()`/`voxel()`/`plots()`/`tilemap()` world-feature kinds (#207.1): one `InstancedMesh` of extruded, colored boxes built from each feature's declared `cells`, following the same direct-buffer pattern as `InstancedBodies`.
1055
+ - GridWorldSceneProps (interface): interface GridWorldSceneProps
1056
+
1057
+ ### @jgengine/shell/world/InstancedBodies
1058
+
1059
+ - InstancedBodies (function): function InstancedBodies({ world, debugTint = false, baseColors, epoch = 0 }: InstancedBodiesProps): React.JSX.Element — Renders a PhysicsWorld's box bodies as a single InstancedMesh — one draw call per batch. Transforms are written directly into `instanceMatrix.array` each frame (bodies never touch the per-entity React path). Reusable by any game: hand it a physics world and go.
1060
+ - InstancedBodiesProps (interface): interface InstancedBodiesProps
1061
+
1062
+ ### @jgengine/shell/world/InstancedJoints
1063
+
1064
+ - InstancedJoints (function): function InstancedJoints({ world, color = "#f5c542" }: InstancedJointsProps): React.JSX.Element — Debug overlay drawing a PhysicsWorld's joints (suspension, ragdoll links, carry tethers) as one LineSegments batch. Endpoints are streamed each frame from `world.readJointSegments`; pair with `InstancedBodies` to see the constraint structure over the bodies.
1065
+ - InstancedJointsProps (interface): interface InstancedJointsProps
1066
+
1067
+ ### @jgengine/shell/world/SpriteBatch
1068
+
1069
+ - SpriteBatch (function): function SpriteBatch({ url, columns = 1, rows = 1, capacity = DEFAULT_CAPACITY, instances, plane = "xy", billboard = false, pixelated = true, alphaTest = DEFAULT_ALPHA_TEST, opacity = 1, }: SpriteBatchProps): React.JSX.Element — Renders a sprite sheet / tile atlas as a single InstancedMesh — one draw call for the whole batch. Each instance picks its atlas frame via a per-instance UV offset attribute patched into the material's vertex shader, so platformer/puzzle-grid presentation never needs one draw call per sprite. Transforms and UV offsets are written directly into the mesh's typed arrays each frame from a plain instance list (bodies never touch the per-entity React path).
1070
+ - SpriteBatchInstance (interface): interface SpriteBatchInstance
1071
+ - SpriteBatchProps (interface): interface SpriteBatchProps
1072
+
1073
+ ### @jgengine/shell/world/WorldHud
1074
+
1075
+ - CombatCameraShake (function): function CombatCameraShake(): null
1076
+ - ProjectileTracers (function): function ProjectileTracers({ lifeMs = 130 }: { lifeMs?: number }): React.JSX.Element
1077
+ - Reticle (function): function Reticle({ className }: { className?: string }): React.JSX.Element
1078
+ - WorldBarSample (interface): interface WorldBarSample
1079
+ - WorldEntityBars (function): function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, }: { statId: string; height?: number; roles?: readonly CatalogEntityRole[]; resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined; }): React.JSX.Element
1080
+ - WorldFloatText (function): function WorldFloatText({ height = 1.9, lifeMs = 950 }: { height?: number; lifeMs?: number }): React.JSX.Element
1081
+ - WorldTelegraphs (function): function WorldTelegraphs(): React.JSX.Element
1082
+ - collectWorldBarSamples (function): function collectWorldBarSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: { matrixWorldInverse: unknown; projectionMatrix: unknown }, viewport: { width:…
1083
+ - paintWorldBarSamples (function): function paintWorldBarSamples(canvas: { width: number; height: number; getContext(kind: "2d"): CanvasRenderingContext2D | null }, samples: readonly WorldBarSample[], dpr: number, barWidthPx = 112, barHeightPx = 10): void
1084
+ - telegraphPulseOpacity (function): function telegraphPulseOpacity(bornMs: number, windupMs: number, nowMs: number): number
1085
+
1086
+ ### @jgengine/shell/world/WorldItems
1087
+
1088
+ - WorldItems (function): function WorldItems({ config }: { config?: WorldItemRenderConfig }): React.JSX.Element — Rarity→beam/color/label render binding + loot-filter overlay (#32/#33) for every dropped `worldItem`.
1089
+
1090
+ ### @jgengine/shell/world/entityPose
1091
+
1092
+ - PoseSource (interface): interface PoseSource
1093
+ - PoseWritable (interface): interface PoseWritable
1094
+ - posesEqual (function): function posesEqual(a: PoseSource, b: PoseSource): boolean
1095
+ - writeEntityPose (function): function writeEntityPose(target: PoseWritable, source: PoseSource): void
1096
+
1097
+ ### @jgengine/shell/world/floatTextStyle
1098
+
1099
+ - FloatTextInfo (interface): interface FloatTextInfo
1100
+ - FloatTextStyle (interface): interface FloatTextStyle
1101
+ - resolveFloatTextStyle (function): function resolveFloatTextStyle(info: FloatTextInfo): FloatTextStyle
1102
+
1103
+ ### @jgengine/shell/world/telegraphPulse
1104
+
1105
+ - telegraphPulseOpacity (function): function telegraphPulseOpacity(bornMs: number, windupMs: number, nowMs: number): number
1106
+
1107
+ ### @jgengine/shell/world/worldBarSamples
1108
+
1109
+ - Projectable (interface): interface Projectable
1110
+ - WorldBarSample (interface): interface WorldBarSample
1111
+ - collectWorldBarSamples (function): function collectWorldBarSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: { matrixWorldInverse: unknown; projectionMatrix: unknown }, viewport: { width:…
1112
+ - paintWorldBarSamples (function): function paintWorldBarSamples(canvas: { width: number; height: number; getContext(kind: "2d"): CanvasRenderingContext2D | null }, samples: readonly WorldBarSample[], dpr: number, barWidthPx = 112, barHeightPx = 10): void
1113
+
1114
+ ## Guides
1115
+
1116
+ # JGengine
1117
+
1118
+ JGengine is a pure-TypeScript game-engine SDK for games built by AI coding agents. Its skills teach the agent how to use the SDK. When a user says “make X with JGengine” or “make X with jgengine.com,” treat that as sufficient intake. If the skills are not installed, run `npx jgengine skills`; then use this skill and build on the SDK. Do not make the user discover or name skills.
1119
+
1120
+ ## Intake
1121
+
1122
+ State the reading as a short numbered list that is easy to correct, then proceed unless the user changes it. Prefer concrete values and actions over prose:
1123
+
1124
+ 1. **POV:** first-person
1125
+ 2. **World:** custom 3D wasteland with three settlements
1126
+ 3. **Core loop:** get quests by talking to people → defeat enemies → return to redeem quests
1127
+ 4. **Interaction:** collect ground items by walking over them; interact with people and doors at close range
1128
+ 5. **Combat:** ranged weapons, damage, death, loot
1129
+ 6. **Progression:** inventory, currency, quest rewards, upgrades
1130
+ 7. **Players:** single-player, or name the multiplayer topology and synchronized systems
1131
+ 8. **UI:** visible controls, objective tracker, health, inventory feedback
1132
+ 9. **Art direction:** one aesthetic, palette, asset family, and UI voice
1133
+ 10. **Done looks like:** one observable end-to-end play scenario
1134
+
1135
+ Keep this compact—roughly one line per item. It is a build map, not a large specification or an approval gate. Infer conventional details from the named game or genre. Ask only when two plausible readings would fundamentally change the game.
1136
+
1137
+ ## Route selectively
1138
+
1139
+ This skill is the foundation for every task (packages, project shape, defineGame, context, catalogs). After intake, also read only the domain skills the work needs:
1140
+
1141
+ | Need | Read |
1142
+ | --- | --- |
1143
+ | Terrain, scenes, camera, movement, physics, maps, sensors | `jgengine-world` |
1144
+ | Seeded generation, terrain/environment generation, grids, buildings, simulation | `jgengine-procedural` |
1145
+ | Damage, effects, weapons, targeting, projectiles, loot, death | `jgengine-combat` |
1146
+ | Items, quests, dialogue, economy, crafting, objectives, turns, social systems | `jgengine-gameplay` |
1147
+ | Networking adapters, authority, rooms/topology, persistence/backend seams | `jgengine-multiplayer` |
1148
+ | React HUD, shell affordances, controls, feedback, accessibility | `jgengine-ui` |
1149
+ | Models, sprites, textures, audio, catalogs, attribution | `jgengine-assets` |
1150
+ | Proof and screenshots | `jgengine-verify` after implementation |
1151
+
1152
+ Do not read every domain by default. Build through documented engine surfaces; do not infer APIs from gallery games. Inspect engine source only when a documented surface appears wrong or a missing primitive blocks the work.
1153
+
1154
+ ## Build behavior
1155
+
1156
+ Scaffold with `npx jgengine create game-name --name "Game Name"` when needed. Build the requested game continuously from the intake, keeping systems end-to-end rather than leaving registered-but-unusable pieces. Use real assets and visible feedback early. Verify at completion with `jgengine-verify`.
1157
+
1158
+ Cartridge-shaped games (declarative config, engine-owned loop): see [reference-cartridge.md](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine/reference-cartridge.md).
1159
+
1160
+ ---
1161
+
1162
+ The engine ships **verbs and primitives**; your game ships **nouns** (catalogs) and thin handlers. This skill is that foundation plus intake. Use domain skills only when needed; use `jgengine-verify` afterward. UI guidance lives in [`../jgengine-ui/reference.md`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md); assets live in `jgengine-assets`.
1163
+
1164
+ Load detailed references only for the selected domain: [`jgengine-combat`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-combat/reference.md), [`jgengine-world`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-world/reference.md), [`jgengine-multiplayer`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-multiplayer/reference.md), and [`jgengine-ui`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md). Each domain skill explains when its reference is needed.
1165
+
1166
+ ## Packages
1167
+
1168
+ All published on npm, source at [github.com/Noisemaker111/jgengine](https://github.com/Noisemaker111/jgengine) (AGPL-3.0):
1169
+
1170
+ | Package | Role | May import |
1171
+ |---------|------|------------|
1172
+ | `@jgengine/core` | Everything below: defineGame, GameContext, scene, combat, game systems, movement, input, world features, runtime/transport contracts | nothing platform-specific — no React, Convex, three.js, browser |
1173
+ | `@jgengine/react` | `GameProvider`, hooks, headless UI primitives | react + core |
1174
+ | `@jgengine/shell` | `GamePlayerShell` — R3F canvas, camera rig library (orbit, first-person, top-down/iso, RTS, over-the-shoulder, lock-on, chase, cinematic + shake channel), input tracking, HUD mounting, `GameUiPreview`, demo game; you supply a `GameRegistry` | react + three + core |
1175
+ | `@jgengine/ws` | Browser-safe `GameBackend` over a pluggable string-pipe transport (WebSocket/socket.io/WebRTC/loopback), protocol codec, HTTP reads, browser-safe authoritative host + router (`host`/`hostRouter`), and P2P (`peer`) | core |
1176
+ | `@jgengine/node` | Node process bindings over `@jgengine/ws`'s host/router: `ws`-package server, socket.io server attach, file persistence (re-exports `createGameHost`/`memoryPersistence` unchanged) | node + ws + core |
1177
+ | `@jgengine/sql` | `HostPersistence` on Postgres (structural pool, no hard `pg` dep) | core |
1178
+ | `@jgengine/convex` | The Convex **adapter** behind the `GameBackend` seam | react + convex + core |
1179
+
1180
+ Import by deep path: `@jgengine/core/<domain>/<file>` (e.g. `@jgengine/core/runtime/gameContext`).
1181
+
1182
+ ## Hit a snag? File an issue
1183
+
1184
+ Any hiccup with JGengine — a doc that's wrong, a missing primitive, a rough edge, or a feature/improvement idea — file it fast at [github.com/Noisemaker111/jgengine/issues](https://github.com/Noisemaker111/jgengine/issues). A 30-second issue (what you were building, the glue it forced, the API you wanted) is worth more than a silent workaround — that's the fastest way gaps and doc errors get closed. Don't reverse-engineer around a broken doc in silence; report it.
1185
+
1186
+ ## Upgrading? Read the changelog
1187
+
1188
+ All eight packages version in lockstep. When you bump (e.g. `0.6` → `0.7`) to pick up new capabilities, read [`CHANGELOG.md`](https://github.com/Noisemaker111/jgengine/blob/main/CHANGELOG.md) — each release leads with a **Migrate** block listing the concrete steps to move a game onto the new APIs. It ships inside every package too (`node_modules/@jgengine/core/CHANGELOG.md`), and as typed values: `import { VERSION, CHANGELOG } from "@jgengine/core/meta/changelog"` to diff your installed version against the latest programmatically.
1189
+
1190
+ ## Concept → Type Reference
1191
+
1192
+ Exact import paths and export names — **do not invent paths**; every row below resolves to a real file under `packages/core/src`. Import the deep path form `@jgengine/core/<path>`.
1193
+
1194
+ | Concept | Import path (`@jgengine/core/…`) | Export(s) |
1195
+ |---------|----------------------------------|-----------|
1196
+ | Game boot | `game/defineGame` | `defineGame`, `GameDefinition`, `GameLoop`, `InventoryDeclaration`, `PhysicsConfig`, `GameServerConfig`, `TimeConfig` |
1197
+ | Simulation clock | `time/simClock` | `createSimClock`, `SimClock`, `TimeConfig`, `ClockSnapshot`, `CalendarTime` |
1198
+ | Runner contract | `game/playableGame` | `PlayableGame`, `GameCameraConfig`, `CameraRigKind`, `CameraProjection`, `SideScrollCameraConfig`, `TopDownCameraConfig`, `RtsCameraConfig`, `ShoulderCameraConfig`, `LockOnCameraConfig`, `ChaseCameraConfig`, `ObserverCameraConfig`, `CameraShakeConfig`, `CinematicCameraConfig`, `CameraKeyframe`, `EntitySpriteConfig` |
1199
+ | Runtime ctx | `runtime/gameContext` | `createGameContext`, `GameContext`, `GameContextContent`, `GameContextItemEntry`, `GameContextEntityEntry`, `GameContextObjectEntry`, `CatalogEntityRole` |
1200
+ | Behaviour lifecycle | `behaviour/behaviour` | `Behaviour` (`onAwake`→`onEnable`→`onStart`→`onUpdate(dt)`→`onDisable`→`onDestroy`), `BehaviourModule`, `createBehaviourWorld`, `BehaviourWorld`, `JGEngineRegister`, `RegisterField`, `BehaviourModules` — Unity-style lifecycle over an id-keyed node tree (`setActive` cascade, lazy update dispatch); key nodes by entity instance ids. Games augment `JGEngineRegister` via `declare module "@jgengine/core/behaviour/behaviour"` for typed `world.modules`. Three.js binding: `Object3DBehaviour`, `attachObject3D`, `useBehaviourWorld` from `@jgengine/shell/behaviour` |
1201
+ | Reactive keyed store | `store/observableKeyedStore` | `createObservableKeyedStore`, `ObservableKeyedStore` — backs `ctx.game.store` |
1202
+ | Scene instance role | `scene/entityStore` | `EntityRole`, `SceneEntity`, `SpawnOptions`, `EntityPose` |
1203
+ | Object spatial queries | `scene/objectQuery` | `raycastObjects`, `raycastObjectsAll`, `ObjectRaycastInput`, `ObjectRaycastHit` — backs `ctx.scene.object.raycast`/`raycastAll` |
1204
+ | Runtime paint layer | `scene/paintLayer` | `createPaintLayer`, `PaintLayer`, `PaintStroke` — backs `ctx.scene.entity.paint` |
1205
+ | Possession | `scene/possession` | `createPossession`, `Possession`, `PossessionDeps`, `PossessionSwappedEvent` |
1206
+ | Form / shapeshift | `scene/form` | `createForms`, `Forms`, `FormDef`, `FormsDeps`, `FormChangedEvent` |
1207
+ | Multiplayer adapters | `runtime/adapter` | `offline`, `ws`, `convex`, `socketIo`, `p2p`, `lan`, `fly`, `servers`, `MultiplayerTopology`, `ServersPoolConfig` |
1208
+ | Loot | `game/lootTable` | `lootTable`, `LootTableDef`, `LootEntry`, `Drop` |
1209
+ | Dropped-item entity | `game/worldItem` | `WORLD_ITEM_ENTITY_NAME`, `WorldItemRecord`, `WorldItemSpawnInput`, `createWorldItemStore`, `resolveDeathDrops`, `scatterOffset`, `scatterPosition`, `selectNearestWorldItem`, `resolveWorldItemPresentation`, `RarityStyle`, `WorldItemPresentation`, `DEFAULT_RARITY`, `DEFAULT_PICKUP_RADIUS`, `DEFAULT_SCATTER` |
1210
+ | Loot filter | `game/lootFilter` | `lootFilter`, `evaluateLootFilter`, `LootFilterRule`, `LootFilterCondition`, `LootFilterItem`, `LootFilterOverride` |
1211
+ | Loadout | `game/loadout` | `LoadoutDef`, `LoadoutItemEntry`, `Loadouts` |
1212
+ | Cosmetic loadout | `game/cosmetics` | `createCosmetics`, `Cosmetics`, `CosmeticLoadoutDef` |
1213
+ | Quest | `game/quest` | `QuestDef`, `QuestRewards`, `QuestObjective`, `QuestJournal` |
1214
+ | World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building` |
1215
+ | Voxel field | `world/voxelField` | `createVoxelField`, `VoxelField`, `VoxelCell`, `VoxelHit`, `VoxelBounds`, `VoxelFieldSummary`, `VoxelFace`, `VOXEL_FACES`, `VOXEL_FACE_NORMALS` — a chunked block lattice, distinct from the `voxel()` `WorldFeature` descriptor |
1216
+ | Terrain field | `world/terrain` | `TerrainField`, `noiseField`, `resolveTerrainField`, `rollingField`, `fractalNoise`, `valueNoise`, `withNormal`, `arenaField`, `flatField`, `resolveGroundStep`, `snapToGround`, `snapEntityToGround`, `resolveTerrainPalette`, `TERRAIN_MATERIAL_PALETTES` |
1217
+ | Seeded RNG | `random/rng` | `seededRng`, `seededStreams` |
1218
+ | Seed share link | `random/seedLink` | `withSeedParam`, `seedFromUrl`, `seedFromSearch`, `dailySeed`, `DEFAULT_SEED_PARAM` — encode/decode a world seed to/from a shareable URL query param; `dailySeed` is the UTC daily-run seed |
1219
+ | Name generator | `random/nameGen` | `createNameGenerator`, `pickFrom`, `fillTemplate`, `NameGenerator`, `NameGeneratorOptions`, `SyllableBank` |
1220
+ | Regions | `world/regions` | `createRegionField`, `isRegionField`, `RegionDef`, `RegionField`, `RegionSample` |
1221
+ | Wind field | `world/wind` | `windField`, `WindField`, `WindFieldConfig`, `WindVector` |
1222
+ | Water surface | `world/water` | `waterSurface`, `waterSurfaceFromDescriptor`, `synthesizeWaves`, `WaterSurface`, `GerstnerWave` |
1223
+ | Scatter | `world/scatter` | `scatter`, `scatterAabb`, `ScatterConfig`, `ScatterPoint` |
1224
+ | Content scatter | `world/scatterItems` | `scatterItems`, `pickWeighted`, `ScatterLayer`, `ScatterInstance` |
1225
+ | Building generator | `world/buildings` | `generateBuilding`, `generateBuildingDistrict`, `createBuildingGrid`, `GeneratedBuilding` |
1226
+ | Building index | `world/buildingIndex` | `buildingIndex`, `BuildingIndex`, `BuildingHit` |
1227
+ | Scene summary | `world/environmentSummary` | `summarizeEnvironment`, `resolveStructureBuildings`, `EnvironmentSummary` |
1228
+ | Map markers | `world/markers` | `createMarkerSet`, `MarkerSet`, `MapMarker`, `MarkerInput`, `MarkerKindStyle`, `DEFAULT_MARKER_KINDS`, `markerKindStyle` |
1229
+ | Fog of war | `world/fog` | `createFogField`, `FogField`, `FogConfig`, `FogBounds`, `FogCells` |
1230
+ | Minimap math | `world/minimap` | `projectToMinimap`, `clampToMinimapEdge`, `compassBearing`, `headingToBearing`, `bearingToCardinal`, `relativeBearing`, `MinimapView` |
1231
+ | Ping | `game/ping` | `createPingSystem`, `classifyPing`, `PingSystem`, `PingPayload`, `PingCategory`, `PingCategoryDef`, `DEFAULT_PING_CATEGORIES`, `PING_FEED_ACTION` |
1232
+ | Proximity prompt | `interaction/proximityPrompt` | `proximityPrompt`, `ProximityPrompt`, `ProximityPromptDisplay`, `keybind`, `gauge`, `label`, `command` |
1233
+ | Skill-check minigame | `interaction/skillCheck` | `evaluateSkillCheck`, `skillCheckMarkerPosition`, `skillCheckZoneAt`, `SkillCheckConfig`, `SkillCheckZone`, `SkillCheckResult` |
1234
+ | QTE sequencer | `interaction/qte` | `evaluateQteSequence`, `pendingQteStep`, `qteProgress`, `QteStep`, `QteInputEvent`, `QteOutcome` |
1235
+ | Item use | `item/use` | `createItemUse`, `ItemUseHandler`, `ItemUseInput`, `ItemUseResult`, `ItemUseRejection` |
1236
+ | Durability | `item/durability` | `createDurability`, `wear`, `repairQuote`, `isDisabled`, `createDurabilityTracker`, `DurabilitySpec`, `DurabilityState`, `RepairSpec`, `RepairQuote` |
1237
+ | Affix roller | `item/affix` | `createAffixRoller`, `seededRng`, `AffixRoller`, `RollerConfig`, `AffixPool`, `AffixDef`, `RarityTier`, `ItemBaseDef`, `RolledItem`, `RolledAffix` |
1238
+ | Modular item | `item/modularItem` | `createModularItem`, `install`, `computeEffectiveStats`, `missingRequiredSlots`, `ModularItemDef`, `MountSlotDef`, `PartDef`, `InstalledPart` |
1239
+ | Storage tier | `inventory/storageTier` | `partitionOnDeath`, `createDeliveryQueue`, `insureLost`, `resolveConsolation`, `tierOf`, `StorageTier`, `ContainerSnapshot`, `DeathPartition`, `DeliveryQueue`, `InsurancePolicy`, `ConsolationPolicy` |
1240
+ | Contested channel | `session/contestedChannel` | `createContestedChannel`, `ContestedChannel`, `ContestedChannelConfig`, `ContestedEvent`, `ContestedPhase`, `ContestedSnapshot` |
1241
+ | Round state | `session/roundState` | `createRoundState`, `lossBonusFor`, `RoundState`, `RoundConfig`, `RoundPhase`, `RoundTeam`, `RoundEvent`, `RoundEconomy`, `RoundSnapshot`, `LossBonusRule` |
1242
+ | Shrinking ring | `session/ring` | `createRing`, `ringSampleAt`, `Ring`, `RingConfig`, `RingPhase`, `RingSample`, `RingHit`, `RingPoint` |
1243
+ | Extraction session | `session/extraction` | `createRaidSession`, `RaidSession`, `RaidSessionConfig`, `ExtractPoint`, `ExtractionResult`, `DeathResult`, `RaidStatus` |
1244
+ | Role assignment | `session/roles` | `assignRoles`, `RoleSpec` |
1245
+ | Downed / revive | `combat/downed` | `createDownedState`, `DownedState`, `DownedConfig`, `DownedPhase`, `DownedEntry`, `DownedEvent` |
1246
+ | Persistence scopes | `runtime/persistenceScope` | `partitionScopes`, `resetRun`, `mergeScopes`, `clearRunFields`, `applyRunReset`, `planScenarioReset`, `ScopeSchema`, `ScenarioReset`, `PersistenceScope` |
1247
+ | Inventory | `inventory/inventoryModel` | `InventoryLayout`, `InventorySet`, `ItemTraits` |
1248
+ | Progression | `game/progression` | `curve`, `evalCurve`, `leveling`, `Curve`, `LevelingTrack`, `LevelProgress` |
1249
+ | Talent tree | `game/talents` | `createTalentTree`, `TalentTree`, `TalentTreeConfig`, `TalentNodeDef`, `TalentRequirement`, `TalentAllocateResult`, `ResolvedTalents`, `TalentSnapshot` — point spends gated by prereqs + points-in-branch, resolved once into a cached flat `StatModifierSet` + ability grants |
1250
+ | Inventory slots | `inventory/slotModel` | `createSlots`, `placeAt`, `removeAt`, `moveSlot`, `firstEmpty`, `compactSlots`, `Slot`, `SlotGrid` |
1251
+ | Shaped inventory | `inventory/shapedGrid` | `createShapedGrid`, `placeShaped`, `moveShaped`, `removeShaped`, `canPlace`, `rotateFootprint`, `occupiedCells`, `gridAdjacencyQuery`, `cellFromPoint`, `ShapedGrid`, `Footprint`, `Placement`, `Rotation` |
1252
+ | Card piles | `cards/cardPile` | `createCardPile`, `createCardPileState`, `draw`, `moveCards`, `shuffleZone`, `pileRng`, `CardPile`, `CardPileState`, `CardPileConfig` |
1253
+ | Modifier pipeline | `cards/modifierPipeline` | `createModifierPipeline`, `runPipeline`, `Modifier`, `TraceStep`, `PipelineResult` |
1254
+ | Lane board | `board/laneBoard` | `createLaneBoard`, `laneAggregate`, `laneOutcome`, `boardTotals`, `lanesWon`, `LaneBoard`, `LaneRule`, `LaneBoardConfig` |
1255
+ | Timeline board | `board/timelineBoard` | `createTimelineBoard`, `tickTimeline`, `TimelineBoard`, `TimelineSlot`, `TimelineFire` |
1256
+ | World geometry | `world/geometry` | `footprintAabb`, `aabbOverlap`, `snapToGrid`, `resolveMove`, `Aabb`, `Footprint` |
1257
+ | Placement | `world/placement` | `validatePlacement`, `footprintObstacle`, `PlacementRules`, `PlacementResult` |
1258
+ | Placement ghost | `world/placementController` | `createPlacementController`, `PlacementController`, `PlacementPreview`, `PlacementCommit`, `SnapMode`, `quarterTurnsToRotationY` |
1259
+ | Connector sockets | `world/connectors` | `snapToNearest`, `socketsCompatible`, `worldSockets`, `socketWorldPosition`, `ConnectorSocket`, `ConnectorPieceDef`, `PlacedPiece`, `SnapResult` |
1260
+ | Structural support | `world/support` | `solveSupport`, `toDebrisBodies`, `SupportPiece`, `SupportLink`, `SupportResult` |
1261
+ | Wall/roof authoring | `world/walls` | `createWallDrawTool`, `footprintFromWalls`, `autoRoof`, `wallSegments`, `createSurfacePaint`, `WallDrawTool`, `RoofPlan`, `EnclosedFootprint` |
1262
+ | Placed structures | `world/placedStructureStore` | `createPlacedStructureStore`, `PlacedStructure`, `PlacedStructureStore`, `PlacedStructureSnapshot` |
1263
+ | Terraform | `world/terraform` | `createEditableTerrain`, `createTerraformBrush`, `brushWeight`, `EditableTerrain`, `TerraformBrush`, `TerraformEdit`, `TerraformMode` |
1264
+ | Build permissions | `world/buildPermissions` | `createPlotPermissions`, `createContributionPool`, `PlotPermissions`, `ContributionPool`, `BuildRole`, `ContributionGoal` |
1265
+ | Interiors | `world/interiors` | `createInteriors`, `Interior`, `Exterior`, `SpaceRef` |
1266
+ | Game clock | `time/gameClock` | `getScaledElapsedMs`, `computeGameDay`, `SECONDS_PER_GAME_DAY` |
1267
+ | Idle / offline catch-up | `time/idleProgress` | `idleWindow`, `linearCatchUp`, `exponentialCatchUp`, `steppedCatchUp`, `IdleWindow`, `IdleWindowConfig`, `SteppedCatchUpResult` — elapsed-real-time production/growth/decay for a game reopened after being closed |
1268
+ | Scene behaviors | `scene/behaviors` | `wander`, `patrol`, `promptable`, `talkable`, `player` |
1269
+ | Capture check | `scene/captureCheck` | `captureChance`, `rollCapture`, `CaptureCheckInput` |
1270
+ | Owned roster | `scene/roster` | `createRoster`, `Roster`, `RosterEntry`, `RosterCaptureOptions` |
1271
+ | Economy wallet | `economy/wallet` | `createEmptyWallet`, `balance`, `grant`, `charge`, `canAfford`, `chargeAll` |
1272
+ | Tech tree | `economy/techTree` | `createTechTree`, `TechTree`, `TechNodeDef`, `canUnlockTech`, `availableTech`, `unlockedRecipes`, `grantTech`, `techPrerequisitesMet` |
1273
+ | Recipe graph | `crafting/recipe` | `createRecipeGraph`, `RecipeGraph`, `RecipeDef`, `RecipeItem`, `canCraft`, `craft`, `missingInputs`, `stationSatisfied`, `craftSeconds` |
1274
+ | Production building | `crafting/production` | `productionBuilding`, `ProductionBuildingDef`, `createProductionState`, `tickProduction`, `feedProduction`, `drainOutput`, `advanceTransport`, `resolvePowerGrid` |
1275
+ | Crop tile / farming | `crafting/crop` | `createCropField`, `CropField`, `CropDef`, `CropTileState`, `tillTile`, `plantCrop`, `waterTile`, `advanceCropDay`, `harvestCrop`, `applyToolToTiles`, `squarePattern`, `diamondPattern`, `createDayTicker` |
1276
+ | Skill-check roll | `stats/rollCheck` | `rollCheck`, `CheckInput`, `CheckResult`, `CheckAdvantage` |
1277
+ | Input bindings (full) | `input/actionBindings` | `hotbarSlotBindings`, `actionLabel`, `bindingLabel`, `resolveActionCommand`, `bindingMatches`, `createActionStateTracker` |
1278
+ | Touch controls | `input/touchScheme` | `deriveTouchScheme`, `touchCode`, `touchActionLabel`, `withTouchCodes`, `TouchControlsConfig`, `TouchGestureBindings`, `TouchDragBinding`, `TouchButtonSpec`, `TouchScheme`, `TouchJoystick`, `TouchButton` |
1279
+ | Pointer hit | `input/pointer` | `PointerHit`, `PointerButton`, `aimToPoint`, `moveTargetFromHit`, `groundOf`, `PointerVec3` |
1280
+ | Navmesh + A* | `nav/navGrid` | `createNavGrid`, `findPath`, `smoothPath`, `NavGrid`, `NavGridConfig`, `NavPoint`, `FindPathOptions` |
1281
+ | Path follow | `nav/pathFollow` | `createPathFollow`, `advancePathFollow`, `pathFromNav`, `PathFollowConfig`, `PathFollowState`, `Waypoint` |
1282
+ | Nav-grid movement constraint | `nav/navConstrain` | `constrainToNavGrid`, `NavConstrainProposed`, `NavConstrainEntity`, `NavConstrainOptions` — a standalone walkable-pass-through + wall-slide helper; adapt its `(proposed, entity)` shape to `PlayerMovementConfig.beforeCommit`'s `(frame) => [x,y,z]` with a small closure |
1283
+ | Selection set | `scene/selection` | `createSelectionSet`, `SelectionSet`, `screenRect`, `selectWithinRect`, `rectContainsPoint`, `isMarquee`, `ScreenRect` |
1284
+ | Context menu | `interaction/contextMenu` | `contextVerb`, `buildContextMenu`, `contextVerbInput`, `ContextVerb`, `ContextMenu` |
1285
+ | Shared / group wallet | `economy/sharedWallet` | `createWalletBook`, `WalletBook`, `WalletScope`, `userScope`, `groupScope`, `balanceIn`, `grantTo`, `chargeFrom`, `contributionOf`, `contributorsOf` |
1286
+ | Analog axis input | `input/axisInput` | `AxisInput`, `AxisChannel`, `AxisBindingMap`, `DRIVE_AXIS_BINDINGS`, `clampAxis`, `rampToward`, `NEUTRAL_AXIS` |
1287
+ | Raw control polling | `runtime/inputSnapshot` | `createInputSnapshot`, `InputSnapshot` — backs `ctx.input` |
1288
+ | Physics world | `physics/physicsWorld` | `PhysicsWorld`, `PhysicsWorldConfig`, `PhysicsBounds`, `PhysicsStats`, `AddBodyOptions` (`{ shape: "box", halfExtents }` \| `{ shape: "sphere", radius }`), `JointOptions`, `JointKind`, `CollisionEvent` |
1289
+ | Ballistic collision sweep | `physics/ballisticSweep` | `createBallisticSweep`, `BallisticSweep`, `BallisticSweepHit`, `BallisticSweepOptions` |
1290
+ | Tweening / easing | `anim/easing` | `Easing`, `lerp`, `clamp01`, `smoothstep`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeOutBack`, `easeOutElastic`, `tween`, `timedProgress` |
1291
+ | Async data source | `data/dataSource` | `createDataSource`, `DataSource`, `DataSourceState`, `DataSourceStatus`, `DataSourceOptions`, `DataSourceClock`, `RefreshOptions` |
1292
+ | JSON fetch | `data/fetchJson` | `fetchJson`, `FetchJsonOptions`, `FetchImpl`, `HttpStatusError`, `JsonParseError` |
1293
+ | JSON data source | `data/jsonDataSource` | `createJsonDataSource`, `JsonDataSourceOptions` |
1294
+ | Dev proxy routing | `data/devProxy` | `proxiedUrl`, `parseDevProxyTable`, `DevProxyTable`, `ProxiedUrlOptions`, `DEFAULT_DEV_PROXY_PREFIX` |
1295
+ | Grid-cell world rendering | `world/gridInstances` | `resolveGridInstances`, `GridInstanceTransform` |
1296
+ | Swarm LOD scheduler | `world/lod` | `createLodScheduler`, `LodScheduler`, `LodSchedulerConfig`, `LodBand` — distance→band index for render detail, `step(id, distance, dt)` throttles per-entity updates by band interval (staggered, accumulates skipped time); pairs with `@jgengine/shell/world/SpriteBatch` for 1000+ entity swarms |
1297
+ | Turn loop | `turn/turnLoop` | `createTurnLoop`, `TurnLoop`, `TurnLoopConfig`, `TurnState`, `PoolConfig`, `PoolState`, `TurnLoopSnapshot` |
1298
+ | Declared-action intent board | `turn/intent` | `createIntentBoard`, `IntentBoard`, `DeclaredIntent` — `declare(participantId, intent)`, `peek`, `all`, `consume`, `clear` |
1299
+ | Commit modes | `turn/commit` | `createCommitController`, `CommitController`, `CommitMode`, `CommitOutcome`, `SubmittedAction` |
1300
+ | Intent board | `turn/intent` | `createIntentBoard`, `IntentBoard`, `DeclaredIntent` |
1301
+ | Tactical grid | `tactics/tacticalGrid` | `createTacticalGrid`, `TacticalGrid`, `TacticalGridConfig`, `Tile`, `ReachableTile`, `PushResult`, `PushCollision` |
1302
+ | Predictive query | `tactics/predictiveQuery` | `predictAreaEffect`, `predictArcEffect`, `predictTiles`, `PredictiveDeps`, `PredictedTarget` |
1303
+ | Sim snapshot | `tactics/snapshot` | `createSnapshotStore`, `SnapshotStore`, `SnapshotSlice`, `Snapshot`, `deepClone` |
1304
+ | Surfaces | `tactics/surface` | `createSurfaceLayer`, `SurfaceLayer`, `SurfaceLayerConfig`, `SurfaceKindDef`, `SurfaceReaction`, `SurfaceEvent` |
1305
+ | Area targeting | `combat/effects` | `resolveAreaTargets`, `AreaTarget`, `AreaTargetInput` (shared AoE targeting behind `effect` + the predictive query) |
1306
+ | Environment field | `world/envField` | `createEnvironmentField`, `EnvironmentField`, `EnvironmentSample`, `EnvironmentFieldConfig`, `OccluderRect`, `HeatSource` |
1307
+ | Weather + fire | `world/weather` | `resolveWeather`, `WeatherState`, `WeatherModifier`, `WeatherModifierTable`, `ResolvedWeather`, `createFireGrid`, `FireGrid`, `FireCell`, `FireGridConfig` |
1308
+ | Realm composition | `world/realm` | `composeRealm`, `RealmCard`, `RealmBase`, `ComposedRealm`, `RealmEnvironmentParams`, `SpawnTableOverride` |
1309
+ | Decay meters | `survival/decayMeter` | `createDecayMeterSet`, `DecayMeterSet`, `DecayMeterConfig`, `MeterThreshold`, `DecayMeterState` |
1310
+ | Status moodles | `survival/moodle` | `createMoodleStack`, `stackMoodles`, `MoodleStack`, `Moodle`, `MoodleSeverity`, `TimedMoodleInput` |
1311
+ | Multi-region health | `survival/regionHealth` | `createMultiRegionHealth`, `MultiRegionHealth`, `HealthRegionConfig`, `AilmentConfig`, `RegionHealthState`, `AilmentInstance` |
1312
+ | Audio contract | `audio/audioFalloff` | `computeFalloffGain`, `resolveEmitterGain`, `distance3`, `AudioFalloffConfig`, `FalloffCurve`, `SoundDef`, `AudioBusDef`, `AudioBusId` |
1313
+ | Beat clock | `time/beatClock` | `createBeatClock`, `createBeatInputBuffer`, `nextBeatTime`, `BeatClock`, `BeatClockConfig`, `BeatSnapshot`, `BeatInputBuffer`, `BufferedAction` |
1314
+ | Spawn director | `ai/spawnDirector` | `createSpawnDirectorState`, `advanceSpawnDirector`, `advanceWave`, `raiseAlert`, `pickSpawnPoint`, `SpawnDirectorConfig`, `WaveManifest`, `SpawnEntry`, `SpawnRequest`, `DirectorContext` |
1315
+ | Threat table | `ai/threat` | `createThreatTable`, `ThreatTable`, `ThreatTableConfig`, `ThreatEntry`, `HighestThreatOptions` |
1316
+ | Group-assist aggro | `ai/groupAssist` | `createAssistNetwork`, `AssistNetwork`, `AssistNetworkConfig`, `AssistMember` — propagates one member's threat gains to same-group members (optional radius + `distanceBetween` gating) so a single pull rallies the group |
1317
+ | Job board | `ai/jobBoard` | `createJobBoard`, `JobBoard`, `JobDef`, `Job`, `JobPhase`, `WorkerState`, `JobReport`, `JobTickContext` |
1318
+ | Crowd flow | `ai/crowd` | `computeFlowField`, `createCrowdField`, `selectPoi`, `FlowField`, `FlowFieldOptions`, `CrowdField`, `Poi`, `SelectPoiOptions` |
1319
+ | Factions & reputation | `faction/factions`, `faction/reputation` | `createFactionGraph`, `createFactionRoster`, `FactionRelation`, `FactionDef`, `FactionGraph`, `FactionRoster`, `createReputationLedger`, `DEFAULT_REPUTATION_TIERS`, `tierForStanding`, `effectiveRelation`, `ReputationTier`, `ReputationLedger` |
1320
+ | Physics actors | `physics/ragdoll`, `physics/carryable`, `physics/forceVolume`, `physics/spatialGrid` | `createRagdoll`, `Ragdoll`, `Carryable`, `carrySpeedMultiplier`, `ForceVolume`, `PlatformCarry`, `SpatialGrid` |
1321
+ | Traversal (grapple/glide) | `physics/traversal` | `Grapple`, `GrappleConfig`, `Glide`, `GlideConfig` |
1322
+ | Structural destruction | `physics/structure` | `StructureGraph`, `StructureNodeSpec`, `StructureEdgeSpec`, `StructureMaterial`, `StructureMaterialTable`, `CollapseEvent`, `DebrisConfig` |
1323
+ | Destructible terrain | `world/carve` | `VoxelVolume`, `VoxelMaterial`, `VoxelMaterialTable`, `CarvableField`, `carvableTerrain`, `CarveOp`, `DepositOp`, `CraterOp`, `MoundOp`, `EMPTY_VOXEL` |
1324
+ | Vehicle body | `physics/vehicleBody` | `createVehicleBody`, `VehicleBody`, `VehicleBodyConfig`, `WheelSpec`, `GripCurve`, `sampleGripCurve`, `DEFAULT_GRIP_CURVE` |
1325
+ | Buoyant boat | `physics/buoyancy` | `createBuoyantBody`, `BuoyantBody`, `BuoyantBodyConfig` |
1326
+ | Crash damage | `physics/damageZones` | `createDamageModel`, `DamageModel`, `DamageZoneDef`, `DamageTransition` |
1327
+ | Mounts / rideables | `scene/mount` | `createMountController`, `MountController`, `MountKit`, `MountSeat`, `RideableConfig` |
1328
+ | Shared-vehicle stations | `scene/stationClaim` | `createStationClaim`, `StationClaim`, `Station`, `SharedVehicleConfig`, `ClaimResult` |
1329
+ | Lag compensation | `multiplayer/lagCompensation` | `createPositionHistory`, `PositionHistory`, `rewindTimestamp`, `resolveHitscan`, `raySphereDistance`, `HitscanRay`, `HitscanTarget` |
1330
+ | Simultaneous commit | `multiplayer/simultaneousCommit` | `createCommitRound`, `CommitRound`, `SealedCommit`, `resolveCommits` |
1331
+ | Combat-snapshot replay | `multiplayer/combatSnapshot` | `serializeBoard`, `cloneSnapshot`, `replayCombat`, `BoardSnapshot`, `SnapshotUnit`, `CombatRules`, `ReplayResult` |
1332
+ | Session matchmaking | `multiplayer/matchmaking` | `browseSessions`, `findByJoinCode`, `quickMatch`, `matchesFilter`, `normalizeJoinCode`, `generateJoinCode`, `SessionListing`, `MatchFilter` |
1333
+ | Auth identity | `multiplayer/identity` | `AuthSession`, `PlayerIdentity`, `sessionPlayer`, `resolveGuestSession` |
1334
+ | Text chat | `game/chat`, `multiplayer/chatContract` | `createChat`, `Chat`, `ChatMessage`, `ChatChannelDef`, `whisperChannelId`, `createChatRateLimiter`, `ChatTransport`, `ChatSync`, `createLocalChatTransport` |
1335
+ | Chat filter | `game/chatFilter` | `createChatFilter`, `normalizeChatText`, `ChatFilter`, `ChatFilterConfig`, `ChatFilterResult` — mask/reject blocked words (leet-normalized token match); wire via `ChatDeps.filter` (word lists are game data, the engine ships the mechanism) |
1336
+ | Voice seam | `multiplayer/voiceContract` | `VoiceTransport`, `VoiceParticipant`, `VoiceRoute`, `createLocalVoiceTransport`, `createPushToTalk`, `PushToTalkMode` |
1337
+ | Race state | `game/race` | `raceTrack`, `RaceTrack`, `createRaceState`, `RaceState`, `RaceEvent`, `RaceWinCondition`, `firstPastPost`, `topK`, `lastStanding`, `everyoneFinishes` |
1338
+ | Reveal query | `sensor/revealQuery` | `createRevealQuery`, `RevealQuery`, `RevealQueryOptions`, `RevealHit` |
1339
+ | Hidden-state probe | `sensor/hiddenStateProbe` | `probeHiddenState`, `probeHiddenStateAll`, `HiddenStateSource`, `HiddenStateValue`, `SensorProbeOptions`, `SensorReading` |
1340
+ | View-frustum sensor | `sensor/frustumSensor` | `createFrustumSensor`, `projectToView`, `framingScore`, `FrustumCamera`, `FrustumTarget`, `FrustumProjection`, `FrustumSample`, `FrustumSensor`, `FramingConfig` |
1341
+ | Recording buffer | `sensor/recordingBuffer` | `createRecordingBuffer`, `RecordingBuffer`, `RecordingFrame`, `RecordingBufferOptions` |
1342
+ | Concealment scoring | `sensor/concealment` | `colorDistance`, `concealmentScore`, `createConcealmentSensor`, `ColorHex`, `ConcealmentTarget`, `ConcealmentSample`, `ConcealmentSensor` |
1343
+ | Freeze violation monitor | `sensor/freezeMonitor` | `createFreezeMonitor`, `FreezeMonitor`, `FreezeSubject`, `FreezeViolation` |
1344
+ | Animation SM | `combat/animationState` | `createAnimationState`, `AnimationState`, `AnimationClip`, `FramePhase`, `FrameRange`, `phasesAtFrame`, `activeRangeAtFrame`, `frameAtMs` |
1345
+ | Accumulator meter | `stats/accumulatorMeter` | `createAccumulatorMeter`, `AccumulatorMeter`, `AccumulatorMeterConfig`, `MeterTier`, `MeterAddResult`, `tierAt` |
1346
+ | Stagger / buildup | `combat/breakMeters` | `createStaggerMeter`, `createBuildupMeter`, `StaggerMeter`, `BuildupMeter`, `BuildupProc` |
1347
+ | Attack tags | `combat/attackTags` | `attackMeta`, `AttackTag`, `AttackMeta`, `hasTag`, `isBlockable`, `isParryable`, `isDodgeable`, `counters` |
1348
+ | Defensive window | `combat/defensiveWindow` | `createDefensiveWindow`, `resolveDefense`, `DefensiveWindowConfig`, `DefenseKind`, `DefenseOutcome`, `windowActiveAt`, `iframeActiveAt` |
1349
+ | Combo string | `combat/comboString` | `createComboRunner`, `advanceCombo`, `ComboString`, `ComboStep`, `AdvanceComboResult` |
1350
+ | Hit reaction | `combat/hitReaction` | `resolveHitReaction`, `HitReaction`, `HitReactionConfig`, `CameraShake`, `applyImpulse` |
1351
+ | Telegraph | `combat/telegraph` | `pointInTelegraph`, `telegraphProgress`, `telegraphFired`, `telegraphTurnProgress`, `telegraphFiredAtTurn`, `telegraphTurnsRemaining`, `TelegraphShape`, `TelegraphConfig` |
1352
+ | Dash / dodge | `movement/dash` | `createDashState`, `DashState`, `DashConfig`, `DashBurst`, `iframeActive`, `dashOffset` |
1353
+ | Ability kit | `combat/abilityKit` | `createAbilityKit`, `AbilityKit`, `AbilitySlotConfig`, `AbilitySlotSnapshot`, `AbilitySlotState`, `AbilityCastType`, `AbilityCastResult`, `AbilitySlotRetune` |
1354
+ | Resource pool | `combat/resourcePool` | `createResourcePool`, `ResourcePool`, `ResourcePoolConfig` — current/max with per-second regen/decay and spend/gain; `pool.current()` is the ability kit's `resourceAvailable` |
1355
+ | Combo points | `combat/comboPoints` | `createComboPoints`, `ComboPoints`, `ComboPointsConfig` — discrete points accrued on action, expiring after a timeout from the last gain, spent in bulk |
1356
+ | Event meter | `stats/eventMeter` | `createEventMeter`, `EventMeter`, `EventMeterConfig`, `EventMeterFeedResult` |
1357
+ | Auto-target policy | `scene/autoTarget` | `selectAutoTarget`, `createAutoTargeter`, `AutoTargetPolicy`, `AutoTargeter`, `AutoTargetDeps` |
1358
+ | Resistance matrix | `combat/resistance` | `resolveResistance`, `resistanceScale`, `ResistanceMatrix`, `ResistVerdict`, `ResistanceResult` |
1359
+ | Run draft | `game/runDraft` | `createRunDraft`, `createRunModifierStack`, `RunDraft`, `RunModifierStack`, `RunModifierOffer` |
1360
+ | Uniform-cell grid | `puzzle/cellGrid` | `CellGrid`, `CellRun`, `createCellGrid`, `cellAt`, `inGridBounds`, `withCell`, `withCells`, `fullRows`, `clearRows`, `collapseColumns`, `findRuns` |
1361
+ | Falling piece | `puzzle/fallingPiece` | `FallingPiece`, `ShapeTable`, `LockDelayState`, `pieceCells`, `pieceCollides`, `mergePiece`, `dropDistance`, `gravityInterval`, `levelForLines`, `lineScore`, `createLockDelay`, `stepLockDelay` |
1362
+ | Falling tile grid | `tactics/fallingGrid` | `createFallingGrid`, `FallingGrid`, `FallingGridConfig`, `FallingGridCell`, `FallingGridSnapshot`, `LockState`, `gravityIntervalMs`, `GravityIntervalConfig` — a generic tile-drop grid (any `TCell` payload), distinct from `puzzle/cellGrid`+`puzzle/fallingPiece`'s row-clear/shape-table pair |
1363
+ | Spawn/respawn points | `game/spawnPoints` | `createSpawnPoints`, `SpawnPoints`, `SpawnPointPose`, `RespawnTarget` |
1364
+ | Level sequence | `game/levelSequence` | `createLevelSequence`, `LevelSequence`, `LevelSequenceConfig`, `LevelDescriptor`, `CurrentLevel`, `LevelSequenceStatus`, `LevelSequenceProgress` |
1365
+ | Devtools overlay + tunables | `devtools/devtools` | `devtools`, `createDevtools`, `tunable`, `snapshotDevtools`, `instrumentLatency`, `Tunable`, `TunableOptions`, `TunableAccessor`, `DevtoolsControl`, `DiscoveredEntry`, `DevtoolsOverrides`, `DevtoolsSnapshot` |
1366
+ | Tunable auto-discovery | `devtools/transformTunables` | `transformTunableExports`, `tunableModuleTable`, `tunableDiscoveryPlugin`, `TunableTransformResult` |
1367
+
1368
+ ## Getting started (new project)
1369
+
1370
+ Fastest path — the `jgengine` CLI scaffolds the entire canonical shape below (harness, skeleton, stub game, verify test, AGENTS.md) as a booting game:
1371
+
1372
+ ```sh
1373
+ npx jgengine create my-game # then: cd my-game && bun dev
1374
+ npx jgengine doctor # later, if the setup drifts (version skew, unstyled HUD, shape strays)
1375
+ ```
1376
+
1377
+ Manual equivalent:
1378
+
1379
+ ```sh
1380
+ bun add @jgengine/core @jgengine/react @jgengine/shell react react-dom three three-stdlib @react-three/fiber @react-three/drei
1381
+ bun add -d @tailwindcss/vite tailwindcss # HUD styling (Vite + Tailwind v4)
1382
+ ```
1383
+
1384
+ A single game's standalone entry mounts `GameHost` (`@jgengine/shell/GameHost`) over the `game` your `game.config.ts` exports. The full standalone harness is four small files plus a script — this is exactly the shape every `Games/*` game ships, so `bun dev` plays it on its own with no host app:
1385
+
1386
+ ```html
1387
+ <!-- index.html -->
1388
+ <!doctype html>
1389
+ <html lang="en">
1390
+ <head>
1391
+ <meta charset="UTF-8" />
1392
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1393
+ <title>My Game</title>
1394
+ </head>
1395
+ <body>
1396
+ <div id="root"></div>
1397
+ <script type="module" src="/src/main.tsx"></script>
1398
+ </body>
1399
+ </html>
1400
+ ```
1401
+
1402
+ ```ts
1403
+ // vite.config.ts — monorepo-aware: the alias branch only fires when this folder
1404
+ // sits inside the engine repo checkout; copied anywhere else, @jgengine/* resolves
1405
+ // from npm dist and the alias list is empty
1406
+ import { existsSync } from "node:fs";
1407
+ import { fileURLToPath } from "node:url";
1408
+ import tailwindcss from "@tailwindcss/vite";
1409
+ import react from "@vitejs/plugin-react";
1410
+ import { defineConfig } from "vite";
1411
+
1412
+ const engineSrc = (pkg: string) => fileURLToPath(new URL(`../../packages/${pkg}/src`, import.meta.url));
1413
+
1414
+ export default defineConfig({
1415
+ plugins: [react(), tailwindcss()],
1416
+ resolve: {
1417
+ alias: existsSync(engineSrc("core"))
1418
+ ? [
1419
+ { find: /^@jgengine\/core\/(.*)$/, replacement: `${engineSrc("core")}/$1` },
1420
+ { find: /^@jgengine\/react\/(.*)$/, replacement: `${engineSrc("react")}/$1` },
1421
+ { find: /^@jgengine\/ws\/(.*)$/, replacement: `${engineSrc("ws")}/$1` },
1422
+ { find: /^@jgengine\/shell\/(.*)$/, replacement: `${engineSrc("shell")}/$1` },
1423
+ { find: /^@jgengine\/assets$/, replacement: `${engineSrc("assets")}/index.ts` },
1424
+ { find: /^@jgengine\/assets\/(.*)$/, replacement: `${engineSrc("assets")}/$1` },
1425
+ ]
1426
+ : [],
1427
+ },
1428
+ });
1429
+ ```
1430
+
1431
+ ```css
1432
+ /* src/index.css */
1433
+ @import "tailwindcss";
1434
+ @source "../node_modules/@jgengine/react/dist";
1435
+ @source "../node_modules/@jgengine/shell/dist";
1436
+ ```
1437
+
1438
+ Inside the engine repo the two `@source` lines point at `../../../packages/react/src` and `../../../packages/shell/src` instead (see any `Games/*/src/index.css`) — same file, different `@source` targets depending on where dist lives.
1439
+
1440
+ ```tsx
1441
+ // main.tsx
1442
+ import "./index.css";
1443
+
1444
+ import { createRoot } from "react-dom/client";
1445
+ import { GameHost } from "@jgengine/shell/GameHost";
1446
+ import { game } from "./game.config";
1447
+
1448
+ const root = document.getElementById("root");
1449
+ if (root === null) throw new Error("main: missing #root mount element");
1450
+ createRoot(root).render(<GameHost playable={game} />);
1451
+ ```
1452
+
1453
+ Add `"dev": "vite"` to `package.json`'s `scripts` — `bun dev` then launches the game standalone.
1454
+
1455
+ `GameHost` resolves multiplayer itself from `game.multiplayer` (falling back to offline when the adapter can't resolve, with a console warning) — pass `multiplayer` (a prebuilt `ShellMultiplayer | null`, used as-is with no resolution attempted) or `resolveMultiplayer` (`(args) => ShellMultiplayer | null`, tried before the built-in resolver, falling back to it on `null`) only when the host app needs to supply its own session, e.g. trying several transports in sequence.
1456
+
1457
+ A multi-game host (a launcher, a dev registry) wires `GamePlayer` over a `GameRegistry`:
1458
+
1459
+ ```tsx
1460
+ import { GamePlayer } from "@jgengine/shell/GamePlayer";
1461
+ import type { GameRegistry } from "@jgengine/shell/registry";
1462
+
1463
+ const games: GameRegistry = {
1464
+ "my-game": () => import("./my-game").then((m) => m.game),
1465
+ };
1466
+
1467
+ function App() {
1468
+ return <GamePlayer gameId="my-game" registry={games} loading={<p>Loading…</p>} />;
1469
+ }
1470
+ ```
1471
+
1472
+ `GamePlayer({ gameId, registry, fallbackGameId?, loading?, multiplayer? })` (`@jgengine/shell/GamePlayer`) is `GamePlayerShell` plus the lazy-load glue: it looks up `gameId` in `registry`, awaits the loader, renders `loading` (default `null`) until it resolves, then mounts `GamePlayerShell playable={...} multiplayer={...}`; switching `gameId` re-triggers the load, and an in-flight load is discarded if the id changes again first. `resolveGameLoader(registry, gameId, fallbackGameId?)` (`@jgengine/shell/registry`) is the underlying lookup — `registry[gameId] ?? registry[fallbackGameId]` — for hosts that want the fallback behavior without the component.
1473
+
1474
+ HUD styling is Tailwind v4 via the `index.css` above — without its `@source` lines the HUD renders unstyled. Then build the game itself under `src/` per the layout below — `src/game.config.ts` is the single entry, defined with `defineGame` from `@jgengine/shell/defineGame`.
1475
+
1476
+ ## Scope
1477
+
1478
+ This file documents engine primitives and conventions only — never game domain. Example ids (`iron_block`, `mob_grunt`, `shop_town`) are placeholders, not content to copy.
1479
+
1480
+ | Engine owns | Your game owns |
1481
+ |-------------|----------------|
1482
+ | Weighted loot RNG, trade validation, loadout application, quest journal state, social graph, stat clamp math, effect absorption, projectile geometry, death resolution, event bus, feeds, leaderboards, input capture, pose hitboxes | Catalog entries and ids, effect id names, XP curves, shop/item/quest/loadout definitions, use-handlers, AI logic, UI content |
1483
+
1484
+ **Rules:**
1485
+
1486
+ 1. **Catalog-first** — shape and behavior of every id lives in game-owned catalog files. Runtime calls pass ids, positions, instance keys.
1487
+ 2. **Three buckets** — inventory items, scene objects, scene entities. Never merge them.
1488
+ 3. **Dumb place/spawn** — no behaviors on `place()`/`spawn()`; the catalog owns them.
1489
+ 4. **Commands for verbs** — input maps to actions, actions to commands/handlers; no raw keys in game logic.
1490
+ 5. **Primitives over glue** — a loop several games need (loot roll, shop buy, kit seeding) belongs in the engine, not copy-pasted per game.
1491
+ 6. **No speculative config** — `defineGame` fields exist only with a live engine consumer.
1492
+ 7. **This file stays domain-free.**
1493
+
1494
+ ## The three buckets
1495
+
1496
+ | Bucket | What | API |
1497
+ |--------|------|-----|
1498
+ | **Inventory** | Stackable ids in containers | `ctx.player.inventory.put / take / move / has / count` |
1499
+ | **Scene object** | Static world content | `ctx.scene.object.place / remove / move / rotate / list` |
1500
+ | **Scene entity** | Movers driven per tick | `ctx.scene.entity.spawn / despawn / setPose / effect / …` |
1501
+
1502
+ A voxel block is an object. A rack is an object with a slot inventory. A GPU is an inventory item inside it. A player, mob, or car is an entity. A dropped-item lying on the ground is also an entity — `ctx.scene.worldItem` (position + item ref + rarity, spawned under `game/worldItem`'s `WORLD_ITEM_ENTITY_NAME`) — never a fourth bucket and never merged into inventory or object.
1503
+
1504
+ ## Game repo layout
1505
+
1506
+ Every game is one shape, enforced by `check-game-shape` (part of `check-types`): the top of `src/` holds only the skeleton, and every game-specific module, UI component, and test lives under `src/game/`. Dense files — one `catalog.ts` per domain, never one file per entry.
1507
+
1508
+ ```
1509
+ src/
1510
+ game.config.ts single entry — export const game = defineGame({...}) from "@jgengine/shell/defineGame"
1511
+ index.tsx barrel — export { game } from "./game.config" (+ any UI-preview scenario re-export)
1512
+ main.tsx standalone host — mounts <GameHost playable={game}/> from "@jgengine/shell/GameHost"
1513
+ loop.ts onInit, onNewPlayer, onTick
1514
+ world.ts WorldFeature + PhysicsConfig (only for games that have one)
1515
+ game/
1516
+ keybinds.ts ActionCodesMap — named actions + hotbarSlotBindings(n)
1517
+ inventories.ts inventory declarations
1518
+ assets.ts Render catalog
1519
+ content.ts itemById / entityById lookups over all catalogs
1520
+ loadouts.ts Loadout ids → items/economy/unlocks per inventory
1521
+ world/ zones.ts, setup.ts (place/spawn from onInit)
1522
+ items/ <domain>/catalog.ts + use-handlers.ts
1523
+ objects/ catalog.ts (+ loot tables beside their domain)
1524
+ entities/ players/ enemies/ npcs/ — catalog.ts per role (never actors/)
1525
+ quests/catalog.ts when using game.quest
1526
+ progression/ curves.ts — game-owned XP curve numbers fed to game/progression
1527
+ ui/GameUI.tsx ALL layout/positioning
1528
+ ui/components/ content-only pieces GameUI places
1529
+ ```
1530
+
1531
+ ## `defineGame` — the single authoring entry
1532
+
1533
+ `@jgengine/shell/defineGame` is the game-authoring entry: one call in `game.config.ts` takes both engine fields (`name`, `assets`, `world`, `physics`, `inventories`, `input`, `server`, `save`, `time`, `feed`, `multiplayer`) and presentation fields (`content`, `loop`, `GameUI`, `camera`, `environment`, `WorldOverlay`, `renderEntity`, `renderObject`, `entitySprites`, `entityModels`, `objectModels`, `hotbarSelection`, `prompts`, `pointer`, `touch`, `worldHealthBars`, `audio`, `entitySounds`, `objectSounds`, `worldItem`, `shadows`, `collision`, `movement`, `devtools`) and returns a ready `PlayableGame` — no separate object to assemble. It is a thin wrapper over the core `defineGame` primitive (below) plus the `PlayableGame` runner assembly; see `packages/shell/src/defineGame.tsx` for the exact accepted fields. Never game tuning (walk speeds, damage, prompts — those live in catalogs).
1534
+
1535
+ **Smart defaults** — omit any of these and the call still resolves: `multiplayer` → `offline()`; `assets` → an empty asset catalog; `loop` hooks (`onInit`/`onNewPlayer`/`onTick`) → no-ops; `content` → `{}`; `GameUI` → an empty component; `camera` → third-person orbit; `feed` → 20-entry ring buffers per action; a `world` of kind `environment()` auto-renders as the backdrop with no `environment` component supplied — a non-`environment()` world (`flat()`, `voxel()`, …) still needs the game to hand it one.
1536
+
1537
+ ```ts
1538
+ // game.config.ts — imports only, nothing inline
1539
+ import { defineGame } from "@jgengine/shell/defineGame";
1540
+ import { assets } from "./game/assets";
1541
+ import { content } from "./game/content";
1542
+ import { GameUI } from "./game/ui/GameUI";
1543
+ import { inventories } from "./game/inventories";
1544
+ import { keybinds } from "./game/keybinds";
1545
+ import { loop } from "./loop";
1546
+ import { physics, world } from "./world";
1547
+
1548
+ export const game = defineGame({
1549
+ name: "My Game",
1550
+ assets,
1551
+ world,
1552
+ physics,
1553
+ inventories,
1554
+ input: keybinds,
1555
+ server: "persistent", // or { mode: "ffa", scoreLimit: 30 } — rules live in game code
1556
+ save: { auto: "5m", scope: "player+chunks" }, // or "none"
1557
+ multiplayer: offline(), // or ws({ topology, url? }) / fly({ app }) / convex({ topology }) / socketIo({ topology, url? }) / p2p({ room? }) / lan({ port?, path? }) / servers({ …, adapter }) — defaults to offline()
1558
+ content,
1559
+ loop, // Partial<GameLoop<GameContext>> — missing hooks default to no-ops
1560
+ GameUI,
1561
+ camera: { perspective: "third" }, // optional — this is the default
1562
+ });
1563
+ ```
1564
+
1565
+ ```ts
1566
+ // game/keybinds.ts — named actions + generated hotbar slots; one key, one action
1567
+ import { hotbarSlotBindings, type ActionCodesMap } from "@jgengine/core/input/actionBindings";
1568
+
1569
+ export const keybinds: ActionCodesMap = {
1570
+ moveForward: ["KeyW"], moveBack: ["KeyS"], moveLeft: ["KeyA"], moveRight: ["KeyD"],
1571
+ jump: ["Space"], sprint: ["ShiftLeft"],
1572
+ interact: ["KeyE"],
1573
+ crouch: { hold: ["KeyC"], toggle: ["KeyZ"] },
1574
+ aim: { hold: ["mouse2"], toggle: ["KeyV"] },
1575
+ tabTarget: ["Tab"], clearTarget: ["Escape"],
1576
+ ...hotbarSlotBindings(9), // hotbarSlot1..9 → Digit1..9 (a 10th slot gets Digit0)
1577
+ };
1578
+ ```
1579
+
1580
+ ```ts
1581
+ // game/inventories.ts
1582
+ import type { InventoryDeclaration } from "@jgengine/core/game/defineGame";
1583
+ export const inventories: Record<string, InventoryDeclaration> = {
1584
+ hotbar: { slots: 9, hud: "hotbar" },
1585
+ backpack: { slots: 28, traits: itemTraits },
1586
+ equipment: { slots: 4, accepts: ["weapon", "armor"], applyModifiers: true },
1587
+ };
1588
+
1589
+ // world.ts — top of src/, not under game/
1590
+ import type { PhysicsConfig } from "@jgengine/core/game/defineGame";
1591
+ import { biomes, type WorldFeature } from "@jgengine/core/world/features";
1592
+ export const world: WorldFeature = biomes({ map: "world/biomes", zones: "world/zones" });
1593
+ export const physics: PhysicsConfig = { gravity: -32 };
1594
+ ```
1595
+
1596
+ - `PhysicsConfig.gravity`/`jumpVelocity` tune the shell's built-in walk controller's fall/jump feel (defaults ~`-24`/`7.1`) — the only two levers on gravity and jump height; everything else about movement (speed, poses) stays catalog `movement` fields.
1597
+ - Input bindings are string arrays (hold semantics) or `{ hold, toggle, repeatMs? }` for the same verb. `repeatMs` turns a held action into an auto-repeat fire (build-mode place-on-drag, rapid-fire without a separate `wasPressed`/interval combo in game code) — the shell refires the command every `repeatMs` while the binding stays down, on top of its normal press-edge fire.
1598
+ - **Keybind → command convention.** The shell fires a command for any bound action that isn't reserved: pressing an action runs a command of the **same name** if one is defined, else a `ui.<action>` fallback (so `openBackpack` → `ui.openBackpack`). Just declare the binding and a matching command — no per-game `keydown` listener. Reserved actions the shell consumes natively and never routes to a command: `moveForward/moveBack/moveLeft/moveRight`, `turnLeft/turnRight`, `sprint`, `jump`, `tabTarget`, `clearTarget`, `useAbility`, `interact`, and any `hotbarSlotN`/`slotN`. `tabTarget`/`clearTarget` run `target.cycle`/`target.clear` (native `cycleTarget`/`setTarget` fallback).
1599
+ - **`interact`** is special: pressing it resolves the active proximity prompt from the `prompts` field of `defineGame({...})` and runs that prompt's `invoke` command. A prompt with `invoke: null` is display-only and does nothing on the key.
1600
+ - UI keybind badges derive from `keybinds` via `actionLabel(keybinds, "openBackpack")` — `bindingLabel` maps codes to short labels (`Digit1`→`1`, `KeyB`→`B`, `mouse0`→`LMB`, `Escape`→`Esc`). Never hardcode label strings; they drift the moment a binding changes.
1601
+ - `server.mode` is a string your loop/commands interpret — the engine ships no gamemode presets.
1602
+ - Never in defineGame: player tuning, catalog helpers (`defineItems` etc.), game nouns, behaviors, prompts, or inline binding/inventory/world blobs. The one exception is `physics.gravity`/`physics.jumpVelocity` — global controller tuning, not a catalog value (see "Controller kinematics" below).
1603
+ - `assets` may be omitted for a game with no models (a HUD-only card/board game, say) — `defineGame` injects an empty catalog, so `GameDefinition.assets` is always present downstream with no per-caller `?.` checks.
1604
+ - `devtools` defaults to `true` — every game gets the F2-toggled debug overlay (Perf/Tune/Logs/Net/Keys) for free, and every top-level `export const` number/boolean/color and every exported flat table of them under `src/` is auto-discovered into the Tune tab with zero game code; set `false` to disable the toggle entirely. See "Devtools — F2 overlay and tunables" below.
1605
+
1606
+ ### `@jgengine/core/game/defineGame` — the underlying primitive
1607
+
1608
+ The low-level engine boot call the shell `defineGame` composes internally: engine fields only (`name`, `assets`, `world`, `physics`, `inventories`, `input`, `server`, `save`, `time`, `feed`, `multiplayer`, `loop`) — no `content`/`GameUI`/`camera`/render fields, those are the shell layer's job.
1609
+
1610
+ ```ts
1611
+ import { defineGame as defineEngineGame } from "@jgengine/core/game/defineGame";
1612
+ import { offline } from "@jgengine/core/runtime/adapter";
1613
+
1614
+ const game = defineEngineGame({
1615
+ name: "My Game",
1616
+ assets, world, physics, inventories,
1617
+ input: keybinds,
1618
+ server: "persistent",
1619
+ save: { auto: "5m", scope: "player+chunks" },
1620
+ multiplayer: offline(),
1621
+ loop, // GameLoop<GameContext>
1622
+ });
1623
+ ```
1624
+
1625
+ Reach for this directly only outside a React host — a headless server, a non-shell runner; a browser game authors through `@jgengine/shell/defineGame` above, which calls this and returns the `PlayableGame` a runner needs.
1626
+
1627
+ ## `PlayableGame` — how a game plugs into a runner
1628
+
1629
+ The type `@jgengine/shell/defineGame` returns and every runner (`GameHost`, `GamePlayerShell`) consumes. A game never builds this object by hand — `defineGame({...})` assembles it from the merged config. Source type at `@jgengine/core/game/playableGame`:
1630
+
1631
+ ```ts
1632
+ export type PlayableGame<TUi = unknown> = {
1633
+ game: GameDefinition;
1634
+ content: GameContextContent; // { itemById?, entityById?, objectById? }
1635
+ loop: Required<GameLoop<GameContext>>; // onInit, onNewPlayer, onTick
1636
+ GameUI: TUi; // React component in web runners
1637
+ prompts?: (ctx: GameContext) => readonly PositionedPrompt[]; // interact-key + HUD source
1638
+ };
1639
+ ```
1640
+
1641
+ `prompts` is the **single source** of positioned proximity prompts: the shell reads it to fire the `interact` key, and the HUD should read the same list through `useActivePrompt(playable.prompts?.(ctx))` rather than building its own — one list, no drift. A prompt is only actionable if its `invoke` is non-null.
1642
+
1643
+ Optional render/world fields the shell also reads: `entitySprites` / `entityModels` (billboards / GLBs keyed by entity kind), `objectModels` (GLBs keyed by object catalog id), `renderObject` (per-object visual override — return your own mesh for a placed object and the shell still positions it; falls back to `objectModels` → colored box), `WorldOverlay` (canvas-layer VFX), `environment` (canvas-layer scenery — ground/sky/structures; when set, replaces the default ground plane + debug grid + rock field), `camera`, `shadows` (cast/receive shadows across the R3F canvas; default true), and `worldHealthBars` (`boolean | { statId?, roles? }` — `roles` restricts bars to entities whose catalog `role` is in the given `CatalogEntityRole` list, e.g. skip friendly NPCs). A model value is a catalog id (`string`, resolved via `game.assets`) or an inline `ModelConfig { url, scale?, y?, anchor?, dims?, material?, animation? }` (`material` overrides color/metalness/roughness/emissive/emissiveIntensity on the cloned mesh, leaving shared GLTF caches untouched; `animation?: { clip?, loop?, timeScale?, paused?, time? }` plays a GLTF clip — `clip` defaults to the first clip, `loop` defaults true — and `paused: true` + `time: <seconds>` holds the rig on one fixed frame, a pose library for inventory previews or held cutscene poses). Catalog-resolved models carry measured `dims` (`catalog.resolve(id).dims = { footprint:{w,d}, center:{x,z}, minY }`); with the default `anchor: "center"` the shell centers the footprint on the placement point and ground-snaps `minY` to it, so corner-pivot kit models place correctly with no per-game pivot math. Applies through both `entityModels` and `objectModels`.
1644
+
1645
+ `renderEntity?: (entity: SceneEntity) => ReactNode` and `renderObject?: (object: SceneObject) => ReactNode` hand you the mesh for one entity/object while the shell still positions it and keeps it tagged for picking/selection; return null/undefined to fall through to model → sprite/box. `objectStyles?: Record<catalogId, { color?, opacity?, hidden? }>` styles the default colored-box object render — `color` overrides the hash color, `opacity < 1` sets transparent, `hidden` skips the mesh but keeps the picking tag.
1646
+
1647
+ **Presentation mode.** `PlayableGame.presentation`: `"3d"` (default) mounts the canvas, camera rig, and pointer; `"hud"` mounts none of that — the game is `GameUI` plus the command/input loop, for board/card/menu games that need no 3D camera at all.
1648
+
1649
+ **Auto environment.** When `world` is an `environment()` descriptor and `PlayableGame.environment` is unset, the shell renders that descriptor as the backdrop automatically — no manual `environment` wiring needed for the common case. Set `environment` explicitly only to override that default (a custom canvas component always wins). The same auto-render convention covers grid-cell worlds (`biomes`/`voxel`/`plots`/`tilemap`) — see "World features" below.
1650
+
1651
+ **Lighting and backdrop.** `PlayableGame.lighting` (`LightingConfig`, `@jgengine/core/game/playableGame`) replaces the shell's hardcoded ambient/directional default when present, regardless of world kind: `ambient?: { color?, intensity? }`, `directional?: { color?, intensity?, position, castShadow? }[]`, `hemisphere?: { skyColor?, groundColor?, intensity? }`. `PlayableGame.backdrop` (`BackdropConfig`) is a generic background/sky/fog for **any** world kind, including a custom `environment` component: `background?: string` (CSS color), `sky?: SkyEnvironmentConfig` (same descriptor `environment()`'s `sky` field takes), `fog?: { color?, near?, far?, density? }` (`density` set switches to exponential `FogExp2` and `near`/`far` are ignored). Both are optional and additive to whatever the world/`environment` renderer already draws.
1652
+
1653
+ **Visibility & streaming (automatic).** Every 3D game gets camera frustum + distance culling for free — the shell's `CullingProvider` reads the live camera each frame, runs the engine `createVisibilitySystem` (`@jgengine/core/visibility/visibilitySystem`) over the scene's entities and placed objects, and toggles `group.visible` so off-screen objects are never submitted to the renderer (never unmounted — gameplay and simulation are untouched). Defaults are conservative (a preload margin larger than the view, hysteresis, `Infinity` default render distance) so existing games only benefit. Tune or opt out via `PlayableGame.visibility` (`VisibilityConfig`, `@jgengine/core/visibility/config`): `enabled: false` disables it; `culling`/`streaming` patch the global `CullingSettings`/`StreamingSettings` (`@jgengine/core/visibility/settings`); `scene` sets scene-wide overrides; `entities`/`objects` override by kind name / catalog id (`alwaysVisible`, `maxRenderDistance`, custom `bounds`, `pinned`, `cullingDisabled`, …). The engine layer also ships `@jgengine/core/visibility/spatialIndex` (the 3D hash the culler queries instead of scanning every object), `@jgengine/core/visibility/assetStreaming` (dedup/budget/grace-period asset loading), and `@jgengine/core/visibility/simulationCulling` (opt-in, off by default — throttles low-priority off-screen updates, never protected entities). Full reference: `packages/core/src/visibility/README.md`.
1654
+
1655
+ **Player movement tuning** — the `movement` field (`PlayerMovementConfig`) tunes the shell's local-player walk controller beyond `physics.gravity`/`jumpVelocity`: `mode` (`"free"` camera-relative default, `"axis"` locks travel to one world `axis`, `"grid"` snaps each committed step to `cellSize` centers), `collideObjects` (collide against placed scene objects as unit-box AABBs even without `collision.voxel`), and `beforeCommit(frame)` — an escape hatch called each frame with `{ entityId, current, next, dt }` that can return a replacement `[x, y, z]` to constrain or redirect the step (rails, bounds, custom collision) before it commits and before `onTick` runs.
1656
+
1657
+ The runner boots `createGameContext({ definition, content, player: { userId, isNew } })`, calls `loop.onInit(ctx)` then `loop.onNewPlayer(ctx)`, and drives `loop.onTick(ctx, dt)` per frame. **Convention: `onNewPlayer` spawns the player entity with `id === ctx.player.userId`** — bounded stats, targeting, and kill attribution key off that.
1658
+
1659
+ ### Object spatial queries, entity patching, and surface sampling
1660
+
1661
+ ```ts
1662
+ ctx.scene.object.at(x, y, z) // cell lookup, cell size 1, most-recent wins
1663
+ ctx.scene.object.inBox(min, max) // inclusive AABB query
1664
+ ctx.scene.object.raycast({ origin, direction, maxDistance, halfExtents?, filter? }) // → nearest hit or null
1665
+ ctx.scene.object.raycastAll({ origin, direction, maxDistance, halfExtents?, filter? }) // → hits, nearest-first
1666
+ ctx.scene.entity.update(id, patch) // name/position/rotations/role/movement/behaviors/meta; false for unknown id
1667
+ ```
1668
+
1669
+ Placed objects are unit boxes (half-extents `[0.5, 0.5, 0.5]`) centered on position, matching the shell's default render, so `raycast`/`raycastAll` (`scene/objectQuery`) match what a player sees. `entity.update` notifies subscribers and bumps `ctx.version()` the same as `spawn`/`despawn`/`setPose` — it's the general-purpose patch the more specific methods (`setPose`, `form.shapeshift`, `possession.possess`) build on.
1670
+
1671
+ `ctx.scene.object.place(catalogId, x, y, z, { instanceId?, parentSpace?, rotation?, visual? })` takes an optional `visual: ObjectVisual` (`@jgengine/core/scene/objectStore`) — `{ scale?: number | [x,y,z], color?, opacity? }` — a per-instance render override independent of the catalog entry; `ctx.scene.object.setVisual(instanceId, visual | undefined)` changes it after placement (`undefined` clears back to the catalog default). Distinct from `objectStyles` on `PlayableGame` (styles a catalog id for every instance); `visual`/`setVisual` targets one placed instance (a damaged crate, a dyed banner, a resized prop).
1672
+
1673
+ `pointerService.worldHitCenter()` (shell) casts from the viewport center regardless of cursor presence (pointer-lock aim) — combine with `pointer.worldHit()` (cursor-driven) to support both mouse-look and free-cursor games from the same probe. `PointerHit` also carries an optional `uv?: { u, v }` on UV-mapped mesh hits (absent for the ground fallback), `material?: { color, metalness?, roughness? } | null` sampled off the hit mesh's `MeshStandardMaterial` (`null`/unset for non-standard materials, e.g. the ground plane) — combine `uv` + `material` for paint tools, decals, and material-aware interaction — and `instanceId?: number`, the hit index when the intersected mesh is a `THREE.InstancedMesh` (grid-world cells, `InstancedBodies` debris, any instanced render), absent otherwise.
1674
+
1675
+ **Runtime paint layer** (`ctx.scene.entity.paint`, backed by `scene/paintLayer`) — a `PaintLayer` keyed by instance id (entity or object): `paint(instanceId, { u, v, radius, color })`, `strokes(instanceId)`, `clear(instanceId?)`, `version(instanceId)` (bumps per paint/clear), `subscribe(listener)`. The shell auto-renders painted instances through a lazily-created 512×512 canvas texture kept in sync — no per-game render wiring. Clearing refills with the material's base color; the original texture pixels are not restored.
1676
+
1677
+ ### Audio — positional emitters, listener falloff, buses
1678
+ Catalog-first, no per-game audio glue. The `audio` field of `defineGame({...})` — `{ sounds: Record<string, SoundDef>, buses?: Record<string, AudioBusDef> }` — declares the sound catalog (`SoundDef = { id, url, bus, gain?, loop?, positional?, falloff? }`) and mix buses (`music`/`sfx`/`ambient`/…, `AudioBusDef = { id, gain? }`) — both types from `@jgengine/core/audio/audioFalloff`. `entitySounds?: Record<string, string>` maps an entity **kind name** (same convention as `entitySprites`/`entityModels`) to a sound id: while a matching entity exists, the shell keeps a looping positional emitter on it, repositioned every frame. `objectSounds?: Record<string, string>` does the same keyed by placed-object catalog id. The pure distance→gain math (`computeFalloffGain(distance, config)`, curves `"linear" | "inverse" | "none"`) lives in core so it is unit-tested without a browser; `@jgengine/shell` (`shell/audio/audioEngine`, `shell/audio/AudioComponents`) is the only package that touches Web Audio — it owns an `AudioContext`, mounts `AudioListener` on the camera every frame, and `EntityAudioEmitters`/`ObjectAudioEmitters` drive per-instance emitter gain from the core falloff function. `GamePlayerShell` wires all of this automatically from `playable.audio`/`entitySounds`/`objectSounds` — a game never touches `AudioContext` directly.
1679
+ ### Camera rigs (`camera` field of `defineGame({...})`)
1680
+ The shell ships a **rig library**; a game picks and tunes one through `camera` config, never by writing camera positions from `onTick`. Select with `camera.rig` (or the `perspective: "third" | "first"` shorthand) — or by config block alone (#207.8): supplying `camera.<rig>` selects that rig with no redundant `rig` field, checked in the table's order; an explicit `rig` wins when several blocks are present:
1681
+ | `rig` | For | Key config (`camera.<rig>`) |
1682
+ |-------|-----|------------------------------|
1683
+ | `orbit` (default) | Third-person chase | `initialDistance`, `targetHeight`, `min/maxPolarAngle`, drag/zoom |
1684
+ | `first` | FPS mouse-look | `firstPerson: { eyeHeight, sensitivity, maxPitch, reticle, viewmodel }` |
1685
+ | `topDown` | ARPG iso / top-down (Diablo IV, Hades II) | `topDown: { height, pitch, yaw, followSmoothing, zoom }` — decoupled follow; `pitch` is camera elevation (PI/2 = straight down, near 0 = grazing and boom-distance blows up past `frustum.far`) |
1686
+ | `rts` | Free-pan / edge-scroll (The Sims, Manor Lords) | `rts: { panSpeed, edgeScroll, rotateSpeed, bounds, start, pan }` — `pan: false` turns it into a static backdrop camera: no WASD/arrow pan, no edge-scroll, no Q/E rotate, no wheel zoom, still re-centers on `followEntityId` if one resolves |
1687
+ | `shoulder` | Over-the-shoulder (Helldivers 2, Remnant II) | `shoulder: { shoulderOffset, distance, ads, side }` — ADS + shoulder-swap (V) |
1688
+ | `lockOn` | Souls-like strafe (Elden Ring) | `lockOn: { targetEntityId?, distance, framingBias, yawSmoothing }` — yaw binds to player→target; WASD becomes strafe |
1689
+ | `chase` | Vehicle chase (Forza, Rocket League) | `chase: { distance, springDamping, fov: { base, max, speedForMax }, shakePerSpeed, view: "chase"|"cockpit"|"hood"|"rear" }` |
1690
+ | `sideScroll` | Fixed lateral follow — 2.5D platformer/beat-'em-up | `sideScroll: { distance, height, lookHeight, axis: "x"\|"z", followSmoothing, fov }` — reads no player input, follows like the other follow rigs (defaults to the local player) |
1691
+ | `observer` | Detached spectator/photo/kill-cam (#120) | `observer: { bind: { kind: "entity", entityId } \| { kind: "point", position }, distance, height, orbitSpeed }` — reads no player input, auto-orbits the bound subject |
1692
+ | `inspection` | Model-viewer / data-viz orbit (#207.7) | `inspection: { anchor: "target"\|"cursor"\|"center", target, initialDistance, initialPosition, min/maxDistance, min/maxPolarAngle, pan, rotateSpeed, zoomSpeed, dampingFactor }` — left-drag orbit, middle/right-drag pan (pan defaults on for this rig only), scroll zoom toward the anchor (`cursor` = zoom-to-cursor); orbits a fixed `target`, never reads player/entity state |
1693
+ | `none` | No camera rig mounted | HUD-only presentations or a game that manages its own camera; see `presentation: "hud"` below |
1694
+ **Frustum:** `camera.frustum: { fov?, near?, far? }` overrides the canvas camera; `far` defaults to 300, so any world whose content spans more than a few hundred units must raise it or distant settlements/terrain silently clip out of view. **Every rig accepts `followEntityId: null`** so avatar-less games (city-builders, card games, auto-battlers) still mount a camera. Leave `followEntityId` unset and the shell defaults it to `ctx.player.possession.active(userId)` every frame, so a possession swap (party control-swap, BG3-style) or a form's mesh/camera-relevant change re-targets the camera automatically — set it explicitly only to override that default. **Shake / trauma (#28):** every rig reads a shake channel; feed it from anywhere with `import { cameraShake } from "@jgengine/shell/camera"` — `cameraShake(amplitude, decayPerSecond?)` (amplitude 0..1) — or from React via `useCameraShake()`. Tune with `camera.shake: { maxOffset, maxRoll, decayPerSecond, exponent, frequency }`. **Cinematic (#29):** set `camera.cinematic: { keyframes: [{ position, lookAt, fov?, duration?, ease? }], loop? }` to play a scripted path over the active rig, and `camera.transitionSeconds` cross-fades the camera when the rig changes so mode swaps don't hard-cut. The pure rig math (shake decay, spring-arm, speed→FOV, offset/strafe, keyframe lerp) is exported from `@jgengine/shell/camera` for testing.
1695
+
1696
+ ## `GameContext` — the ctx surface
1697
+
1698
+ `createGameContext` (in `@jgengine/core/runtime/gameContext`) wires every system:
1699
+
1700
+ ```
1701
+ ctx.scene.object place, remove, move, rotate, get, list, subscribe,
1702
+ at, inBox, raycast, raycastAll, catalog
1703
+ ctx.scene.entity spawn, despawn, setPose, update, get, list,
1704
+ stats.{get,set,delta}, setTarget, getTarget, cycleTarget,
1705
+ canReceive, preview, effect, paint,
1706
+ willHitProjectile, fireProjectile, settleProjectile,
1707
+ distance, inRadius, hasLineOfSight, queryArc, moveToward,
1708
+ spawnPoseOf, resetToSpawn, resetAllToSpawn,
1709
+ form.{register,get,active,abilities,shapeshift,revert}
1710
+ ctx.game commands, events, feed, loot, trade, quest, social, chat,
1711
+ unlocks, economy, leaderboard, roster, store, cards, turn
1712
+ ctx.game.social friends, party, presence, emotes.play, worldInvites
1713
+ ctx.game.store set, delete, get, has, subscribe, mapSnapshot, arraySnapshot — game-defined
1714
+ keyed reactive store slot (any value type); mutations bump ctx.version()
1715
+ ctx.game.cards pile(id, config?) — lazily creates (config required on first call) or returns
1716
+ the existing notify-wrapped CardPile for id
1717
+ ctx.game.turn loop(id, config?) — lazily creates (config required on first call) or returns
1718
+ the existing notify-wrapped TurnLoop for id
1719
+ ctx.player userId, isNew, inventory, stats (modifiers), loadout,
1720
+ applyLoadout, movement (pose/aim), motion (impulse/setVerticalVelocity/setY/takePending),
1721
+ possession, cosmetics
1722
+ ctx.player.motion impulse(vy), setVerticalVelocity(vy), setY(y), takePending() — game-code
1723
+ seam into the shell's vertical-motion integrator; drained once per frame
1724
+ before gravity, so a jump pad or grapple release calls this from
1725
+ onTick/commands instead of touching y directly
1726
+ ctx.item use, weapon
1727
+ ctx.input publish(held), isDown(action), held() — per-frame held-action snapshot, polled from onTick
1728
+ ctx.world ground (TerrainField), groundHeightAt(x, z) — the canonical
1729
+ sampler for the game's declared world; environment worlds
1730
+ resolve their terrain field, every other world kind is 0.
1731
+ Use it for every spawn/placement/waypoint y — never
1732
+ hand-roll a noise sampler or hardcode y = 0 on relief
1733
+ ctx.camera follow(entityId | null), followedEntityId(), setCinematic(config), cinematic(),
1734
+ subscribe — runtime camera-follow/cinematic override; the shell reads
1735
+ followedEntityId() each frame, falling back to the static
1736
+ playable.camera.followEntityId when it returns undefined
1737
+ ctx.time advance, now, calendar, snapshot; pause, play, toggle,
1738
+ setSpeed, cycleSpeed; after, every, at (game-time timers)
1739
+ ctx.subscribe / ctx.version change signal — UI layers bind via useSyncExternalStore
1740
+ ```
1741
+
1742
+ `content.itemById(id)` supplies `{ use?, weapon?, trade? }`; `content.entityById(id)` supplies `{ stats?, receive?, onDeath?, movement?, role? }`; `content.objectById(id)` supplies `GameContextObjectEntry` `{ proximityPrompt?, breakable?, slotInventory? }`. Build all three from your catalogs in `content.ts`. A placed object resolves its catalog entry via `ctx.scene.object.catalog(instanceId)`; `ctx.scene.object.at(x, y, z, tolerance?)` finds placed objects near a point (grid interaction, click resolution beyond the pointer service). `ctx.scene.entity.update(id, patch)` writes a shallow patch onto a spawned entity's mutable fields (e.g. `movement.walkSpeed`) without a full respawn — `scene/movementSpeed`'s `applyStatDrivenSpeed(deps, id, { baseSpeed, multiplierStat?, flatBonusStat? })` is the catalog-driven helper that recomputes and writes `movement.walkSpeed` from a stat-modifier pair each time a buff changes.
1743
+
1744
+ ### Two tiers: `ctx` runtime vs pure factories
1745
+
1746
+ The `ctx` surface above is the **stateful runtime** — it's what game code uses. Every subsystem it wires is *also* exported as a **pure factory** that `createGameContext` composes internally: `createTradeSystem`, `createDeathSystem`, `createEffectSystem`, `createProjectileSystem`, `createSpatialApi`, `createEntityStatsApi`, `createEntityStore`, `createObjectStore`, `createStats`, `createLoadouts`, `createLootRegistry`, `createQuestJournal`, `createSocial`, `createSlots`, `createInteriors` (plus stateless helpers beside each — `canAffordCosts`/`resolveBuy` in `game/trade`, `getStatValue`/`applyPoolDelta` in `scene/entityStats`, and so on). **Build a game through `ctx`, not these** — reach for the factories only for unit tests of pure game math, headless servers, or a custom runtime. Import the domain deep path (`@jgengine/core/combat/death`, `@jgengine/core/game/trade`, `@jgengine/core/stats/statModifiers`, …) and read the `.d.ts`; each is a real export in the published package.
1747
+
1748
+ `createSpatialApi`'s optional `grid: { cellSize }` opts `inRadius`/`queryArc` into a lazily-built x/z broadphase index over `candidates()` instead of a linear scan — worth it once candidate counts run into the hundreds+. The index is reused across calls until `invalidate()` is called, so call it after any position change (move, spawn, despawn); a candidate outside the index at query time still resolves exactly (never silently skipped), only a *moved* one can be missed until invalidated.
1749
+
1750
+ ## `loop` — lifecycle
1751
+
1752
+ ```ts
1753
+ export function onInit(ctx: GameContext) {
1754
+ ctx.item.use.register(itemUseHandlers);
1755
+ ctx.player.loadout.register(loadouts);
1756
+ for (const table of lootTables) ctx.game.loot.register(table);
1757
+ ctx.game.quest.register(quests);
1758
+ ctx.game.quest.bind("entity.died");
1759
+ ctx.game.feed.bind("entity.died");
1760
+ ctx.game.events.on("entity.died", (evt) => onEntityDied(ctx, evt));
1761
+ setupWorld(ctx);
1762
+ }
1763
+
1764
+ export function onNewPlayer(ctx: GameContext) {
1765
+ ctx.scene.entity.spawn("player_default", { id: ctx.player.userId, position: spawnPoint });
1766
+ if (ctx.player.isNew) ctx.player.applyLoadout(ctx.player.userId, "starterKit");
1767
+ }
1768
+
1769
+ export function onTick(ctx: GameContext, dt: number) {
1770
+ // AI, regen, respawn timers — dt is GAME time (see ctx.time). Never death detection (see entity.died)
1771
+ }
1772
+ ```
1773
+
1774
+ `onInit` runs once per boot; register everything there. Loot tables register through `ctx.game.loot.register` — `lootTable()` is a pure validating factory, there is no global side-effect registry.
1775
+
1776
+ ## `ctx.time` — the simulation clock
1777
+
1778
+ `onTick`'s `dt` is **game time, not real time**: the shell scales each frame's real delta by `definition.time.scale` (real→game seconds at 1×) and the live speed multiplier, so writing decay/regen/AI as `rate * dt` makes it obey pause and fast-forward for free — never read wall-clock in a tick. Configure via `defineGame({ time: { scale?, speeds?, dayLength?, start?, startPaused?, daysPerYear?, seasons? } })` (all optional; default is real-time 1:1 with speeds `[1,2,3,4]`, 365-day years).
1779
+
1780
+ - **Continuous** work scales through `dt`. **Scheduled** work uses game-time timers: `ctx.time.after(sec, cb)`, `ctx.time.every(sec, cb)`, `ctx.time.at(gameSec, cb)` — measured in game-seconds, so 4× fires them 4× sooner and pause freezes them. Each returns a cancel handle.
1781
+ - **Controls** (drive from a HUD or a command): `pause()`, `play()`, `toggle()`, `setSpeed(mult)` (0 pauses), `cycleSpeed()`. Read state with `ctx.time.snapshot()` / `ctx.time.calendar()` (`{ day, hour, minute, second, dayFraction, year, dayOfYear, yearFraction, season? }`), or in React with `useGameClock()` → snapshot + `controls`. Speeding to 4× or pausing affects **everything** on the tick — no per-system wiring.
1782
+ - **Calendar year/season** rides the same day counter, no separate clock: `year`/`dayOfYear` fall out of `day` divided by `TimeConfig.daysPerYear` (default 365), `yearFraction` is progress through the current year (0..1). Setting `TimeConfig.seasons: string[]` (e.g. `["spring","summer","fall","winter"]`) slices the year into equal named segments and populates `calendar().season`; omit `seasons` and the field is absent — a living-world sim names its own season boundaries this way instead of a hand-rolled `dayOfYear % ...` module.
1783
+
1784
+ ### Beat clock — BPM signal + input quantization
1785
+
1786
+ `@jgengine/core/time/beatClock` is a separate, purpose-built signal from `simClock` — a BPM tick generator for rhythm games (Hi-Fi Rush–style quantized combat), not a day/pause clock. `createBeatClock({ bpm, beatsPerBar? }, onBeat?)` returns a `BeatClock`: call `advance(gameDt)` from `onTick` with the same **game-time** `dt` (never wall-clock) — it fires `onBeat(beatIndex)` once per newly crossed integer beat and returns a `BeatSnapshot` (`beat`, `beatIndex`, `bar`, `beatInBar`, `phase`). `createBeatInputBuffer<T>(beatDurationSec)` is the auto-correct input buffer: `buffer(action, nowSec)` quantizes an off-beat press to fire on the next beat tick (or immediately if pressed exactly on one); `advance(nowSec)` drains and returns every action whose beat has arrived. `nextBeatTime(nowSec, beatDurationSec)` is the underlying pure quantization function. Feed a music track's actual BPM in; the buffer is what makes an early/late input still land on-beat.
1787
+
1788
+ ## Content catalogs
1789
+ ## `ctx.game.store` — reactive game state
1790
+
1791
+ ```ts
1792
+ ctx.game.store.set("health", 100) // any key, any value type
1793
+ ctx.game.store.get("health") // T | undefined
1794
+ ctx.game.store.has("health")
1795
+ ctx.game.store.delete("health")
1796
+ ctx.game.store.subscribe(listener) // change-signal fires on set/delete
1797
+ ctx.game.store.mapSnapshot() / arraySnapshot()
1798
+ ```
1799
+
1800
+ A reactive per-game keyed store (`ObservableKeyedStore<unknown>`) attached to `GameContext` — reach for it instead of a module-level singleton store for ad-hoc reactive game state (turn trackers, deck UIs, anything that doesn't already have a `ctx` surface). `set`/`delete` bump `ctx.version()` and notify `ctx.subscribe` listeners; `get`/`has` are plain reads. Unlike a per-slot handle, there is no `define`/seed step — a key simply doesn't exist until the first `set`.
1801
+
1802
+ ## `ctx.game.cards` / `ctx.game.turn` — lazily-created piles and turn loops
1803
+
1804
+ `ctx.game.cards.pile(id, config?)` and `ctx.game.turn.loop(id, config?)` lazily create (config required on first call) or return the existing notify-wrapped `CardPile`/`TurnLoop` for `id` — call with just the id after the first `onInit` seed to fetch the same instance; every mutating method is wrapped so it bumps `ctx.version()`/notifies `ctx.subscribe` automatically, same as every other `ctx` surface. This replaces manually constructing `createCardPile`/`createTurnLoop` and wiring notification yourself.
1805
+
1806
+ ## Movement, pose, input
1807
+ ## External data — `data/dataSource` and the dev proxy
1808
+
1809
+ Renderer-free async-state primitives (`@jgengine/core/data`) for a game that reads a live external source (a leaderboard API, a session browser, remote config) — distinct from `ctx.game.store`/multiplayer, which are for the game's own authoritative state.
1810
+
1811
+ - **`createDataSource(load, options?)`** (`data/dataSource`) → `DataSource<T>` wraps one `load(signal)` async call as `{ status: "idle"|"loading"|"ready"|"error", data, error }`. `getState()` reads the current snapshot, `subscribe(listener)` fires on every change, `refresh({ force? })` re-runs `load` (de-duplicates a call already in flight unless `force`; aborts the prior call first when forced), `startPolling(intervalMs?)`/`stopPolling()` run `refresh` on an interval (`intervalMs` falls back to the one passed at construction; throws if neither is given), `dispose()` tears down polling and in-flight requests for good. Pass `options.clock` (`{ setInterval, clearInterval }`) to swap the timer source in tests.
1812
+ - **`fetchJson<T>(url, options?)`** (`data/fetchJson`) — `fetch` + JSON-parse in one call; throws `HttpStatusError` (`status`, `statusText`, `url`) on a non-OK response and `JsonParseError` (`url`, `cause`) on unparsable JSON, so a `DataSource`'s `error` is always one of these two typed shapes, never a bare `Error`. `options.fetchImpl` swaps the fetch implementation for tests/SSR.
1813
+ - **`createJsonDataSource<T>(url, options?)`** (`data/jsonDataSource`) — sugar combining the two above: a `DataSource<T>` whose `load` calls `fetchJson(url, options)`.
1814
+ - **Dev proxy (`data/devProxy`)** — same-origin routing for external APIs during `bun dev` so browser CORS never blocks a game's `fetchJson` call against a third-party host. `parseDevProxyTable(raw)` parses a `VITE_JGENGINE_DEV_PROXY` env value (a JSON object of `{ routeName: "https://api.example.com" }`) into a `DevProxyTable`; `proxiedUrl(target, { dev?, table?, prefix? })` rewrites a `target` URL whose prefix matches a table entry into `/proxy/<routeName>/<rest>` (default prefix `/proxy`) when `dev` is true (defaults to `import.meta.env.DEV`) — else returns `target` unchanged, so the same call hits the real host in production. `apps/dev`'s `vite.config.ts` reads the same env var and wires a matching Vite server `proxy` entry per route (`changeOrigin: true`, strips the `/proxy/<routeName>` prefix) — set `VITE_JGENGINE_DEV_PROXY` once and both sides (the URL rewrite and the actual proxy route) agree.
1815
+
1816
+ ## Multiplayer and the backend seam
1817
+ ## Genre cheat sheet
1818
+
1819
+ - **Voxel/crafting**: objects for blocks/machines, `voxel()`, `object.break`/`object.placeFromInventory`.
1820
+ - **Tycoon/lab**: objects + `slotInventory`, `plots()`, configure via prompt → command.
1821
+ - **Shooter**: `fireProjectile`/`settleProjectile`; grenades settle → `effect({ at, radius })`; `movement.poses`/`aim` + zoom modifier; `servers({ … })` + game-owned `server.mode`; loadout classes from commands.
1822
+ - **MMO/RPG**: bounded stats + `leveling()` over a game XP curve; `tabTarget` → `cycleTarget`; handlers read `getTarget`; quests bound to `entity.died`/`inventory.added`; social party + `partyShare`; `server: "persistent"`.
1823
+ - **All combat games**: react to `entity.died` (feed/leaderboard/score) — never poll HP.
1824
+
1825
+ ## Anti-patterns
1826
+
1827
+ | Wrong | Right |
1828
+ |-------|-------|
1829
+ | Player tuning in `defineGame` | Entity catalog `movement` + stats |
1830
+ | `behaviors: […]` on place/spawn | Catalog entry |
1831
+ | Engine `weapon.fire` / `consumable.use` / `combat.*` | `item.use` + catalog `use` → game handler |
1832
+ | `ItemUseInput.to` for targets | `getTarget(from)` in handlers |
1833
+ | `effect({ to })` for gunshots | `fireProjectile` + `settleProjectile` |
1834
+ | Polling HP in `onTick` for kills | `entity.died` event |
1835
+ | `combat.lootTable` / `loot.enemy` | `onDeath` on the entity that died |
1836
+ | Hand-rolled `Math.random()` loot in commands | `lootTable()` + `ctx.game.loot.roll` |
1837
+ | Hand-rolled `xpForLevel`/`levelFromXp` | `game/progression` `curve()` + `leveling()` |
1838
+ | Hardcoded shop arrays | `item.trade.shops` + `tradableAt` |
1839
+ | Kit seeding via scattered `put`/`grant` | `applyLoadout` |
1840
+ | Per-user quest state hand-rolled | `game.quest.register` + binds |
1841
+ | `useKillFeed` / per-domain feed hooks | `useFeed({ action })` |
1842
+ | Raw keys in game logic | `defineGame` input actions |
1843
+ | Positioning inside `ui/components/` or on primitives (`CurrencyPill className="absolute …"`) | Screen wrappers in `GameUI.tsx` only |
1844
+ | Game UI classes without `@source` in host CSS | `@source` entries for your game dirs + `node_modules/@jgengine/{shell,react}` |
1845
+ | One file per catalog entry / per brand | Dense `<domain>/catalog.ts` |
1846
+ | Convex mutations called from game code | `commands.run` through the `GameBackend` transport |
1847
+ | Half a system: quest without tracker, cooldown without sweep, keybind never shown, stub "coming soon" modal | Finish the system end to end — or cut it whole (see `jgengine`) |
1848
+ | Game-side workaround for a missing engine primitive | File the gap at github.com/Noisemaker111/jgengine/issues (or PR the primitive) and cut or scope the dependent system honestly |
1849
+ | Game nouns in this skill | Engine primitives + placeholder ids only |
1850
+
1851
+ ## New-game definition of done
1852
+
1853
+ This is a gate, not a suggestion — every box, in one pass (workflow: **`jgengine`** skill). "Compiles and the hooks are wired" is not done; a declared system with no UI, no feedback, or no way to exercise it is not done — finish the system or cut it whole.
1854
+
1855
+ - [ ] `game.config.ts` (`defineGame` from `@jgengine/shell/defineGame`) + `index.tsx` (barrel) + `main.tsx` (standalone host) + `loop.ts` + `game/content.ts`
1856
+ - [ ] Catalogs: `game/entities/<role>/catalog.ts`, `game/items/<domain>/catalog.ts`, `game/objects/catalog.ts`; loot tables beside their domain
1857
+ - [ ] Entity `stats` + `receive` orders aligned on the same stat ids; `role` set (drives targeting + camera)
1858
+ - [ ] `game/items/use-handlers.ts` registered in `onInit`; handlers read `getTarget`/`aim`, never a target input
1859
+ - [ ] `game/loadouts.ts` + `applyLoadout` in `onNewPlayer` (gated on `isNew`)
1860
+ - [ ] `game/quests/catalog.ts` + binds; if using xp/level, a game-owned curve fed to `game/progression` (`curve`/`leveling`) — **with their HUD/tracker, or cut**
1861
+ - [ ] `onInit`: register handlers/loadouts/loot/quests, event listeners, feed binds, leaderboard tracks; `setupWorld`
1862
+ - [ ] Player spawns with `id === ctx.player.userId`
1863
+ - [ ] `game/ui/GameUI.tsx` owns layout; components use `@jgengine/react` hooks
1864
+ - [ ] UI passes the **quality bar** above (contrast, scale, framing, genre fit) — not just hook wiring
1865
+ - [ ] Camera tuned via `camera` in `defineGame({...})` — defaults untouched means the feel was never checked
1866
+ - [ ] For an `environment()` world: a `<game>.world.test.ts` asserts `summarizeEnvironment(world)` (`@jgengine/core/world/environmentSummary`) is non-empty with the expected counts — the browserless scene-correctness gate
1867
+ - [ ] HUD screenshotted over a staged `GameUiPreview` scenario and **judged by looking at the image** against the UI quality bar in [`../jgengine-ui/reference.md`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md) — the final human glance, not the verification loop
1868
+ - [ ] Co-located bun tests for pure game math (curves, cooldowns, spawn logic)
1869
+ - [ ] Multiplayer via adapter config only; no direct backend calls
1870
+
1871
+ ## Quick reference
1872
+
1873
+ ```
1874
+ defineGame (shell) engine fields (assets, world, physics, inventories, input, server, save, time, feed, multiplayer)
1875
+ + presentation fields (content, loop, GameUI, camera, environment, shadows, movement, devtools, …) in one call — smart defaults fill the rest
1876
+ defineGame (core) the underlying engine-only primitive: assets, world, physics, inventories, input, server, save, time, feed, multiplayer, loop
1877
+ PlayableGame { game, content, loop, GameUI, camera, … } — the runner contract `defineGame` (shell) returns
1878
+ GameContext ctx.scene / ctx.game / ctx.player / ctx.item / ctx.camera / ctx.input + subscribe/version
1879
+ scene.object place, remove, move, rotate, at, setVisual (per-instance ObjectVisual: scale/color/opacity override)
1880
+ scene.entity spawn (anchor/offset), despawn, setPose, update; stats; targeting; effects;
1881
+
1882
+ # jgengine-ui
1883
+
1884
+ Use this skill for the **visual and interaction design of the game interface**: title screens, HUDs, menus, prompts, maps, inventories, dialogue, touch controls, transitions, pause/results states, accessibility, and screenshot critique.
1885
+
1886
+ Do not use this skill as a React, routing, state-management, or hooks reference. The main `jgengine` skill owns routing and points to the engine APIs. When implementation needs `@jgengine/react` hooks or shell APIs, follow the links in the main skill and the compact API appendix in [reference.md](reference.md); keep this skill focused on what the player sees and feels.
1887
+
1888
+ ## Required outcome
1889
+
1890
+ A JGengine game must read as a self-contained game, not a responsive website with a canvas inside it.
1891
+
1892
+ Before shipping UI:
1893
+
1894
+ 1. Give the game a concise UI art direction.
1895
+ 2. Compose explicit desktop/mobile game layouts instead of document flow.
1896
+ 3. Keep persistent HUD information sparse and hierarchical.
1897
+ 4. Adapt touch controls to the genre and reserve their screen zones.
1898
+ 5. Implement authored focus, pressed, selected, disabled, success, failure, and warning states.
1899
+ 6. Add purposeful motion and feedback.
1900
+ 7. Capture screenshots and revise what actually renders.
1901
+
1902
+ ## Ownership boundary
1903
+
1904
+ The main `jgengine` skill owns intake, engine architecture, API routing, hooks, commands, state, and verification routing. This skill owns presentation quality.
1905
+
1906
+ Read [reference.md](reference.md) when building or reviewing a game interface. It contains the implementation quality bar, layout rules, art-direction template, touch-control requirements, acceptance criteria, and the compact existing React API surface.
1907
+
1908
+ ## Non-negotiable defaults
1909
+
1910
+ - Active play owns the viewport; no marketing header, page title bar, document scrolling, or website container.
1911
+ - Screen placement belongs in the game's `ui/GameUI.tsx` composition layer.
1912
+ - Persistent gameplay information is frameless unless a physical/diegetic frame is part of the game's art direction.
1913
+ - Instructions are contextual and temporary, not permanent keyboard grids.
1914
+ - Mobile controls share input mechanics but not one universal visual skin.
1915
+ - Themes change geometry, composition, typography roles, icons, motion, materials, sound, and density—not only colors.
1916
+ - Ordinary rounded cards, pill buttons, generic dark modals, and dashboard grids are fallback failures, not defaults.
1917
+
1918
+ ## Preview states ship with the UI
1919
+
1920
+ Every game ships `src/preview.tsx`: a static default frame (default export, used by the website card) plus a `states` named export (`GamePreviewStates` from `@jgengine/react/preview`) keying named UI states — `stage_1`, `game_over`, `boss_intro` — to components. Build state entries from the game's **real UI components** with fixture snapshots (canned props/state), not redrawn lookalikes; that turns every key into a capturable render test. Capture any state instantly with `bun run shoot <game> --preview <stateKey>` — no sim, no three.js, no hang risk — and use it as the screenshot-critique loop for HUD/menu/overlay work before any full-shell `--mode ui`/`play` glance.
1921
+
1922
+ ## Rejection test
1923
+
1924
+ Reject and revise the UI when it could be mistaken for a SaaS dashboard, landing page, admin panel, documentation page, or generic emulator overlay.
1925
+
1926
+ # JGengine UI — game presentation reference
1927
+
1928
+ This reference defines the required visual and interaction quality for JGengine games. The main `jgengine` skill owns engine architecture, hooks, input commands, and routing. This document owns what the interface looks like, how it is composed, how it responds, and how it is verified.
1929
+
1930
+ ## The rule
1931
+
1932
+ A game must visually own its viewport. It must not resemble a dashboard, landing page, documentation page, or ordinary responsive web app.
1933
+
1934
+ HTML and React are valid implementation tools. Website visual grammar is not the default.
1935
+
1936
+ ## 1. Start with a concise UI art direction
1937
+
1938
+ Before implementing screens, write this short block:
1939
+
1940
+ ```md
1941
+ UI ART DIRECTION
1942
+
1943
+ Player fantasy:
1944
+ Emotional tone:
1945
+ Shape language:
1946
+ Material language:
1947
+ Typography roles: display / body / numerical / labels
1948
+ Motion language:
1949
+ Icon language:
1950
+ Sound language:
1951
+ Information hierarchy:
1952
+ Forbidden patterns:
1953
+ ```
1954
+
1955
+ Keep it practical. It should directly influence layout, silhouettes, controls, timing, and materials.
1956
+
1957
+ Example forbidden patterns:
1958
+
1959
+ - generic rounded dashboard cards
1960
+ - pill buttons
1961
+ - long centered paragraphs during play
1962
+ - ordinary two-column form layouts
1963
+ - persistent keyboard-instruction grids
1964
+ - multiple equally weighted bordered panels
1965
+ - generic translucent mobile circles
1966
+ - large website-style modals
1967
+ - document-flow wrapping used as HUD layout
1968
+
1969
+ A theme is not complete when only colors and fonts change. It must also affect composition, geometry, spacing rhythm, borders, icons, animation, sound, information density, terminology, button construction, and touch controls.
1970
+
1971
+ ## 2. Screen inventory and hierarchy
1972
+
1973
+ Identify the screens the game actually needs:
1974
+
1975
+ - boot/loading
1976
+ - title or attract screen
1977
+ - mode selection
1978
+ - onboarding/tutorial
1979
+ - gameplay HUD
1980
+ - pause
1981
+ - settings
1982
+ - map/inventory/dialogue where relevant
1983
+ - victory/results
1984
+ - failure/retry
1985
+
1986
+ For each screen, define:
1987
+
1988
+ - the player’s primary question
1989
+ - the primary action
1990
+ - the most important information
1991
+ - what can be hidden
1992
+ - what belongs in-world instead of in the HUD
1993
+
1994
+ ### HUD tiers
1995
+
1996
+ **Tier 1 — immediate action and survival**
1997
+ Health, timer, current target, ammo, danger, capture state.
1998
+
1999
+ **Tier 2 — short-term decisions**
2000
+ Objective progress, route progress, cooldowns, combo, pursuit distance.
2001
+
2002
+ **Tier 3 — reference information**
2003
+ Full map, inventory, controls, schedule, mission details, lore.
2004
+
2005
+ Tier 1 is immediately readable. Tier 2 is quieter. Tier 3 is usually hidden until requested.
2006
+
2007
+ Do not style every datum as an equally important bordered box.
2008
+
2009
+ ## 3. Full-viewport game composition
2010
+
2011
+ The active game should behave like an application mode:
2012
+
2013
+ - own the full viewport
2014
+ - avoid document scrolling
2015
+ - avoid site navigation and marketing chrome during play
2016
+ - respect safe-area insets
2017
+ - keep exit/settings/fullscreen controls minimal
2018
+ - separate world, HUD, controls, screens, and system overlays
2019
+
2020
+ Recommended layer contract:
2021
+
2022
+ ```tsx
2023
+ <GamePlayer>
2024
+ <WorldLayer />
2025
+ <HudLayer />
2026
+ <ControlLayer />
2027
+ <ScreenLayer />
2028
+ <SystemLayer />
2029
+ </GamePlayer>
2030
+ ```
2031
+
2032
+ - `WorldLayer`: game renderer
2033
+ - `HudLayer`: non-blocking gameplay information
2034
+ - `ControlLayer`: touch/input surfaces
2035
+ - `ScreenLayer`: title, pause, settings, tutorial, victory, failure, transitions
2036
+ - `SystemLayer`: exit, fullscreen, engine settings, devtools
2037
+
2038
+ Do not place unrelated interface pieces into one ordinary DOM flow.
2039
+
2040
+ ## 4. Explicit game layout modes
2041
+
2042
+ Do not rely on generic responsive wrapping. Compose explicit modes such as:
2043
+
2044
+ - desktop-wide
2045
+ - desktop-compact
2046
+ - mobile-landscape
2047
+ - mobile-portrait
2048
+
2049
+ A mobile layout is not a shrunken desktop HUD.
2050
+
2051
+ On mobile:
2052
+
2053
+ - reserve thumb-control zones
2054
+ - keep critical HUD out of those zones
2055
+ - hide keyboard legends
2056
+ - reduce persistent information
2057
+ - move Tier 3 information behind contextual panels
2058
+ - respect browser and device safe areas
2059
+ - support portrait only when intentionally designed
2060
+ - otherwise show a polished rotate-device state
2061
+
2062
+ All viewport anchoring should live in the game’s top-level UI composition file. Child components own their internal layout, not their screen position.
2063
+
2064
+ ### Design-resolution fit (`platforms` + `hudFit`)
2065
+
2066
+ Design-resolution fit is on by default for every game: each `HudCanvas` auto-scales from `hudFit.designSize` (default 1600×900) down to the live viewport, clamped by `hudFit.minScale`/`maxScale` (default 0.4–1), so the authored layout shrinks instead of overflowing a phone. No declaration needed. `hudFit.mobile` overrides the fit on compact displays only — tune the phone presentation there instead of hand-rolling media queries. The player's Graphics → UI scale setting multiplies the computed scale on every platform. Declaring `platforms: ["web"]` (without `"mobile"`) opts a desktop-only game out; its compact displays keep the legacy fixed 0.85 zoom.
2067
+
2068
+ **Overflow is an error, not a style note.** `HudCanvas` measures every `HudPanel` against the viewport at runtime; offenders land in a `data-hud-overflow` attribute (and a console warning), and `bun run shoot <game> --device mobile` (or `both`) exits non-zero naming the escaping panels. A game is not mobile-done while shoot reports HUD OVERFLOW.
2069
+
2070
+ ## 5. Change the visual grammar
2071
+
2072
+ Avoid making every element the same rounded translucent rectangle.
2073
+
2074
+ Use genre-appropriate structures:
2075
+
2076
+ - clipped corners
2077
+ - irregular silhouettes
2078
+ - image-backed frames
2079
+ - mechanical plates
2080
+ - radial interfaces
2081
+ - ribbons and tabs
2082
+ - gauges and meters
2083
+ - emblems and decorative corners
2084
+ - notches and edge anchors
2085
+ - diegetic objects
2086
+ - world-space prompts
2087
+ - asymmetrical compositions
2088
+ - masks, textures, layered borders, and strong focal elements
2089
+
2090
+ Practical rule: no more than roughly 20% of a normal gameplay screen should resemble an ordinary web card or modal.
2091
+
2092
+ Every persistent panel must justify why it exists, remains visible, has that shape, and occupies that position.
2093
+
2094
+ ## 6. Game UI primitives
2095
+
2096
+ Prefer small headless or lightly styled primitives over a giant universal design system. Useful concepts include:
2097
+
2098
+ - `HudAnchor`
2099
+ - `StatReadout`
2100
+ - `Meter`
2101
+ - `ObjectiveTracker`
2102
+ - `ActionPrompt`
2103
+ - `Reticle`
2104
+ - `MinimapFrame`
2105
+ - `DialoguePlate`
2106
+ - `Countdown`
2107
+ - `BossBar`
2108
+ - `ItemPickup`
2109
+ - `DamageIndicator`
2110
+ - `PauseScreen`
2111
+ - `ResultsScreen`
2112
+ - `VirtualControlZone`
2113
+ - `ScreenTransition`
2114
+
2115
+ A primitive should expose game-oriented choices such as shape, material, urgency, placement, hierarchy, entry motion, icon treatment, compactness, and diegetic-versus-overlay presentation.
2116
+
2117
+ Do not create a primitive whose only value is wrapping a `div` with border radius.
2118
+
2119
+ ## 7. Complete interaction states
2120
+
2121
+ Every interactive element needs intentional states:
2122
+
2123
+ - rest
2124
+ - hover where applicable
2125
+ - keyboard/controller focus
2126
+ - pressed
2127
+ - selected
2128
+ - disabled
2129
+ - success
2130
+ - failure
2131
+ - warning
2132
+
2133
+ Do not communicate all states with background-color changes alone. Use appropriate combinations of:
2134
+
2135
+ - scale compression
2136
+ - position shift
2137
+ - edge or glow response
2138
+ - mask movement
2139
+ - icon movement
2140
+ - text response
2141
+ - brief particles
2142
+ - sound
2143
+ - haptics when supported
2144
+ - controlled shake only when appropriate
2145
+
2146
+ Focus must look authored while remaining accessible. Menus should support keyboard/controller-style focus navigation when practical.
2147
+
2148
+ ## Settings menu
2149
+
2150
+ **Settings menu (themed, four layouts, no forced chrome).** The engine builds the whole menu for free — Sound (master + per-bus volume), Graphics (quality/dpr + shadows), Gameplay (FOV slider, default 40–120), Controls (per-action key rebinding, inline click-to-rebind, persisted) — from the game's `audio.buses` and `input` map. What it does **not** do is bolt a fixed gear onto every game: **there is no auto trigger.** You place the entry yourself so it lives *inline with your game's own UI*, never a stray corner overlay. Drop `<SettingsTrigger className=…>` (from `@jgengine/react`) anywhere in your HUD or menu — headless button, `className` for skin/placement, optional `children` to replace the default gear glyph, renders nothing when there's nothing to show. Or call `useSettings().open()` from your own control. Tune the menu via `defineGame({ settings })` (`GameSettingsConfig` from `@jgengine/core/settings/settingsModel`):
2151
+
2152
+ - `variant: "panel" | "sheet" | "sidebar" | "fullscreen"` — the layout + skin (default `panel`; `sheet` is the mobile bottom-sheet). All four are fixed-size (no shrink-to-content jitter) and read the game's `--jg-*` theme tokens, falling back to a neutral dark skin.
2153
+ - `actions: SettingsActionDef[]` — game-state actions (Restart, Quit to menu, …). They become the **first "Game" tab, shown before anything else** — the home for buttons that used to float over the HUD. Each: `{ id, label, kind?: "default"|"danger", description?, run(ctx) }`; the menu closes right after `run`.
2154
+ - `hideBindings: string[]` — input actions to drop from the rebindable Controls list. A game-state key like `restart` belongs in `actions`, not the rebind grid — hide it here so it stops showing up as a "rebindable" control.
2155
+ - `surface: "quick"` — additionally mount compact on-screen volume/graphics buttons. Omit for none. `settings: false` — off entirely.
2156
+ - `extra: GameSettingDef[]` — append rows to any category, built-in *or a brand-new one* named by `category` (any string). Each row: `{ id, label, category, kind: "slider"|"toggle"|"select", default, min?, max?, step?, options?, onChange?(value, ctx) }`.
2157
+ - `categories: SettingCategoryDef[]` — declare custom category tabs, or relabel/reorder built-ins (`{ id, label, order? }`).
2158
+ - `hide: SettingCategory[]` — drop built-in categories.
2159
+
2160
+ **Game-state controls go in `actions`, never a floating button.** Restart/quit/new-game buttons stapled to the bottom of the HUD are the anti-pattern — declare them as `actions` (first Game tab) and place a `<SettingsTrigger>` inline. A contextual button on a win/lose *results* card is fine; a persistent game-state button pinned over live play is not.
2161
+
2162
+ **Present it any way you want.** `useSettings()` (`@jgengine/react`) returns the live controller — `{ categories, actions, variant, surface, isOpen, open, close, setOpen }` — so a game can drive its own pause-menu button, or render `categories`/`actions` (rows carry `value`/`set`/bounds, keybinds carry `rebind`/`reset`) entirely inside its own HUD. `useHasSettings()` gates a custom entry; `useSetting(id, fallback)` reads/writes one value. Set a slider's `min`/`max` explicitly — an omitted range collapses the thumb to 0/1.
2163
+
2164
+ ## 8. Motion and game feel
2165
+
2166
+ Add purposeful motion for:
2167
+
2168
+ - screen entry and exit
2169
+ - confirm and cancel
2170
+ - warnings
2171
+ - score increases
2172
+ - objective updates
2173
+ - damage
2174
+ - victory and failure
2175
+ - countdowns
2176
+ - pause
2177
+ - item pickup
2178
+
2179
+ Motion should be brief, readable, interruptible when necessary, coordinated, consistent with the game’s art direction, and respectful of reduced-motion settings.
2180
+
2181
+ Do not animate everything constantly. Motion communicates hierarchy, cause and effect, urgency, and state changes.
2182
+
2183
+ ## 9. Mobile controls are genre-authored
2184
+
2185
+ Shared input mechanics may remain shared. Their visual treatment and arrangement must match the game.
2186
+
2187
+ Examples:
2188
+
2189
+ **Driving**
2190
+ Steering region or wheel, accelerator, brake, handbrake, optional camera/map control.
2191
+
2192
+ **Stealth**
2193
+ Movement zone, sneak/crouch hold, contextual interaction, temporary map/schedule control.
2194
+
2195
+ **Shooter**
2196
+ Movement zone, aim region, fire/action cluster, weapon or ability controls.
2197
+
2198
+ **Puzzle/arcade**
2199
+ Direct drag, tap, swipe, paddle region, or discrete directions. Do not add a joystick without a gameplay reason.
2200
+
2201
+ Requirements:
2202
+
2203
+ - never cover critical HUD information
2204
+ - use the game’s shape and material language
2205
+ - fade training labels after learning
2206
+ - consider thumb reach
2207
+ - preserve accessible target sizes
2208
+ - visually respond to activation
2209
+ - support optional scaling where appropriate
2210
+
2211
+ Do not use one generic translucent controller across all games.
2212
+
2213
+ ## 10. Progressive instruction
2214
+
2215
+ Do not leave large control grids visible during gameplay.
2216
+
2217
+ Prefer:
2218
+
2219
+ - contextual prompts
2220
+ - brief onboarding
2221
+ - first-use hints
2222
+ - a controls screen
2223
+ - pause-menu reference
2224
+ - icons attached to actions
2225
+ - progressive disclosure
2226
+
2227
+ Desktop keyboard legends must not appear on touch devices. Prompts should appear near the relevant action, object, or HUD region and clear when no longer useful.
2228
+
2229
+ ## 11. Reference directions for flagship games
2230
+
2231
+ ### Clockwork Heist
2232
+
2233
+ Use gentleman-thief mechanical field-kit language: watch geometry, midnight enamel, aged brass, ivory paper, engraved labels, mechanical shutters, and an authored schedule timeline. The timer should feel like a clock. The schedule should be an on-demand pocket-watch or dossier panel. Touch controls should use brass/enamel construction. Restart belongs in pause, not permanently over the world.
2234
+
2235
+ ### Canyon Chase
2236
+
2237
+ Use desert pursuit language: battered dashboard, analog instruments, radio display, route strip, warning lamps, and road-sign typography. Target gap should read as a pursuit gauge. Border progress should resemble an odometer, route strip, or mile marker. Driving controls should feel like steering, throttle, brake, and handbrake—not generic circles.
2238
+
2239
+ ### Brick Breaker
2240
+
2241
+ Use arcade-cabinet language: bezel framing, CRT/vector treatment, arcade numerical readouts, attract mode, launch feedback, and brief level overlays that clear before play. Pause and results should feel like cabinet states, not web dialogs.
2242
+
2243
+ These games must remain structurally distinct, not recolors of one component set.
2244
+
2245
+ ## 12. Accessibility and performance
2246
+
2247
+ Maintain:
2248
+
2249
+ - sufficient contrast
2250
+ - readable text size
2251
+ - keyboard navigation
2252
+ - controller navigation where available
2253
+ - reduced-motion support
2254
+ - visible authored focus
2255
+ - touch target sizing
2256
+ - semantic labels where practical
2257
+ - responsive scaling
2258
+ - reasonable DOM and animation performance
2259
+
2260
+ Use blur, masks, textures, filters, and full-screen effects carefully, especially on mobile.
2261
+
2262
+ ## 13. Screenshot verification is mandatory
2263
+
2264
+ Capture and inspect meaningful states:
2265
+
2266
+ - desktop title screen
2267
+ - desktop gameplay
2268
+ - mobile landscape
2269
+ - mobile portrait where supported
2270
+ - pause
2271
+ - victory or failure
2272
+ - an interaction prompt
2273
+ - touch controls in active use
2274
+
2275
+ Inspect for:
2276
+
2277
+ - overlap and clipping
2278
+ - weak contrast
2279
+ - unreadable scale
2280
+ - excessive cards
2281
+ - website-like composition
2282
+ - broken safe areas
2283
+ - conflicting hierarchy
2284
+ - browser chrome interference
2285
+ - poor thumb reach
2286
+ - keyboard instructions on touch devices
2287
+ - inconsistent art direction
2288
+
2289
+ Revise after inspection. Typechecking is not visual proof.
2290
+
2291
+ ## 14. Rejection criteria
2292
+
2293
+ Require revision when any of these are true:
2294
+
2295
+ - normal site navigation remains visible during active gameplay
2296
+ - the player must scroll the page to use the game
2297
+ - the game is presented inside a normal content card
2298
+ - essential HUD is covered by touch controls
2299
+ - controls overlap each other
2300
+ - keyboard instructions appear on touch devices
2301
+ - large instruction panels remain visible during gameplay
2302
+ - more than three ordinary card-like panels are persistently visible
2303
+ - the main action looks like a standard website button
2304
+ - pause resembles a generic website modal
2305
+ - victory/failure is only text plus restart
2306
+ - restart is permanently visible without a gameplay reason
2307
+ - menu elements lack pressed, focused, selected, or disabled states
2308
+ - UI changes have no transition or feedback
2309
+ - generic virtual controls are used without genre adaptation
2310
+ - the theme only changes colors and fonts
2311
+ - unrelated UI elements have equal visual weight
2312
+ - mobile portrait technically fits but is not intentionally composed
2313
+ - important UI sits beneath safe areas
2314
+ - ordinary document flow determines the HUD layout
2315
+ - generic default styling is used because no art direction was written
2316
+
2317
+ ## 15. Compact implementation API appendix
2318
+
2319
+ Use the main `jgengine` skill for the authoritative engine API routing. The React package exposes `GameProvider`, hooks, and headless primitives from `@jgengine/react` and its documented subpaths. Common hooks include player/game state, entities, stats, inventory, quests, prompts, clocks, markers, fog, and engine stores. The shell provides `GamePlayerShell`, input integration, devtools, and `GameUiPreview`.
2320
+
2321
+ Use these APIs to bind state; do not let API wiring dictate visual composition. Keybind labels should derive from the game’s binding table, and UI actions should dispatch through game commands rather than existing only as click handlers.
2322
+
2323
+ Headless components are not finished design. They are behavior and accessibility seams that the game must art-direct.
2324
+
2325
+ ## Definition of done
2326
+
2327
+ UI work is complete only when:
2328
+
2329
+ - the game owns the viewport
2330
+ - site chrome is absent during active play
2331
+ - no document scrolling is required
2332
+ - HUD and control layers have clear responsibilities
2333
+ - mobile controls do not cover critical content
2334
+ - touch controls match the genre and game identity
2335
+ - title, pause, and results screens feel authored
2336
+ - interaction states and transitions are present
2337
+ - screenshots have been reviewed and revised
2338
+ - the implementation remains accessible and performant
2339
+ - future generated UI is explicitly prevented from falling back to generic website-card styling
2340
+
2341
+ ---
2342
+ name: jgengine-world
2343
+ description: World API: movement, cameras, physics, maps, sensors, spawn placement.
2344
+ ---
2345
+
2346
+ # jgengine-world
2347
+
2348
+ ## Movement, pose, input
2349
+
2350
+ ```ts
2351
+ ctx.player.movement.getPose(id) / setPose(id, "crouch") // validates catalog movement.poses
2352
+ ctx.player.movement.getAim(id) / setAim(id, "ads") // ADS = aim state + zoom modifier, not a pose
2353
+ ```
2354
+
2355
+ Poses (`standing/crouch/prone/running`) change the collision capsule (`POSE_HITBOX`); aim pairs with a `player.stats` zoom modifier on `"reticle"`. Game code reads action names only (`isDown("aim")`, `wasPressed("interact")`) — hold vs toggle is resolved by the binding config, never by raw key branches.
2356
+
2357
+ ### `ctx.input` — polling the raw controls
2358
+
2359
+ `ctx.input` (`@jgengine/core/runtime/inputSnapshot`) is a per-frame held-action snapshot for `onTick` to poll, distinct from the command-dispatch path (bound actions still run commands the normal way): `publish(held: readonly string[])` (the shell calls this once per frame before `onTick`), `isDown(action)`, `held()` for the full list. Publishing never bumps `ctx.version()` — it's a poll surface, not reactive state.
2360
+
2361
+ **`repeatMs`** — bind an action as `{ hold: [...], repeatMs: 150 }` (`input/actionBindings`) and the shell fires its command on the down edge, then again every `repeatMs` while held, resetting on release (hotbar-style repeat-fire without a per-game timer).
2362
+
2363
+ **Aim on generic commands** — every command resolved from a bound action now runs with `{ yaw, pitch, aim: { yaw, pitch } }` in its payload, so a handler can read the camera-relative aim without going through `pointer.worldHit()`.
2364
+
2365
+ ### Controller kinematics — movement config, physics tuning, respawn
2366
+
2367
+ `PlayableGame.movement` (`PlayerMovementConfig`) tunes the shell's built-in walk controller (never touch `PhysicsWorld` for ordinary player movement):
2368
+
2369
+ ```ts
2370
+ movement: {
2371
+ mode?: "free" | "axis" | "grid"; // "free" (default) camera-relative; "axis" locks travel to one world axis; "grid" snaps each committed position to cell centers
2372
+ axis?: "x" | "z"; // world axis for mode "axis". Default "x"
2373
+ cellSize?: number; // cell size for mode "grid". Default 1
2374
+ collideObjects?: boolean; // collide the walking player against placed scene objects (unit-box AABBs) even without collision.voxel
2375
+ beforeCommit?: (frame: MovementCommitFrame) => readonly [number, number, number] | undefined | void; // intercepts each frame's resolved position before the pose commits; return a replacement to constrain/redirect the step
2376
+ }
2377
+ ```
2378
+
2379
+ `nav/navConstrain`'s `constrainToNavGrid(grid, { y? })` is a standalone walkable-pass-through + wall-sliding helper over a `nav/navGrid`; its `(proposed, entity)` shape doesn't match `beforeCommit`'s `(frame) => [x,y,z]` signature directly, so wire it in with a small adapter closure rather than passing it straight through.
2380
+
2381
+ `defineGame({ physics: { gravity, jumpVelocity } })` drives the kinematics controller directly: `gravity` (signed, e.g. `-24`) and `jumpVelocity` override the built-in tuning; omit either to keep the defaults. This is the one global exception to "never player tuning in `defineGame`" — it configures the shared controller, not a catalog entry. It is still **distinct** from `physics/physicsWorld`'s standalone rigid-body sim (see below) — `defineGame.physics` never touches that sim.
2382
+
2383
+ **Vertical motion intents** — `ctx.player.motion` (`@jgengine/core/runtime/motionIntents`): `impulse(vy)` adds to the vertical velocity the shell's controller is about to integrate, `setVerticalVelocity(vy)` replaces it outright, `setY(y)` wins over physics for that frame. The shell calls `takePending()` once per frame, before integrating gravity, to drain what accumulated; this is not reactive state (jump pads, launch abilities, bounce pads).
2384
+
2385
+ **`collision: { voxel: true }` — object lattice as solids.** When set, the shell's local player uses a voxel body whose `isSolid(x,y,z)` is rebuilt from `ctx.scene.object.list()` as exact `` `${x},${y},${z}` `` keys (integer cell queries). Integer-placed objects are walkable/blocking; fractional-coord objects decorate without colliding. Removing an object opens a real trapdoor under gravity — do not fake the fall with `setPose`. `visual.scale` does not shrink the collider (always a unit cell). The voxel body is created once; prefer `motion.setY` / `impulse` for vertical relocation — full XY teleport of the local voxel body is not supported. Solid cache rebuilds when object **count** changes. Recipe: `jgengine` → voxel trapdoor board.
2386
+
2387
+ **Respawn** — `ctx.scene.entity.spawnPoseOf(id)` reads the spawn pose, `resetToSpawn(id)` teleports back to it with zero velocity, `resetAllToSpawn(filter?)` does it for every entity matching an optional filter and returns the count (round resets, out-of-bounds recovery).
2388
+
2389
+ ## Touch & mobile
2390
+
2391
+ Every game is touch-playable with zero per-game input code. On a coarse-pointer device the shell derives a `TouchScheme` from the game's `input` bindings (`deriveTouchScheme`, `@jgengine/core/input/touchScheme`): a virtual joystick binds whichever of `moveForward`/`moveBack`/`moveLeft`/`moveRight` (or `turnLeft`/`turnRight`) are bound, on-screen buttons cover the remaining actions, and drag-to-look mounts automatically for `first`-person camera rigs. Touch controls feed synthetic `touch:<action>` codes into the same `ActionStateTracker` the keyboard uses — game code reads `isDown`/`wasPressed` and never branches on input source.
2392
+
2393
+ Refine the derived scheme with the `touch` field of `defineGame({...})` (`TouchControlsConfig`, all optional):
2394
+
2395
+ ```ts
2396
+ touch: {
2397
+ gestures: {
2398
+ tap: "rotateCw",
2399
+ swipeUp: "hold",
2400
+ swipeDown: "hardDrop",
2401
+ drag: { left: "shiftLeft", right: "shiftRight" },
2402
+ },
2403
+ buttons: [
2404
+ { action: "rotateCcw", label: "CCW" },
2405
+ { action: "softDrop", label: "Soft" },
2406
+ ],
2407
+ },
2408
+ ```
2409
+
2410
+ - **`gestures`** — bind `tap` / `swipeUp` / `swipeDown` / `swipeLeft` / `swipeRight` / `drag` (`{ left?, right?, up?, down?, stepPx? }`, repeats its action every `stepPx` of travel) on the play surface. An action consumed by a gesture is removed from the derived button set.
2411
+ - **`buttons`** — curate the on-screen cluster (order preserved; bare string or `{ action, label?, icon? }`); omit to auto-derive one button per remaining bound action. Buttons render a glyph, not text: `iconForAction` (`@jgengine/react/gameIcons`) resolves the action name to a `GameIconName` (`jump`, `sprint`, `rotateCw`, `hardDrop`, `swap`, `hand`, `restart`, arrows, …), the `label` becomes the `aria-label`; set `icon: "<GameIconName>"` to pick one explicitly or `icon: false` to force the text label.
2412
+ - **`hidden`** — actions to drop from the derived buttons without gesture-binding them.
2413
+ - **`movement: false`** — suppress the virtual joystick even when movement actions are bound.
2414
+ - **`look` / `lookSensitivity`** — drag-to-look on the play surface; defaults to `true` for `first`-person camera rigs, `0.005` radians/px.
2415
+ - **`touch: false`** — opt out entirely when the game's own DOM UI is already touch-native.
2416
+
2417
+ `useDisplayProfile()` (`@jgengine/react/display`) reports `{ coarsePointer, compact, portrait }` — live media-query state, SSR-safe — for adaptive HUD layout; see the mobile/touch rules in [`../jgengine-ui/reference.md`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md).
2418
+
2419
+ ## Interaction — `proximityPrompt`
2420
+
2421
+ One primitive for all float UI: `{ radius, display, invoke }` where `display` is `{ kind: "keybind", actionId }` | `{ kind: "gauge", gaugeId }` | `{ kind: "label", text }` and `invoke` is `{ command, args? }` or null (display-only). `talkable: "dialogue_id"` on an entity expands to a talk prompt. Engine picks the nearest prompt in radius (priority tie-break). Never build per-game hint resolver chains.
2422
+
2423
+ ## Pointer-driven input and navigation
2424
+ The **pointer is a service, not per-game glue**. Opt in with `camera` plus a `pointer` config in `defineGame({...})`; the shell casts the cursor into the world and dispatches commands you define — verbs stay commands, catalogs stay data.
2425
+ - **`pointer.worldHit()` (shell service).** The shell raycasts the cursor to `{ point, normal, entity, object }` (a renderer-free `PointerHit` from `@jgengine/core/input/pointer`) — entity/object are the topmost instance ids under the cursor, else `null`, with a ground-plane fallback for open terrain. Consume it renderer-free: `aimToPoint(origin, point)` builds an `Aim` for `item.use`/projectiles (ground-target skillshots, twin-stick), `groundOf(hit)` drops to `[x, z]` for routing. `pointer.worldHitCenter()` is the same raycast pinned to the viewport center instead of the live cursor — the reticle-aim query a locked/hidden-cursor rig (first-person, gamepad) needs when there is no cursor position to read.
2426
+ - **The `pointer` field of `defineGame({...})`** (all optional): `moveCommand` (left-click ground → `run(cmd, { point, entity, object })`, click-to-move), `select` (left-drag marquee + single-click box-select of entities), `orderCommand` (right-click ground → `run(cmd, { selection, point })`, issue a command to the selection), `contextMenu` (right-click an entity/object → its catalog `verbs` menu), `secondaryCommand` (right-click ground/entity/object → `run(cmd, { point, entity, object, aim })` when neither `orderCommand` nor `contextMenu` claims the click — a generic right-click verb for games with no selection/RTS model), `aim` (route the primary ability's aim to the cursor), `grabWorldItems` (left-click a `worldItem` within pickup radius → engine-owned `ctx.scene.worldItem.pickup`, no game command). Enabling `select`/`moveCommand` frees the left button for verbs; orbit moves to middle-drag.
2427
+ - **`createDragCapture({ maxPull?, grabRadius? })`** (`@jgengine/core/input/pointer`) — a renderer-agnostic slingshot/drawback state machine: `begin(origin, at)` starts a pull (rejected outside `grabRadius`), `update(at)` tracks the cursor, `release()` returns the final `DragState { origin, current, pull, magnitude, fraction }` (pull clamped to `maxPull`), `cancel()` aborts. Angry Birds-style slingshots, bow drawback, throwable wind-up — pair with `aimToPoint` to fire.
2428
+ - **Selection math** (`scene/selection`) is pure and testable: `createSelectionSet()`, `screenRect`/`selectWithinRect`/`isMarquee` over projected screen points.
2429
+ - **Context menu** (`interaction/contextMenu`): a catalog entity/object carries `verbs: contextVerb(label, command, args?)[]`; the shell builds the menu with `buildContextMenu` and dispatches the chosen command via `contextVerbInput` (verb args + `target`/`point`, so one handler can walk-then-act).
2430
+ - **Navmesh + A\*** (`nav/navGrid`): `createNavGrid({ bounds, cellSize, diagonal? })` → mark obstacles with `blockAabb`/`setWalkable`, or `populateNavGridFromEnvironment(grid, world)` to block every generated building's footprint on an `environment()` world's `structures` in one call (returns the count blocked) instead of hand-walking the district. `findPath(grid, from, to, { clearance?, smooth?, stepCost? })` returns a string-pulled `[x, z]` polyline (blocked start/goal snap to the nearest walkable cell) feeding **both click-to-move and AI routing**; `stepCost?(from, to)` multiplies the base cost of a grid step — `slopeStepCost(terrainField, weight?)` is the ready-made factory that penalizes steep terrain (routes around cliffs instead of over them). Renderer-free — AI and gameplay consume it without the shell.
2431
+ - **`pathFollow`** (`nav/pathFollow`): the lighter authored-polyline mover for tower-defense creeps that needs no navmesh — `createPathFollow({ waypoints, speed, loop? })` + pure `advancePathFollow(config, state, dt)` (crosses multiple waypoints per tick, reports `done`/`heading`/`distanceTravelled`). Feed it a navmesh route with `pathFromNav(route, elevation, offset?)` and the same follower drives click-to-move — `elevation` is either a fixed `y` or a `{ sampleHeight(x, z) }` field (any `TerrainField` qualifies), so a route across relief rides the ground instead of a flat plane.
2432
+ - **`constrainToNavGrid(grid, { y? })`** (`nav/navConstrain`) is a standalone walkable-pass-through + wall-slide helper: it passes through walkable moves, slides along walls at the navmesh boundary instead of stopping dead, and optionally remaps `y` to the grid. Its `(proposed, entity)` shape doesn't match `PlayerMovementConfig.beforeCommit`'s `(frame) => [x,y,z]` signature, so wire it in with a small adapter closure (see "Controller kinematics" above) to wall a player/AI to the same navmesh `findPath` already routes against.
2433
+ ## AI — director, threat, jobs, crowds (`ai/*`)
2434
+ Renderer-free AI over the same navmesh (`findPath`/`pathFollow`) gameplay already uses. Everything ticks on **game-time `dt`** (the `ctx.time` simClock delta), so it obeys pause and fast-forward for free. Manifests, patrol routes, job definitions, threat weights, and POIs are **game data** — the primitives own the loop, the catalog owns the content.
2435
+ - **Spawn director** (`ai/spawnDirector`) — budgets and escalates spawns for wave shooters and difficulty directors (Brotato, Bloons TD 6, Risk of Rain 2, Helldivers 2, Deep Rock Galactic). `createSpawnDirectorState(config)` then pure `advanceSpawnDirector(config, state, dt, { alive, players? })` → `{ state, spawns: SpawnRequest[] }`. Each `WaveManifest` grants a `budget` spent on affordable weighted `SpawnEntry`s (`cost`/`weight`/`minWave`), capped by `maxAlive`; `duration` auto-advances waves (or call `advanceWave` on "wave cleared"). Budget also trickles via `budgetPerSecond`, ramps a difficulty curve with `escalationPerSecond` (grows with sim-time), scales with `playerBudgetPerSecond`, and surges on `raiseAlert(state, amount)` decaying over time (bug-breach/dropship escalation). Seeded (`seed`) so ticks are deterministic. `pickSpawnPoint(points, players, { roll, bias })` biases placement toward (or away from) players. `config.spawnPoints?: NavPoint[]` lets the director pick a point itself: each `SpawnRequest` then also carries `point` (the chosen `[x, z]`, biased by `config.spawnPointBias`) and `laneId` (the point's index into `spawnPoints`) — feed multiple named lanes/portals and read `laneId` back to route the spawned entity down its lane instead of correlating positions by hand.
2436
+ - **Threat table** (`ai/threat`) — MMO/extraction aggro (Escape from Tarkov, WoW-style tanking). `createThreatTable({ decayPerSecond?, max?, forgetBelow? })`: `add(source, amount)` accumulates, `decay(dt)` bleeds off per game-second and forgets emptied sources, `highest({ current?, stickiness? })` returns the top-threat source to feed `scene/targeting` — `stickiness` (e.g. 1.1) keeps the current target until another exceeds it by that factor, so aggro doesn't jitter. `taunt(source, durationSeconds)` forces the mob onto a tank for that window (overriding `highest`) and lifts the taunter's threat to the current top so aggro stays after the window; `forcedTarget()` reports the active taunt, `decay(dt)` counts it down. `ranked()` for a threat meter.
2437
+ - **Patrol** (`scene/behaviors`) — `patrol({ waypoints, speed, loop? })` is a `BehaviorDescriptor` (a route is data) that layers a fixed beat on top of `wander`; drive it with `createPathFollow`/`advancePathFollow` (lane creeps, scav patrols in Deadlock/Tarkov). Route waypoints between guard posts with `findPath`.
2438
+ - **Job board** (`ai/jobBoard`) — colony/companion task assignment (Palworld stations, Schedule I employees, Sons of the Forest directives). `createJobBoard()`: `post(job)` a `JobDef` (`station`, `work` seconds, `priority`, `arriveRadius`, `repeat`), `claim(worker)` auto-pulls the highest-priority queued job or `assign(worker, jobId)` for a player order (steals it from its holder), `release` requeues. Per tick `advance(worker, dt, { distanceToStation })` runs the state machine `travelling → working → done` (path to `station(worker)` via `findPath`, occupy, run the loop), returning a `JobReport` on completion; `repeat` jobs re-run as a production loop and report each cycle.
2439
+ - **Crowd flow** (`ai/crowd`) — many agents routing to their own points of interest with congestion (Two Point Museum corridors, Dave the Diver seating). `computeFlowField(grid, goals, { clearance?, congestion? })` runs Dijkstra from the goals over the walkable grid → `direction(point)`/`next(point)` steer any agent toward the nearest goal (no per-agent A*). `createCrowdField(grid)` tracks per-cell occupancy (`enter`/`leave`/`count`); pass `crowd.penalty(weight)` as the field's `congestion` to reroute flow around crowded cells each tick. `selectPoi(pois, from, { roll, occupancy?, distanceBias?, distance? })` weights a POI by appeal and proximity, skips ones at `capacity`, and accepts a `distance` override (e.g. `findPath` length) to choose over the navmesh, not line-of-sight.
2440
+ - **Factions & reputation** (`faction/factions`, `faction/reputation`) — allegiance-based hostility for RPGs/MMOs/strategy (WoW-style Horde-vs-Alliance, at-war reputation grinds, guards-vs-thieves). `createFactionGraph({ factions, symmetric?, unaligned? })` models a faction-vs-faction relation matrix — each `FactionDef` lists `relations` toward others (`"hostile" | "neutral" | "friendly"`) with `towardSelf`/`towardOthers` fallbacks; `relationBetween(a, b)` resolves it (mirroring undeclared directions when `symmetric`, defaulting `unaligned` for unknowns). `createFactionRoster(graph)` maps entities to factions (`assign`/`factionOf`/`members`), then `relationBetweenEntities`/`isHostile`/`hostilesOf(observer, candidates)` answer "who is my enemy" as a data lookup — feed it into `scene/targeting`'s `classify` and `ai/threat` eligibility instead of a per-game role-string check. `createReputationLedger({ tiers?, initial?, min?, max? })` is the per-actor standing ledger: `gain`/`set`/`standing` track reputation, `tier`/`relation` map it through `DEFAULT_REPUTATION_TIERS` (the classic Hated→Exalted ladder, or your own), `standings(actor)` snapshots all. `effectiveRelation({ base, ledger, actorId, factionId })` overlays a player's earned standing on the base faction relation, so a reputation grind flips a normally-hostile faction to neutral/friendly.
2441
+ ## Map, fog of war & ping
2442
+ Minimap/world-map/fog/compass state is renderer-free core (`world/*`), the top-down terrain image bakes in the shell, and the minimap/compass/world-map are react components. Ping rides the existing party + feed — it is not a new channel.
2443
+ - **Markers** (`world/markers`): `createMarkerSet()` is a reactive keyed set of `MapMarker { id, kind, position, label?, owner?, expiresAt?, meta? }` — `add`/`remove`/`get`/`list`/`query({ kind, owner, near, radius })`/`prune(now)`/`subscribe`. `kind` is a game-owned catalog string; `DEFAULT_MARKER_KINDS` (objective/enemy/loot/location/danger/ping/player/ally) supplies colors + glyphs the react map reads (override with your own `MarkerKindStyle` palette). Objective/entity/loot markers all live here.
2444
+ - **Fog of war** (`world/fog`): `createFogField({ bounds, cellSize })` is reveal-on-event — `reveal(x, z, radius?)` (a dig/act), `revealAlong(from, to, radius?)` (a walked trail); once a cell is revealed it stays revealed. `isRevealed`/`fraction`/`cells()` (stable snapshot for rendering)/`reset`/`subscribe`.
2445
+ - **Minimap math** (`world/minimap`): pure projection + bearings — `projectToMinimap(worldPoint, { center, worldRadius, size, rotate? })` → pixel `{ x, y, inside, distance }` (north = −Z maps up), `clampToMinimapEdge` for off-map markers, `compassBearing(from, to)`/`headingToBearing(yaw)`/`bearingToCardinal`/`relativeBearing` for the compass strip.
2446
+ - **Ping** (`game/ping`): `classifyPing(hit, { roleOf, categoryOf }, options?)` turns a G1 `pointer.worldHit()` `PointerHit` into a category (hostile entity → `enemy`, tagged object → its catalog category, open ground/ally → `location`). `createPingSystem({ markers, feed, party?, ttlMs?, classify, classifyOptions? })` composes classify + broadcast: `ping(from, hit, category?)` classifies, drops a categorized marker, and pushes the `PingPayload` to the party feed under `PING_FEED_ACTION` (`"party.ping"`) — the shell's feed bridge fans it to the squad. `DEFAULT_PING_CATEGORIES` is the enemy/loot/location/danger wheel. Enable the verb with the `pointer.pingCommand` field of `defineGame({...})`: the shell binds the `ping` input action → `worldHit()` → runs your command with `{ point, entity, object, normal }`.
2447
+ - **Shell render** (`@jgengine/shell/map`): `bakeTerrainMap(field, bounds, { resolution? })` renders a `TerrainField`/`RegionField` to a top-down PNG data-URL for the map background; `MapMarkerBeacons({ markers })` renders world-space beacons (the visible side of a ping) — wire via the `WorldOverlay` field of `defineGame({...})`. See the `extraction-map` demo game.
2448
+ ## Sensors, vision & observer tools (`sensor/`)
2449
+ Pure `@jgengine/core/sensor/*` primitives for querying and surfacing world state the player can't normally see or reach through the standard occlusion/proximity rules — reveal vision, hidden-state sensors, photo-mode framing, and session replay. Shell renderers/HUD pieces live in `@jgengine/shell/vision` and `@jgengine/shell/replay`.
2450
+ | Primitive | Answers |
2451
+ |-----------|---------|
2452
+ | `createRevealQuery({ resolvePosition, resolveTags, candidates })` → `RevealQuery` | `inRadius(center, radius, tags)` — occlusion-ignoring tagged-entity radius query (Dark Sight / detective-vision reveal, #115). `inRadius` already never checks occlusion (only combat's AoE `effect()` layers a LoS filter on top of it) — this is that same query shaped for a vision readout: scoped to catalog-declared tags, sorted nearest-first |
2453
+ | `probeHiddenState(origin, sources, { range, variableId, falloff? })` / `probeHiddenStateAll(...)` → `SensorReading \| null` | A sensor verb: reads a hidden zone/entity state variable (EMF/thermometer/geiger, #116) in range, strongest reading first; `strength` falls off linearly with distance by default |
2454
+ | `projectToView(camera, point)` → `FrustumProjection` | Pure camera-frustum projection (no three.js) — `inView`, `screenX/screenY` (-1..1), `distance` |
2455
+ | `framingScore(projection, config?)` → `number` | 0..1 framing quality from screen-center placement + distance-to-ideal (photo-mode "is this subject framed", #117) |
2456
+ | `createFrustumSensor(config?)` → `FrustumSensor` | `tick(camera, targets, dt)` — per-target in-view + framing + `dwellSeconds` (resets the instant a target leaves frame); a view-frustum sensor on a held camera object (Content Warning-style monster-filming scoring) |
2457
+ | `createRecordingBuffer(options?)` → `RecordingBuffer<T>` | `append(t, data)` / `seek(t)` / `range(fromT, toT)` — a session-recording buffer for replay/photo mode/kill-cam (#120), keyed on game-time so pause/fast-forward scrub consistently |
2458
+ | `colorDistance(a, b)` / `concealmentScore(entityColors, backgroundColors)` / `createConcealmentSensor(config?)` → `ConcealmentSensor` | Camouflage/blend-in scoring — how well an entity's palette matches its surroundings (hide-and-seek, stealth camo checks) against a `threshold` |
2459
+ | `createFreezeMonitor(config?)` → `FreezeMonitor` | Detects a tracked subject moving past a tolerance speed during a "freeze" window (red-light-green-light, statue games) and reports `FreezeViolation`s |
2460
+ Shell wiring: `@jgengine/shell/vision/RevealVision` (`RevealHighlights` — depth-test-disabled 3D highlight meshes for tagged entities in radius, meant for `WorldOverlay`; `RevealScreenTint` — full-screen CSS tint for "vision mode is on", meant for `GameUI`), `@jgengine/shell/vision/HiddenStateProbeHud` (`SensorReadoutMeter` — needle-strength HUD readout), `@jgengine/shell/vision/FrustumSensorHud` (`FrustumSensorReadout` — drives the sensor off the live render camera via `useThree`/`useFrame`, portals its HUD through drei's `Html fullscreen`), `@jgengine/shell/replay/useSessionRecorder` (records an entity's pose into a `RecordingBuffer` every frame; drive an observer-cam ghost, scrubber, or kill-cam export from it). The detached spectator/photo cam itself is the `observer` camera rig (see Camera rigs above) — bind it to any entity or fixed point.
2461
+
2462
+ ## World features
2463
+
2464
+ Renderer-free world surface — query primitives, environment fields + weather + realm composition, survival meters/moodles, interactive building & terraform, the optional headless physics world, vehicles/mounts/racing, and spawn placement. Full surface: **[reference.md](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-world/reference.md)**.
2465
+
2466
+ ## Turn-based & tactics (renderer-free)
2467
+
2468
+ # jgengine domain API — World features
2469
+
2470
+ Reference module for the [`jgengine-world` API](SKILL.md) skill. Load this when you need the renderer-free world surface.
2471
+
2472
+ ## World features
2473
+
2474
+ Descriptors from `@jgengine/core/world/features` — config data the runner/world layer interprets:
2475
+
2476
+ | Feature | Use |
2477
+ |---------|-----|
2478
+ | `biomes({ map, zones, bounds? })` | Region atmosphere/rules layering; zones reference biome ids |
2479
+ | `voxel({ seed, generate?, streaming? })` | Block worlds |
2480
+ | `plots(config)` | Shared city + instanced interiors |
2481
+ | `tilemap({ map })` | 2D/2.5D levels |
2482
+ | `flat()` | Plain arena |
2483
+ | `environment({ terrain, sky, weather, vegetation, water, structures, pads })` | Composable outdoor scene — terrain + sky/time-of-day + rain/snow + grass + ocean + buildings + ground pads. Each field takes the matching descriptor: `terrain()`, `sky()`, `rain()`/`snow()`, `grass()`, `ocean()`, `building()`, `pad()`. `building()` and `ocean()` take `position: [x, z]` to site a cluster/water body away from the origin (several settlements, an offset lake); building clusters ground-snap to the terrain field per building; each `pad()` (a flat platform/paved patch — `{ center, size: [w,d] | { radius }, height?, color?, rotationY? }`) implicitly flattens the terrain beneath it via a `TerrainFlattenMask`, so a building pad or spawn circle never fights the noise field underneath |
2484
+
2485
+ `biomes`/`voxel`/`plots`/`tilemap` share a `WorldGridConfig` (`cells?: WorldGridCell[]`, `cellSize?`, `baseHeight?`, `defaultColor?`) — a `WorldGridCell` is `{ x, z, height?, color? }`, one extruded box per cell. `resolveGridInstances(config)` (`@jgengine/core/world/gridInstances`) is the pure cell→instance math (position, scale, color per cell); the shell renders the result as a single `THREE.InstancedMesh` **automatically whenever `PlayableGame.environment` is unset and `game.world` is one of these four grid kinds** — no manual render wiring for a cell-based world, same auto-render convention as `environment()` worlds.
2486
+
2487
+ `terrain()`'s `material` (a named palette — `"grass" | "sand" | "snow" | "rock" | "ash"`, resolved via `resolveTerrainPalette`/`TERRAIN_MATERIAL_PALETTES` in `world/terrain`) sets the default low/high/waterline colors; `colors: { low?, high?, waterline? }` overrides any of them field-by-field, and `segments` tunes the render mesh's subdivision. `flatten: TerrainFlattenMask[]` (`{ center, radius, height?, falloff? }`) carves explicit flat circles into the noise field independent of pads — building foundations, spawn circles, roads — blending back to the noise height over `falloff` (default `radius * 0.5`). `sky({ preset?, timeOfDay?, horizonColor?, zenithColor?, sunIntensity?, ambientIntensity?, fog? })` — `preset: "day" | "dusk" | "night"` (default `"day"`) is the static look; `timeOfDay: true` instead drives sun position, sky colors, and fog from the world clock's `calendar().dayFraction` every frame (`@jgengine/shell`'s `TimeOfDayDaylight` mounts this automatically for an `environment()` world with `sky` set — no per-game render wiring). **Gotcha:** `sunIntensity` / `ambientIntensity` are honored only on the **day** keyframe (`daylightCycle` builds dusk/night/dawn from fixed constants). Raising intensities under `preset: "dusk"` or `"night"` does nothing — use `preset: "day"` plus warm `horizonColor`/`zenithColor` when the first screenshot must be bright and readable (see `jgengine`'s first-shot art recipe).
2488
+
2489
+ `parentSpace` positions are local to that space — convert at seams only.
2490
+
2491
+ ### Query primitives (renderer-free, for gameplay)
2492
+
2493
+ Pure `@jgengine/core` functions so gameplay reads the same world the shell renders — no three.js needed:
2494
+
2495
+ | Primitive | Answers |
2496
+ |-----------|---------|
2497
+ | `resolveTerrainField(terrain(...))` / `noiseField(cfg)` → `TerrainField` | `sampleHeight(x,z)`, `sampleNormal(x,z)`, `waterLevel` — ground-snap, collision, camera. `resolveGroundStep` slope-limits movement |
2498
+ | `snapToGround(field, position, offset?)` → `[x,y,z]` | Replaces a position's `y` with the field's sampled ground height (+ `offset`) at its `x`/`z` — the pure version of a spawn/placement ground-snap |
2499
+ | `snapEntityToGround(entities, id, field, offset?)` → `boolean` | Ground-snaps an already-spawned entity in place via `entities.setPose` — false when `id` is unknown; the imperative sibling of `snapToGround` for "drop this entity onto the terrain right now" (mount dismount, teleport, respawn) |
2500
+ | `windField(cfg)` → `WindField` | `at(t)`, `atPoint(x,z,t)`, `strengthAt` — one wind source for weather sway, grass, sailing, fire spread |
2501
+ | `waterSurface(cfg)` / `waterSurfaceFromDescriptor(ocean(...))` → `WaterSurface` | `height(x,z,t)`, `normal`, `displace` — buoyancy, floating, shoreline (CPU Gerstner matching the ocean shader) |
2502
+ | `scatter(cfg)` → `ScatterPoint[]` | Seeded, overlap-aware point distribution — vegetation, props, lots, spawn points (`minDistance`, `avoid` rects) |
2503
+ | `createRegionField({ regions })` → `RegionField` | `sampleRegion(x,z)` blends content-agnostic biomes by nearest selector — height + `tint`/`water`/`fog`/`speedMultiplier` + opaque `data`. Extends `TerrainField`, so it ground-snaps too |
2504
+ | `scatterItems(field, area, layersFor)` → `ScatterInstance[]` | Region-driven content scatter — density per region, grounded, above-water/slope-aware. `pickWeighted` for weighted rolls. (vs `scatter`'s pure geometric points) |
2505
+ | `buildingIndex(district)` → `BuildingIndex` | `at`/`within`/`nearest`/`isInside`/`blockers` over a generated district — placement avoidance, pathfinding |
2506
+
2507
+ **Voxel field (`world/voxelField`).** `createVoxelField<T extends string>({ chunkSize? })` (default 16) is a logical block lattice for voxel games and instanced renderers — distinct from the `voxel()` `WorldFeature` descriptor above (that's the runner-level world kind; this is the block data structure a voxel game's gameplay reads and writes). `set`/`remove`/`get`/`has`/`fill`/`clear`/`count`/`cells`/`bounds` are the block CRUD (`set` returns `false` only when the identical type is already there — a no-op write). `neighbors(x, y, z)` returns the 6-adjacent occupied cells; `exposedFaces(x, y, z)` returns the `VoxelFace`s (`"px"|"nx"|"py"|"ny"|"pz"|"nz"`) not touching another voxel — feed that straight into greedy-meshing/face-culling. `raycast(origin, direction, maxDistance)` runs a 3D DDA and returns `{ x, y, z, type, face, adjacent, distance }`, where `adjacent` is the empty cell just in front of the hit — the placement target for block-place tools. Renderers dirty-track via `chunkOf(x, y, z)` + `chunkVersion(chunk)` and `subscribe(listener)`, so an instanced mesh only rebuilds the chunks that changed. For a non-`environment()` voxel world, assert on `field.summary()` (`{ blocks, types, bounds }`) the same way an `environment()` world asserts on `summarizeEnvironment` (see `jgengine-verify`).
2508
+
2509
+ **Destructible terrain (`world/carve`).** Two runtime-editable primitives for dig/carve worlds. `VoxelVolume` is a dense grid of material ids (0 = empty) — `carve({ center, radius, toolStrength })` clears a sphere of solid cells the tool is strong enough to break and returns the count removed (feed a loot roll), `deposit({ center, radius, material })` fills one (Deep Rock tunnels, Astroneer terrain add); `solidAtWorld` reads it back for collision. `CarvableField` (via `carvableTerrain(base)`) wraps any `TerrainField` and writes craters/mounds into its height — `carve({ x, z, radius, depth })`/`deposit({ x, z, radius, height })` — so ground-snap, collision, and the shell mesh all read the deformed surface (Helldivers 2 explosion craters). Cell strengths come from a `VoxelMaterial` table (DATA). Renders through `@jgengine/shell/terrain/CarvedTerrain`.
2510
+
2511
+ Renderers for these descriptors live in `@jgengine/shell` (`shell/terrain`, `shell/water`, `shell/weather`, `shell/structures`).
2512
+
2513
+ ### Environment fields, weather hooks & realm composition
2514
+ Renderer-free survival/environment primitives that extend the world query layer — meters, spawn gating, and damage-in-sunlight read the same world the shell renders, all ticking on game-time `dt`.
2515
+ - **Environment field** (`world/envField`): `createEnvironmentField({ dayLength, baseTemperature, nightDrop, altitudeLapse, terrain, rain, occluders, heatSources, ambientFloor, temperatureAt })` → `EnvironmentField`. Sample **temperature**, **wetness**, **lightExposure** (direct sun/sky), and **ambientLight** (spawn gating) at any `(x, z, time)` — `sample(x, z, time, y?)` returns all four plus `sheltered`. Occluders (roofs/canopy) shade sun and shelter from rain; heat sources (campfires) warm nearby positions; `sunElevation(time)` drives the day cycle. Sun damages a vampire, cold forces campfires, low ambient light spawns mobs — the field answers "am I in sun vs. shade / cold vs. warm / dark vs. lit". Pure and instantaneous; stateful build-up belongs to a decay meter reading the field.
2516
+ - **Weather → gameplay** (`world/weather`): `resolveWeather(state, table)` turns a `WeatherState { kind, intensity }` into concrete `ResolvedWeather` (`grip`, `visibility`, `structureDamage`, `chill`, `ignition`, `spread`) via a game-owned `WeatherModifierTable` — multipliers interpolate from neutral by intensity, rate effects scale linearly. Read `grip`/`visibility` in movement and AI, `structureDamage` on a building tick.
2517
+ - **Fire spread** (`world/weather`): `createFireGrid({ cols, rows, cellSize, origin, fuelAt, spreadRate, burnRate, wind, windBias })` → `FireGrid` is a **coarse cellular** propagation (not a fluid solver): `ignite(x, z)` / `igniteCell(col, row)`, then `step(dt, { spread, wetnessAt })` transfers heat to neighbours biased by wind, consumes fuel (`unburnt → burning → burnt`), and honours firebreaks (zero-fuel cells) and rain/wetness suppression. `resolveWeather(...).spread` feeds the step; `@jgengine/shell/weather` `FireSpreadLayer` renders the burning/scorched cells.
2518
+ - **Realm composition** (`world/realm`): `composeRealm(base, cards)` assembles a played instance at runtime from a deck of modifier **cards** (Nightingale realm cards) — a `major` card is the biome base, `minor` cards layer environment param overrides, a `WeatherState`, and spawn-table edits (`set`/`add`/`scale`/`remove`). The result recomposes both the environment (into a sampleable field via `composed.environmentField(extra?)`) and the `spawnTable`, and depends on the weather hooks above to turn its `weather` into gameplay modifiers.
2519
+ ### Survival meters, moodles & multi-region health
2520
+ The `survival/` domain — decay-over-time meters and per-part health, both feeding one stacking **moodle** status display distinct from numeric bars.
2521
+ - **Decay meters** (`survival/decayMeter`): `createDecayMeterSet([{ id, max, min?, start?, rate, thresholds }])` → `DecayMeterSet`. Each named meter (hunger, thirst, oxygen, sanity, warmth, stamina) drains/recovers on `tick(dt)` at `rate`, refills from consumables/actions via `refill(id, amount)`, and raises threshold moodles (`below`/`above`). `setRateModifier(id, mult)` lets the environment drive them — read an env field, then speed warmth loss when cold or oxygen loss in a toxic biome.
2522
+ - **Moodles** (`survival/moodle`): the shared status stack, distinct from raw bars. `stackMoodles(...groups)` folds meter, ailment, and buff `Moodle[]` into one worst-first display (same-id stacks add, worst severity wins). `createMoodleStack()` holds timed buffs (`add({ id, label, duration })` — Valheim's concurrent food buffs) and expires them on `tick(dt)`.
2523
+ - **Multi-region health** (`survival/regionHealth`): `createMultiRegionHealth({ regions, ailments })` → `MultiRegionHealth` gives per-part pools (head/thorax/arms/legs, Tarkov/DayZ style) — `damage(regionId, amount)` scales by `vulnerability` and kills when a `vital` part empties; a stacking **ailment queue** (`applyAilment`, `tick(dt)` drains like bleed) carries per-injury treatment (`treat(itemId)` clears wounds via bandage/tourniquet/splint). `ailmentMoodles()` shares the moodle display with the meters (#78 + #90).
2524
+ ### Interactive building & terraform (renderer-free tools)
2525
+ Turn data-only placement into the build tooling of Valheim/Enshrouded/The Sims/Fortnite/Dinkum. All pure `@jgengine/core/world`; the shell renders the ghost/tint/brush (`shell/structures/PlacementGhost`, `shell/terrain/EditableGround`, `shell/terrain/TerraformBrushCursor`) driven by `pointer.worldHit()`.
2526
+ | Primitive | Answers |
2527
+ |-----------|---------|
2528
+ | `createPlacementController({ footprint, rules, snapMode, grid })` | Owns the ghost: `hover(hit)` → `PlacementPreview` (`valid` tint wraps `validatePlacement`), `rotate()`, `setSnapMode`/`cycleSnapMode` (`"grid"`/`"free"`/`"surface"`), `commit()` → `PlacementCommit` (`rotationY` via `quarterTurnsToRotationY`). Feed it `pointer.worldHit()`. |
2529
+ | `snapToNearest(registry, placed, movingDef, cursor, { snapDistance })` | Typed connector sockets — snaps a piece's socket onto the nearest **compatible** placed socket (`socketsCompatible` = both sides `accept` the other type). `worldSockets`/`socketWorldPosition` expand a piece's sockets to world space. |
2530
+ | `solveSupport(pieces, links, { maxDistance })` → `SupportResult` | Walks the connector graph to any `grounded` piece: `supported` stays, `unsupported` collapses, `distance` (hops-to-ground) drives the white→red decay tint. `toDebrisBodies(pieces, unsupported)` → `AddBodyOptions[]` for the `PhysicsWorld` debris sink. |
2531
+ | `createWallDrawTool({ snap, closeTolerance })` | Drag wall points → auto-encloses when the path returns to the start (`isEnclosed`), `footprint()` derives the room `EnclosedFootprint`, `roof()` auto-fits a hip/gable/flat `RoofPlan`. `createSurfacePaint()` stores per-tile floor/wall surfaces. |
2532
+ | `createPlacedStructureStore()` | Save/load a built layout: `add`/`move`/`rotate`/`remove`/`select`, `snapshot()`↔`load()` round-trip (survives reload), `subscribe` for the renderer. |
2533
+ | `createEditableTerrain({ bounds, base, cellSize })` → `EditableTerrain` | A `TerrainField` you can **write back to**: `apply(edit: TerraformEdit)` raises/lowers/flattens/paints under a cursor and re-samples `sampleHeight`; `surfaceAt`, `snapshot`/`restore`, `reset`. `createTerraformBrush(field)` is the cursor tool (`raise`/`lower`/`flatten`/`paint`, radius/strength). This write-back grid is the shared terrain-edit pattern. |
2534
+ | `createPlotPermissions({ plotId, ownerId, guildId? })` + `createContributionPool(goal)` | Per-plot/guild edit authority (`canEdit`/`canView`, `grant`/`revoke` `BuildRole`, guild inheritance) for co-op building, plus a pooled-resource contribution model (`contribute` caps at the goal, reports overflow, `isComplete`, per-contributor totals). |
2535
+
2536
+ ### Physics world (optional, headless)
2537
+
2538
+ `physics/physicsWorld` `PhysicsWorld` is a standalone fixed-capacity rigid-body sim (SoA buffers, spatial-hash broadphase, sleeping) — **not** the `defineGame` `physics: { gravity, jumpVelocity }` field, which the built-in walk controller reads directly every frame (see "Controller kinematics" above; both values are real and honored, not dead config). Reach for `PhysicsWorld` when a game needs many colliding dynamic bodies (piles, debris, stress scenes): `new PhysicsWorld({ capacity, bounds, … })`, `addBody({ position, mass?, ...shape })`, then `step(dt)` per tick → `PhysicsStats`. Core owns the sim; `@jgengine/shell/world/InstancedBodies` renders its bodies. Most games never need it — the character controller covers ordinary movement. The broadphase grid (`nx*ny*nz` cells from `bounds`/`cellSize`) throws at construction if it would exceed a sane cell cap — shrink `bounds` or raise `cellSize` (same guard on `physics/spatialGrid`'s `SpatialGrid`).
2539
+
2540
+ **Body shape: box or sphere.** `AddBodyOptions` is a discriminated union — `{ shape?: "box", halfExtents }` (box is the default, `shape` omittable) or `{ shape: "sphere", radius }` (the radius fills all three half-extent columns, so broadphase/bounds see the sphere's enclosing AABB). `world.shape[i]` (`SHAPE_BOX` / `SHAPE_SPHERE`) reports a live body's shape for a consumer walking the raw SoA arrays. Sphere-sphere and sphere-box pairs resolve with a proper radial normal (not the axis-aligned box/box path) — balls, projectile bodies, and rolling debris collide correctly against both boxes and each other.
2541
+
2542
+ **Collision shapes are box/sphere/voxel only.** Every collider in the engine — `PhysicsWorld` bodies (box `halfExtents` or sphere `radius`), object/entity picking (`scene/objectQuery` raycasts against unit boxes), and `world/voxelField`/`world/carve` blocks — is an axis-aligned box, a sphere, or a voxel cell. Arbitrary authored level-mesh collision (a sculpted GLB as a collider) is not supported. The seams for custom collision are `movement.beforeCommit` (steer or replace the walk controller's resolved step) and object raycasts (query the scene yourself and react) — not a mesh collider.
2543
+
2544
+ **Ballistic collision sweep (`physics/ballisticSweep`).** `createBallisticSweep(world, { step?, radius? })` → `BallisticSweep`, a `(origin, velocity, gravity, maxTime) => BallisticSweepHit | null` function that marches the closed-form arc (constant gravity, straight lateral) through a `PhysicsWorld` and reports the first sample inside any live body's AABB (sleeping bodies included), refined by one bisection step; `null` means the whole arc is clear. `step` (default 1/60) is the march interval in seconds, `radius` (default 0) inflates every body's AABB before the point test — pass the projectile's own radius. Wire it into `combat/projectiles` via `ProjectileSystemDeps.sweepBallistic`: when set, a ballistic shot settles at the swept impact point instead of the closed-form landing; omitted or `null` falls back to that closed-form arc.
2545
+
2546
+ **Removing and moving bodies.** `removeBody(id)` tombstones a body — it drops out of integration/broadphase and its slot is queued for the next `addBody` — without moving or invalidating any other body's `id` (ids are raw SoA slots, stored as-is in joints and game state, so nothing ever gets swapped). It conservatively wakes any sleeping body whose AABB touched the removed one's (no persistent contact set to consult, so this errs toward waking too much, never too little). `setVelocity(id, x, y, z)` and `setPosition(id, x, y, z)` write a body's velocity/position directly (instead of poking the public `velX`/`posX` SoA arrays) and wake it if asleep; `teleport(id, x, y, z)` is `setPosition` plus a hard velocity reset (respawn/teleporter, vs. sliding). `isAlive(id)`/`highWater` (one past the highest slot ever handed out) let a consumer that iterates the raw SoA arrays skip tombstoned holes correctly instead of assuming `count` is a dense `0..count` range.
2547
+
2548
+ **Joints & constraints.** `hingeJoint`/`fixedJoint`/`distanceJoint`/`springJoint(opts)` connect two bodies, or a body to a fixed world point (omit `bodyB`). The sim is translational (no angular DOF), so `hinge`/`fixed` pin the shared anchor (the `axis` is retained metadata), `distance` holds a fixed separation, and `spring` drives toward `restLength` with `stiffness`/`damping` (suspension, follow-point carry). `removeJoint(id)`, `setJointAnchor(id, x, y, z)` (move anchor B — a world anchor's follow point, or body B's local offset), `setJointAnchorA(id, x, y, z)` (move anchor A's local offset — e.g. re-rotating a suspension mount each frame as the chassis turns), `setJointRest`, and `readJointSegments(out)` for `@jgengine/shell/world/InstancedJoints` (debug line render). This is the foundation under vehicles, ragdolls, grapples, and carry.
2549
+
2550
+ **Collision → gameplay events.** `world.onCollision(listener, minApproachSpeed?)` delivers every impacting contact — `CollisionEvent { a, b, nx, ny, nz, approachSpeed, impulse }` — to game code during `step` (the object is reused; read/copy it, never retain). This is the seam crash-damage and destruction read; pass `null` to detach.
2551
+
2552
+ **Actors on top of the sim:** `physics/ragdoll` (`createRagdoll(world, { bones, links, balance? })` — jointed bones, floppy or active-ragdoll via a balance motor), `physics/carryable` (`Carryable` — grab a body to a follow point, shared multi-owner carry, `carrySpeedMultiplier` encumbrance, drop/throw; the raycast pick is the caller's job, core owns the constraint), `physics/forceVolume` (`ForceVolume` — impulse/velocity/accelerate trigger region, `once` for boost pads; `PlatformCarry` — carry bodies standing on a moving platform by its per-`step` delta). Separately, `physics/spatialGrid` `SpatialGrid` is a broad-phase grid over the x/z plane, **distinct** from the rigid-body sim, for cheap same-tick proximity across hundreds–thousands of simple movers — `rebuild(count, xs, zs)` then `queryCircle` (swarm enemies hitting a player/AoE) or `forEachPair` (mutual separation).
2553
+
2554
+ **Swarm-scale presentation.** `@jgengine/shell/world/SpriteBatch` renders up to `capacity` sprite-sheet quads as one `InstancedMesh` (per-instance atlas `frame`, `plane: "xy" | "xz"` or `billboard`, `pixelated` nearest-filtering) — the batched path for bullet-heaven swarms, item streams, and tile-grid presentation instead of one `<sprite>` per entity. Pair it with `world/lod`'s `createLodScheduler` (see the concept table) to band render detail by distance and throttle far entities' update work.
2555
+
2556
+ **Traversal (`physics/traversal`).** `Grapple` fires a rope from a body to a fixed world point on the joint API — `fire(x,y,z)` attaches a `distance` (rigid) or `elastic` (spring) joint, `reel(dt)`/`payOut(dt)` shorten/lengthen the rope to pull the traveller in, `moveAnchor` re-points it (ziplines, grapple-to-moving-target). Grapple/zipline/swing (Sekiro, Deep Rock, Just Cause) are all the same primitive; the raycast that finds the anchor is the caller's. `Glide` is a reduced-gravity, forward-thrust wingsuit/glider over a body — call `apply(dt, steerX, steerZ)` each frame before `step` to feed back most of gravity (`gravityScale`), thrust along the steer vector, and clamp descent; stop calling it to fall normally, no attach/detach state.
2557
+ **Structural destruction (`physics/structure`).** `StructureGraph` models a building as nodes (pieces) + load-bearing edges with some nodes `anchor`ed (foundations). `damage(id, n)`/`damageEdge(a,b,n)`/`severEdge(a,b)` wear pieces and connections; when one breaks, the graph recomputes reachability to an anchor and returns a single `CollapseEvent { fell }` — every piece the loss disconnected. `toDebris(world, event)` sinks the fallen pieces into a `PhysicsWorld` as rigid bodies (The Finals, Rainbow Six). It is coarse by design: **replicate the collapse event (the `fell` id list), not each fragment's physics** — game clients re-derive the debris locally. Piece integrity and edge strength default from a `StructureMaterial` table (DATA).
2558
+ ### Vehicles, mounts, crash damage & racing
2559
+ Five primitives layer a driving/racing game over the physics sim and `world/water`. All are **data-first** (spec the chassis/wheels/grip curve, damage thresholds, and checkpoint layout as catalog data) and pure `@jgengine/core`; renderers live in the game/shell. Each `update(dt, …)` runs **before** the shared `world.step(dt)`.
2560
+ - **Analog input — `input/axisInput`.** `AxisInput { throttle, brake, steer, handbrake }` is a continuous channel, **distinct from the digital action bindings**. `new AxisChannel({ bindings, smoothing })` ramps held keys into pedal-like analog values (`sample(dt, isDown)`), or `setAnalog(axis, value)` drives it straight from a gamepad axis. `DRIVE_AXIS_BINDINGS` is a ready WASD/arrow map.
2561
+ - **`physics/vehicleBody`.** `createVehicleBody(world, config)` is an arcade car: a chassis box body with per-wheel suspension held by G3's `springJoint` against the sampled `groundHeight`, drive/brake along the heading, and a `GripCurve` (`sampleGripCurve`) that bleeds lateral velocity for cornering — and, under `handbrake`, drift. `update(dt, axisInput)` then `world.step`. Because the chassis is a real body it still collides, which feeds crash damage. Rocket League, Trackmania, Wreckfest.
2562
+ - **`physics/buoyancy`.** `createBuoyantBody(world, { body, water, … })` floats a body on a CPU `WaterSurface` (Archimedes per hull point + water drag) so it settles at the waterline and rides the Gerstner waves; pass an `AxisInput` to `update(dt, time, input?)` and it drives as a boat (thrust + yaw + keel). Sea of Thieves, BOTW rafts.
2563
+ - **`scene/mount`.** `createMountController()` transfers control to a driven entity: `register({ id, kit, seats })`, `mount(riderId, mountId, seatId?)`, `dismount`. Read `cameraTarget(riderId)` to point the follow camera and `driveTarget(riderId)` to route that rider's `AxisInput` at the mount — the control seat drives, passenger seats ride (multi-seat shared vehicles), and an un-mounted rider drives themselves. `driver(mountId)`/`occupants(mountId)`/`kitOf`. Palworld mounts, V Rising horse, a crewed ship.
2564
+ - **`scene/stationClaim`.** `createStationClaim(controller?)` layers **facet stations** on `scene/mount` for a vehicle several players crew at once: `register({ id, kit, stations })` where each `Station` tags a seat with a `facet` (`"steer"`/`"sails"`/`"cannon"`). `claim(playerId, vehicleId, facetOrStationId)`, `release`, `controllerOf(vehicleId, facet)` (who mans it), `facetOf(playerId)`, `openFacets`, `crew`. Only a `control` station operates the hull (`driver`/`driveTarget`); the rest ride but command their own facet. Sea of Thieves helm + sails + cannons.
2565
+ - **`physics/damageZones`.** `createDamageModel({ zones, disableAt })` maps accumulated contact impulse (from `onCollision`) to **coarse discrete stages** (not soft-body): `absorb(zoneId, impulse)` / `routeCollision(event, resolveZone)` bump a zone's stage (caller swaps the visual/collider), an optional `detachStage` ejects a part as debris once, and crossing `disableAt` flips a whole-vehicle `disabled` state. Wreckfest crumple/derby.
2566
+ - **`game/race`.** `raceTrack({ checkpoints, laps })` is an ordered ring of AABB checkpoint volumes (the final one is the finish line); `createRaceState({ track, win })` — driven each tick by `update(now, positions)` on game time — emits `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished`, keeps split times, resolves a pluggable `RaceWinCondition` (`firstPastPost`, `topK` round-cut, `everyoneFinishes`, `lastStanding` derby), and `resetToCheckpoint(id)` hands back a respawn pose. `removeRacer(id)` drops a racer mid-race and renumbers the remaining standings; `reset()` clears all racer progress/finish state back to construction time so the same instance replays without rebuilding it. Trackmania, Mario Kart, Fall Guys.
2567
+
2568
+ ### Spawn placement
2569
+
2570
+ `spawn(catalogId, { id?, position | anchor, offset?, parentSpace?, group? })` — anchor `{ kind: "entity" | "zone", id }` with offset `{ radius, pattern }` or `{ xyz }`. Catalog supplies movement/model; no behaviors on spawn.
2571
+
2572
+ **Named spawn/respawn points** (`game/spawnPoints`) — `createSpawnPoints()`: `record(id, { x, y, z, rotationY? })` names a point (level bounds, team spawns, checkpoints), `get(id)`/`list()` read them back, `respawn(entities, entityId, spawnId)` teleports an existing entity to a recorded point via `setPose` in one call — the id-keyed alternative to threading raw coordinates through respawn logic by hand.
2573
+
2574
+ **Level sequence** (`game/levelSequence`) — `createLevelSequence({ levels: [{ id, config }], retriesPerLevel? })` is a pure, deterministic campaign machine: `start()` enters level 0, `clear()` marks the current level cleared, `advance()` moves to the next (or `"complete"` after the last), `fail()` consumes an attempt and returns `"retry"` while `retriesPerLevel` remain else `"failed"`, `retry()` restarts after a retry-eligible failure. `current()` → `{ id, index, config, attempt } | null`; `progress()` → `{ index, total, cleared }`; `reset()` rewinds to idle. Mirrors the reducer style of `game/race` and `ai/spawnDirector` — a level-select/roguelike-run campaign shell without hand-rolling the state machine per game.
2575
+
2576
+ # jgengine-api — Cartridge
2577
+
2578
+ Reference module for the [`jgengine`](./SKILL.md) skill. Load this when building a game as a **cartridge** — one declarative config instead of hand-wired loop/state/UI files. A cartridge game is ~75% smaller than the classic shape and nearly all of it is data; reach for `defineGame` directly only when the game's simulation doesn't fit the cartridge archetypes even through the escape hatches.
2579
+
2580
+ ## Why
2581
+
2582
+ Across the classic games, 78–91% of each game's LOC was generic wiring restated per game: a run-state store with subscribe/notify, chase-and-contact enemy loops, auto-fire weapon routines, magnet pickups, draft plumbing, phase-gated HUD trees, and tests for all of it. The cartridge layer owns every one of those; the game ships only its **data** (catalogs, tuning, layout) and any genuinely bespoke rules through typed escape hatches.
2583
+
2584
+ ## Shape
2585
+
2586
+ `game.config.ts` is the whole game — `cartridge({...})` from `@jgengine/shell/cartridge` compiles the config into a `PlayableGame` (validating it first — a bad reference throws at import with a list of problems). `loop.ts` and `world.ts` disappear; the spec carries the world feature and the loop is engine-owned. `main.tsx` is 5 lines via `mountGame` (`@/components/ui/mount`, registry). Sprites/SVG art stay in `src/game/assets.ts`; the game keeps one test file under `src/game/`.
2587
+
2588
+ ```ts
2589
+ import { cartridge, type CartridgeConfig } from "@jgengine/shell/cartridge";
2590
+ import { standardCartridgePanels } from "@/components/ui/cartridge-panels";
2591
+
2592
+ export const config: CartridgeConfig = {
2593
+ name, seed, panels: standardCartridgePanels, assets, entitySprites,
2594
+ flow: { start: "gate", countdownSeconds: 3, restart: true }, // press-to-start → 3-2-1 → playing; "restart" command
2595
+ player: { kind, health, walkSpeed }, // compiled into content + spawned per player
2596
+ enemies: { id: { label, health, walkSpeed, xp, contact: { damage, intervalSeconds }, behavior: "chase" | "none" } },
2597
+ combat: { contactRadius },
2598
+ spawning: { director: SpawnDirectorConfig, placement: { kind: "ring", radius } },
2599
+ weapons: { id: { kind: "projectile" | "orbit" | "pulse" | "custom", damage, cooldownMs, maxLevel, ... } },
2600
+ progression: { xp: Curve, maxLevel, draft: { choices, upgrades } },
2601
+ fields: { magnetRadius, damageMultiplier }, // named run-scalars upgrades mutate
2602
+ xpGems: { collectRadius, pullSpeed, rarityThresholds, defaultRarity },
2603
+ rules: { win: { kind: "survive", seconds }, lose: { kind: "playerDeath" } | { kind: "custom", check }, killLeaderboardStat },
2604
+ world, physics, camera, worldItem, theme, hud, screens, // screens: start/win/lose
2605
+ };
2606
+ export const game = cartridge(config);
2607
+ ```
2608
+
2609
+ The run is a phase machine every game gets for free: `start → countdown → playing → won | lost`. `flow.start: "gate"` renders `screens.start` (TitleScreen binding) until begin; `flow.countdownSeconds` renders the big-number countdown; `flow.restart: true` registers a `restart` command that resets the run in place, clears cartridge entities, and restores player stats — no hand-rolled session store, phase union, or reset function. `run.playingSeconds` is the run clock (drives the survive rule and the timer panel), so pauses and pre-game phases never skew timing.
2610
+
2611
+ ## Core surface (`@jgengine/core/cartridge/`)
2612
+
2613
+ - `@jgengine/core/cartridge/spec` — the `CartridgeSpec` types, `Leveled` values (`number | { base, perLevel, min?, max? } | { table }` resolved by `leveled(value, level)`), `WASD_KEYBINDS` (the default when `input` is omitted).
2614
+ - `@jgengine/core/cartridge/runtime` — `createCartridge(spec)` → `{ content, loop, run(ctx), weaponKit(ctx), chooseUpgrade(ctx, offerId) }`. Owns the run store (outcome/kills/fields/weapon levels, subscribe/notify), spawn director advance + ring placement, chase + contact-damage enemies (terrain-grounded), the three weapon archetypes with auto-targeting, magnet xp-gem pickups feeding `leveling()`, pause-and-draft level-ups, kill → gem drop + leaderboard, and win/lose rules.
2615
+ - `@jgengine/core/cartridge/validate` — `validateCartridge(spec)` → problem list: spawn waves must reference declared enemies, upgrades must reference declared weapons/fields, stacks can't exceed weapon `maxLevel`, tuning must be positive, rarity thresholds descending. The game's test asserts it returns `[]`.
2616
+
2617
+ Weapon archetypes: `projectile` (auto-target nearest in `range`, travel-time bolt fx), `orbit` (leveled blade count/radius sweeping `hitRadius`), `pulse` (radial AoE with linear falloff and an expanding-ring fx). Upgrade effects: `weaponLevel` (+1 level per stack), `statBonus` (+max and +current on pick), `fieldAdd` / `fieldMultiply` (recomputed from stacks). Weapon damage is multiplied by the `damageMultiplier` field when declared.
2618
+
2619
+ ## Presentation (shell + registry)
2620
+
2621
+ `hud.panels` is a schema — items `vital | xp | timer | score | abilityBar | component`, anchored via the movable HUD layout; `screens.win/lose` render the results/death screens; the upgrade-draft modal appears whenever a draft is pending. The shell renders the frame and injects concrete components through the `panels` seam — `standardCartridgePanels` (registry `cartridge-panels`) binds the jgengine UI kit; swap any binding for a custom component without touching the engine. Weapon fx render engine-side from the run store's bolt/pulse feeds (`fxColor`/`fxEmissive` per weapon). `theme` is the `--jg-*` CSS var block — hand-pick it or derive from a few seed colors with `deriveJgTheme` (registry `jg-theme`).
2622
+
2623
+ ## Escape hatches — bespoke logic without leaving the cartridge
2624
+
2625
+ - `weapons.<id> = { kind: "custom", fire(ctx, run, args) }` — cooldown/level/damage handled for you; the fire shape is yours.
2626
+ - `spawning.placement = { kind: "custom", position(ctx, run) }`.
2627
+ - `rules.win = { kind: "custom", check(ctx, run) }`.
2628
+ - `progression.draft.upgrades[].effect = { kind: "custom", apply(ctx, run, stacks) }`.
2629
+ - `systems: [(ctx, run, dt) => ...]` — extra per-tick simulation after the built-ins, only while playing.
2630
+ - `hud` items `{ kind: "component", Component }` — arbitrary React panels alongside the schema ones.
2631
+
2632
+ A game that is *mostly* bespoke simulation (custom physics, session machines) should keep `defineGame` and write its systems — the cartridge is for games whose shape the archetypes already cover.
2633
+
2634
+ ## Testing
2635
+
2636
+ Cartridge behaviors are engine-tested once (`packages/core/src/cartridge/runtime.test.ts`); the game test is one call plus any game-specific assertions:
2637
+
2638
+ ```ts
2639
+ import { bootCartridge, cartridgeSmokeTest, tickCartridge } from "@jgengine/core/cartridge/testkit";
2640
+ cartridgeSmokeTest(config); // validate + world summary + headless run/spawn/kill/gem smoke
2641
+ ```
2642
+
2643
+ `bootCartridge`/`tickCartridge` build a headless `GameContext` and drive the loop (auto-choosing drafts) for custom assertions — see `Games/swarm-survivor/src/game/cartridge.test.ts`. The testkit imports `bun:test`; import it from test files only.