@jgengine/shell 0.6.0 → 0.8.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 (118) hide show
  1. package/CHANGELOG.md +214 -2
  2. package/README.md +26 -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 +33 -1
  8. package/dist/GamePlayerShell.js +641 -59
  9. package/dist/audio/AudioComponents.d.ts +12 -0
  10. package/dist/audio/AudioComponents.js +53 -0
  11. package/dist/audio/audioEngine.d.ts +23 -0
  12. package/dist/audio/audioEngine.js +110 -0
  13. package/dist/behaviour.d.ts +31 -0
  14. package/dist/behaviour.js +63 -0
  15. package/dist/camera/GameCameraRig.d.ts +16 -0
  16. package/dist/camera/GameCameraRig.js +56 -0
  17. package/dist/camera/GameFirstPersonCamera.js +2 -0
  18. package/dist/camera/GameInspectionCamera.d.ts +6 -0
  19. package/dist/camera/GameInspectionCamera.js +24 -0
  20. package/dist/camera/GameOrbitCamera.d.ts +3 -1
  21. package/dist/camera/GameOrbitCamera.js +4 -2
  22. package/dist/camera/cameraRigs.d.ts +26 -0
  23. package/dist/camera/cameraRigs.js +522 -0
  24. package/dist/camera/index.d.ts +7 -0
  25. package/dist/camera/index.js +7 -0
  26. package/dist/camera/inspectionCameraMath.d.ts +25 -0
  27. package/dist/camera/inspectionCameraMath.js +44 -0
  28. package/dist/camera/rigMath.d.ts +169 -0
  29. package/dist/camera/rigMath.js +393 -0
  30. package/dist/camera/rigResolve.d.ts +15 -0
  31. package/dist/camera/rigResolve.js +52 -0
  32. package/dist/camera/shakeChannel.d.ts +29 -0
  33. package/dist/camera/shakeChannel.js +38 -0
  34. package/dist/defineGame.d.ts +16 -0
  35. package/dist/defineGame.js +51 -0
  36. package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
  37. package/dist/devtools/DevtoolsOverlay.js +334 -0
  38. package/dist/environment/Daylight.d.ts +50 -0
  39. package/dist/environment/Daylight.js +100 -0
  40. package/dist/environment/EnvironmentScene.js +18 -26
  41. package/dist/environment/GroundPad.d.ts +7 -0
  42. package/dist/environment/GroundPad.js +12 -0
  43. package/dist/environment/daylightCycle.d.ts +26 -0
  44. package/dist/environment/daylightCycle.js +116 -0
  45. package/dist/environment/groundPadMath.d.ts +13 -0
  46. package/dist/environment/groundPadMath.js +10 -0
  47. package/dist/environment/index.d.ts +2 -0
  48. package/dist/environment/index.js +2 -0
  49. package/dist/map/MapMarkerBeacons.d.ts +12 -0
  50. package/dist/map/MapMarkerBeacons.js +16 -0
  51. package/dist/map/index.d.ts +2 -0
  52. package/dist/map/index.js +2 -0
  53. package/dist/map/terrainMap.d.ts +23 -0
  54. package/dist/map/terrainMap.js +79 -0
  55. package/dist/materialOverride.d.ts +8 -0
  56. package/dist/materialOverride.js +32 -0
  57. package/dist/multiplayer.d.ts +16 -9
  58. package/dist/multiplayer.js +62 -12
  59. package/dist/pointer/PointerOverlays.d.ts +12 -0
  60. package/dist/pointer/PointerOverlays.js +17 -0
  61. package/dist/pointer/PointerProbe.d.ts +4 -0
  62. package/dist/pointer/PointerProbe.js +40 -0
  63. package/dist/pointer/pointerService.d.ts +26 -0
  64. package/dist/pointer/pointerService.js +89 -0
  65. package/dist/registry.d.ts +4 -1
  66. package/dist/registry.js +3 -1
  67. package/dist/render/modelRender.d.ts +14 -0
  68. package/dist/render/modelRender.js +79 -0
  69. package/dist/replay/useSessionRecorder.d.ts +14 -0
  70. package/dist/replay/useSessionRecorder.js +22 -0
  71. package/dist/structures/PlacementGhost.d.ts +8 -0
  72. package/dist/structures/PlacementGhost.js +11 -0
  73. package/dist/structures/index.d.ts +2 -0
  74. package/dist/structures/index.js +2 -0
  75. package/dist/structures.d.ts +1 -0
  76. package/dist/structures.js +1 -0
  77. package/dist/terrain/CarvedTerrain.d.ts +22 -0
  78. package/dist/terrain/CarvedTerrain.js +19 -0
  79. package/dist/terrain/EditableGround.d.ts +11 -0
  80. package/dist/terrain/EditableGround.js +38 -0
  81. package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
  82. package/dist/terrain/TerraformBrushCursor.js +13 -0
  83. package/dist/terrain/index.d.ts +4 -1
  84. package/dist/terrain/index.js +4 -1
  85. package/dist/terrain/terrainMath.d.ts +15 -1
  86. package/dist/terrain/terrainMath.js +87 -1
  87. package/dist/touch/TouchControlsOverlay.d.ts +18 -0
  88. package/dist/touch/TouchControlsOverlay.js +173 -0
  89. package/dist/vision/FrustumSensorHud.d.ts +21 -0
  90. package/dist/vision/FrustumSensorHud.js +67 -0
  91. package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
  92. package/dist/vision/HiddenStateProbeHud.js +11 -0
  93. package/dist/vision/RevealVision.d.ts +28 -0
  94. package/dist/vision/RevealVision.js +53 -0
  95. package/dist/water/OceanShader.d.ts +1 -1
  96. package/dist/water/OceanShader.js +3 -3
  97. package/dist/weather/FireSpreadLayer.d.ts +14 -0
  98. package/dist/weather/FireSpreadLayer.js +68 -0
  99. package/dist/weather/index.d.ts +1 -0
  100. package/dist/weather/index.js +1 -0
  101. package/dist/world/DataObjects.d.ts +44 -0
  102. package/dist/world/DataObjects.js +75 -0
  103. package/dist/world/GridWorldScene.d.ts +10 -0
  104. package/dist/world/GridWorldScene.js +54 -0
  105. package/dist/world/InstancedJoints.d.ts +14 -0
  106. package/dist/world/InstancedJoints.js +32 -0
  107. package/dist/world/WorldHud.d.ts +7 -1
  108. package/dist/world/WorldHud.js +76 -14
  109. package/dist/world/WorldItems.d.ts +5 -0
  110. package/dist/world/WorldItems.js +50 -0
  111. package/dist/world/floatTextStyle.d.ts +14 -0
  112. package/dist/world/floatTextStyle.js +39 -0
  113. package/llms.txt +2382 -0
  114. package/package.json +10 -5
  115. package/dist/demo/demoGame.d.ts +0 -2
  116. package/dist/demo/demoGame.js +0 -208
  117. package/dist/demo/environmentShowcase.d.ts +0 -2
  118. package/dist/demo/environmentShowcase.js +0 -15
package/llms.txt ADDED
@@ -0,0 +1,2382 @@
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.8.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/audio/AudioComponents
13
+
14
+ - AudioListener (function): function AudioListener({ engine }: { engine: AudioEngine }): null
15
+ - EntityAudioEmitters (function): function EntityAudioEmitters({ engine, entitySounds, }: { engine: AudioEngine; entitySounds: Record<string, string> | undefined; }): null
16
+ - ObjectAudioEmitters (function): function ObjectAudioEmitters({ engine, objectSounds, }: { engine: AudioEngine; objectSounds: Record<string, string> | undefined; }): null
17
+
18
+ ### @jgengine/shell/audio/audioEngine
19
+
20
+ - createAudioEngine (function): function createAudioEngine(config: AudioSceneConfig = {}): AudioEngine
21
+ - Vec3 (interface): interface Vec3
22
+ - AudioSceneConfig (interface): interface AudioSceneConfig
23
+ - AudioEmitterHandle (interface): interface AudioEmitterHandle
24
+ - AudioEngine (interface): interface AudioEngine
25
+
26
+ ### @jgengine/shell/behaviour
27
+
28
+ - attachObject3D (function): function attachObject3D<T extends Object3DBehaviour>(world: BehaviourWorld, object: Object3D, behaviour: T, nodeId: string = object.uuid): T — Binds `behaviour` to `object` and attaches it to `world` under `nodeId` (default: the object's uuid). Render hooks are chained onto the object's existing `onBeforeRender`/`onAfterRender` only when actually overridden, and are gated on the behaviour being active; the update lifecycle still flows through `world.update`.
29
+ - useBehaviourWorld (function): function useBehaviourWorld(world: BehaviourWorld, scaleDt?: (rawDt: number) => number): void — Bootstraps `world` on mount and dispatches `world.update` every frame at simulation priority. `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.
30
+ - Object3DBehaviour (class): class Object3DBehaviour extends Behaviour — A core `Behaviour` bound to a three.js object. `onBeforeRender`/`onAfterRender` ride the object's own three.js render callbacks, so they only fire for renderable objects (Mesh, Line, Points, Sprite) that are visible and in frustum — attach to the mesh itself, not a parent Group, when you need them.
31
+
32
+ ### @jgengine/shell/camera/cameraRigs
33
+
34
+ - TopDownRig (function): function TopDownRig(props: RigProps): null
35
+ - 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.
36
+ - RtsRig (function): function RtsRig(props: RigProps & { panKeysEnabled?: boolean }): null
37
+ - ShoulderRig (function): function ShoulderRig(props: RigProps): null
38
+ - LockOnRig (function): function LockOnRig(props: RigProps): null
39
+ - ChaseRig (function): function ChaseRig(props: RigProps): null
40
+ - 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.
41
+ - CinematicRig (function): function CinematicRig(props: RigProps): null
42
+ - CAMERA_RIG_FRAME_PRIORITY (const): const CAMERA_RIG_FRAME_PRIORITY: -1
43
+ - CAMERA_POST_FRAME_PRIORITY (const): const CAMERA_POST_FRAME_PRIORITY: number
44
+ - RigProps (interface): interface RigProps
45
+
46
+ ### @jgengine/shell/camera/GameCameraRig
47
+
48
+ - GameCameraRig (function): function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): React.JSX.Element
49
+ - 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.
50
+ - GameCameraRigProps (interface): interface GameCameraRigProps
51
+
52
+ ### @jgengine/shell/camera/GameFirstPersonCamera
53
+
54
+ - GameFirstPersonCamera (function): function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): React.JSX.Element | null
55
+ - GameFirstPersonCameraProps (interface): interface GameFirstPersonCameraProps
56
+
57
+ ### @jgengine/shell/camera/GameInspectionCamera
58
+
59
+ - 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.
60
+ - GameInspectionCameraProps (interface): interface GameInspectionCameraProps
61
+
62
+ ### @jgengine/shell/camera/GameOrbitCamera
63
+
64
+ - GameOrbitCamera (function): function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls = false, }: GameOrbitCameraProps): React.JSX.Element
65
+ - seedOrbitCameraTarget (function): function seedOrbitCameraTarget(camera: Camera, target: Vector3, distance: number, height: number): void — Seed orbit target before controls mount (demo spawn at origin).
66
+ - CameraFollowListener (type): type CameraFollowListener = (state: CameraFollowState) => void
67
+ - GameOrbitCameraProps (interface): interface GameOrbitCameraProps
68
+
69
+ ### @jgengine/shell/camera/index
70
+
71
+ - GameOrbitCamera (function): function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls = false, }: GameOrbitCameraProps): React.JSX.Element
72
+ - CameraFollowListener (type): type CameraFollowListener = (state: CameraFollowState) => void
73
+ - GameOrbitCameraProps (interface): interface GameOrbitCameraProps
74
+ - GameFirstPersonCamera (function): function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): React.JSX.Element | null
75
+ - GameFirstPersonCameraProps (interface): interface GameFirstPersonCameraProps
76
+ - 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.
77
+ - GameInspectionCameraProps (interface): interface GameInspectionCameraProps
78
+ - GameCameraRig (function): function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): React.JSX.Element
79
+ - 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.
80
+ - GameCameraRigProps (interface): interface GameCameraRigProps
81
+ - CAMERA_POST_FRAME_PRIORITY (const): const CAMERA_POST_FRAME_PRIORITY: number
82
+ - CAMERA_RIG_FRAME_PRIORITY (const): const CAMERA_RIG_FRAME_PRIORITY: -1
83
+ - ChaseRig (function): function ChaseRig(props: RigProps): null
84
+ - CinematicRig (function): function CinematicRig(props: RigProps): null
85
+ - LockOnRig (function): function LockOnRig(props: RigProps): null
86
+ - 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.
87
+ - RtsRig (function): function RtsRig(props: RigProps & { panKeysEnabled?: boolean }): null
88
+ - ShoulderRig (function): function ShoulderRig(props: RigProps): null
89
+ - 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.
90
+ - TopDownRig (function): function TopDownRig(props: RigProps): null
91
+ - RigProps (interface): interface RigProps
92
+ - CameraShakeContext (const): const CameraShakeContext: React.Context<import("/home/runner/work/jgengine/jgengine/packages/shell/src/camera/shakeChannel").CameraShakeChannel>
93
+ - cameraShake (function): function cameraShake(amplitude: number, decayPerSecond?: number): void — Feed the default camera-shake channel from anywhere (see G7 hitstop cross-cut).
94
+ - createCameraShakeChannel (function): function createCameraShakeChannel(defaultDecayPerSecond = 1.6): CameraShakeChannel
95
+ - 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.
96
+ - useCameraShake (function): function useCameraShake(): CameraShakeChannel — The active rig's shake channel — call `.shake(...)` to add trauma from React UI.
97
+ - CameraShakeChannel (interface): interface CameraShakeChannel
98
+ - addTrauma (function): function addTrauma(state: TraumaState, amount: number): void — Add trauma (0..1) and clamp; larger hits raise the ceiling toward 1.
99
+ - angleDelta (function): function angleDelta(a: number, b: number): number — Shortest signed angular delta from `a` to `b`, wrapped to (-PI, PI].
100
+ - blendShoulder (function): function blendShoulder(hip: ResolvedShoulder, ads: ResolvedShoulder, t: number): ResolvedShoulder — Blend two shoulder framings by an ADS factor in [0,1].
101
+ - chaseDesiredPosition (function): function chaseDesiredPosition(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
102
+ - chaseLookAt (function): function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Look point ahead of / above a chased vehicle.
103
+ - 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).
104
+ - clamp (function): function clamp(value: number, min: number, max: number): number
105
+ - createTrauma (function): function createTrauma(): TraumaState
106
+ - crossfadePose (function): function crossfadePose(from: CameraPose, to: CameraPose, t: number): CameraPose — Linear cross-fade between two full camera poses (position, lookAt, fov).
107
+ - forwardVector (function): function forwardVector(yaw: number): Vec3
108
+ - lerp (function): function lerp(from: number, to: number, blend: number): number
109
+ - 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`.
110
+ - 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.
111
+ - resolveChase (function): function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase
112
+ - 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.
113
+ - resolveObserver (function): function resolveObserver(config: ObserverCameraConfig | undefined): ResolvedObserver
114
+ - resolveShoulder (function): function resolveShoulder(config: ShoulderCameraConfig | undefined, aiming: boolean): ResolvedShoulder
115
+ - resolveSideScroll (function): function resolveSideScroll(config: SideScrollCameraConfig | undefined): ResolvedSideScroll
116
+ - 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.
117
+ - resolveTopDown (function): function resolveTopDown(config: TopDownCameraConfig | undefined): ResolvedTopDown
118
+ - rightVector (function): function rightVector(yaw: number): Vec3
119
+ - rtsPanKeysConflict (function): function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean
120
+ - 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.
121
+ - 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).
122
+ - 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.
123
+ - 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.
124
+ - smoothstep (function): function smoothstep(t: number): number
125
+ - smoothYaw (function): function smoothYaw(current: number, desired: number, speed: number, dt: number): number — Exponential yaw smoothing that respects wrap-around.
126
+ - 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`.
127
+ - springArmStep (function): function springArmStep(current: Vec3, desired: Vec3, damping: number, dt: number): Vec3 — Exponential spring-arm approach toward a desired point (frame-rate independent).
128
+ - stepTrauma (function): function stepTrauma(state: TraumaState, decayPerSecond: number, dt: number): void — Decay trauma linearly toward 0 and advance the shake clock.
129
+ - 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.
130
+ - 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)).
131
+ - CameraPose (interface): interface CameraPose
132
+ - DirectorCameraValues (interface): interface DirectorCameraValues
133
+ - ResolvedChase (interface): interface ResolvedChase
134
+ - ResolvedDirectedCamera (interface): interface ResolvedDirectedCamera
135
+ - ResolvedObserver (interface): interface ResolvedObserver
136
+ - ResolvedShoulder (interface): interface ResolvedShoulder
137
+ - ResolvedSideScroll (interface): interface ResolvedSideScroll
138
+ - ResolvedTopDown (interface): interface ResolvedTopDown
139
+ - ShakeOffset (interface): interface ShakeOffset
140
+ - StaticCameraValues (interface): interface StaticCameraValues
141
+ - TraumaState (interface): interface TraumaState
142
+ - cameraFollowStep (function): function cameraFollowStep(input: { camera: Vec3; target: Vec3; previousTarget: Vec3 | null; lockedDistance: number | null; }): CameraFollowState
143
+ - 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.
144
+ - DEFAULT_ORBIT_CAMERA (const): const DEFAULT_ORBIT_CAMERA: ResolvedOrbitCameraConfig
145
+ - distanceBetween (function): function distanceBetween(a: Vec3, b: Vec3): number
146
+ - GAME_SIM_FRAME_PRIORITY (const): const GAME_SIM_FRAME_PRIORITY: 0 — Run simulation/movement before orbit follow so poses are current.
147
+ - lerpVec3 (function): function lerpVec3(from: Vec3, to: Vec3, blend: number): Vec3
148
+ - ORBIT_CAMERA_FRAME_PRIORITY (const): const ORBIT_CAMERA_FRAME_PRIORITY: -1 — Orbit follow reads the latest entity pose after GAME_SIM_FRAME_PRIORITY.
149
+ - 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.
150
+ - 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.
151
+ - resolveFollowTargetFromPosition (function): function resolveFollowTargetFromPosition(position: readonly [number, number, number], config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset">): Vec3
152
+ - resolveOrbitCameraConfig (function): function resolveOrbitCameraConfig(patch?: OrbitCameraConfig): ResolvedOrbitCameraConfig
153
+ - resolveTargetSmoothing (function): function resolveTargetSmoothing(config: ResolvedOrbitCameraConfig, dragging: boolean): number
154
+ - seedOrbitFollowState (function): function seedOrbitFollowState(input: { entityPosition: readonly [number, number, number]; config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset" | "initialDistance" | "initialHeight">; }): OrbitFollowRuntimeState
155
+ - smoothBlend (function): function smoothBlend(deltaSeconds: number, speed: number): number — Frame-rate independent exponential smoothing factor in [0, 1].
156
+ - CameraFollowState (interface): interface CameraFollowState
157
+ - OrbitCameraConfig (interface): interface OrbitCameraConfig
158
+ - OrbitFollowRuntimeState (interface): interface OrbitFollowRuntimeState
159
+ - ResolvedOrbitCameraConfig (interface): interface ResolvedOrbitCameraConfig — Fully resolved shell config after merging with DEFAULT_ORBIT_CAMERA.
160
+ - Vec3 (interface): interface Vec3
161
+ - resolveInspectionCameraConfig (function): function resolveInspectionCameraConfig(config?: InspectionCameraConfig): ResolvedInspectionCameraConfig
162
+ - resolveInspectionZoomToCursor (function): function resolveInspectionZoomToCursor(anchor: InspectionZoomAnchor): boolean — Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag.
163
+ - 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.
164
+ - ResolvedInspectionCameraConfig (interface): interface ResolvedInspectionCameraConfig
165
+
166
+ ### @jgengine/shell/camera/inspectionCameraMath
167
+
168
+ - resolveInspectionCameraConfig (function): function resolveInspectionCameraConfig(config?: InspectionCameraConfig): ResolvedInspectionCameraConfig
169
+ - 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.
170
+ - resolveInspectionZoomToCursor (function): function resolveInspectionZoomToCursor(anchor: InspectionZoomAnchor): boolean — Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag.
171
+ - ResolvedInspectionCameraConfig (interface): interface ResolvedInspectionCameraConfig
172
+
173
+ ### @jgengine/shell/camera/orbitCameraMath
174
+
175
+ - 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.
176
+ - 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.
177
+ - smoothBlend (function): function smoothBlend(deltaSeconds: number, speed: number): number — Frame-rate independent exponential smoothing factor in [0, 1].
178
+ - resolveOrbitCameraConfig (function): function resolveOrbitCameraConfig(patch?: OrbitCameraConfig): ResolvedOrbitCameraConfig
179
+ - resolveTargetSmoothing (function): function resolveTargetSmoothing(config: ResolvedOrbitCameraConfig, dragging: boolean): number
180
+ - resolveFollowTargetFromPosition (function): function resolveFollowTargetFromPosition(position: readonly [number, number, number], config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset">): Vec3
181
+ - lerpVec3 (function): function lerpVec3(from: Vec3, to: Vec3, blend: number): Vec3
182
+ - distanceBetween (function): function distanceBetween(a: Vec3, b: Vec3): number
183
+ - seedOrbitFollowState (function): function seedOrbitFollowState(input: { entityPosition: readonly [number, number, number]; config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset" | "initialDistance" | "initialHeight">; }): OrbitFollowRuntimeState
184
+ - 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.
185
+ - cameraFollowStep (function): function cameraFollowStep(input: { camera: Vec3; target: Vec3; previousTarget: Vec3 | null; lockedDistance: number | null; }): CameraFollowState
186
+ - Vec3 (interface): interface Vec3
187
+ - CameraFollowState (interface): interface CameraFollowState
188
+ - OrbitCameraConfig (interface): interface OrbitCameraConfig
189
+ - ResolvedOrbitCameraConfig (interface): interface ResolvedOrbitCameraConfig — Fully resolved shell config after merging with DEFAULT_ORBIT_CAMERA.
190
+ - DEFAULT_ORBIT_CAMERA (const): const DEFAULT_ORBIT_CAMERA: ResolvedOrbitCameraConfig
191
+ - GAME_SIM_FRAME_PRIORITY (const): const GAME_SIM_FRAME_PRIORITY: 0 — Run simulation/movement before orbit follow so poses are current.
192
+ - ORBIT_CAMERA_FRAME_PRIORITY (const): const ORBIT_CAMERA_FRAME_PRIORITY: -1 — Orbit follow reads the latest entity pose after GAME_SIM_FRAME_PRIORITY.
193
+ - OrbitFollowRuntimeState (interface): interface OrbitFollowRuntimeState
194
+
195
+ ### @jgengine/shell/camera/rigMath
196
+
197
+ - rtsPanKeysConflict (function): function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean
198
+ - clamp (function): function clamp(value: number, min: number, max: number): number
199
+ - lerp (function): function lerp(from: number, to: number, blend: number): number
200
+ - smoothstep (function): function smoothstep(t: number): number
201
+ - forwardVector (function): function forwardVector(yaw: number): Vec3
202
+ - rightVector (function): function rightVector(yaw: number): Vec3
203
+ - resolveTopDown (function): function resolveTopDown(config: TopDownCameraConfig | undefined): ResolvedTopDown
204
+ - 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.
205
+ - springArmStep (function): function springArmStep(current: Vec3, desired: Vec3, damping: number, dt: number): Vec3 — Exponential spring-arm approach toward a desired point (frame-rate independent).
206
+ - resolveSideScroll (function): function resolveSideScroll(config: SideScrollCameraConfig | undefined): ResolvedSideScroll
207
+ - 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.
208
+ - 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.
209
+ - 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`.
210
+ - 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)).
211
+ - angleDelta (function): function angleDelta(a: number, b: number): number — Shortest signed angular delta from `a` to `b`, wrapped to (-PI, PI].
212
+ - smoothYaw (function): function smoothYaw(current: number, desired: number, speed: number, dt: number): number — Exponential yaw smoothing that respects wrap-around.
213
+ - resolveShoulder (function): function resolveShoulder(config: ShoulderCameraConfig | undefined, aiming: boolean): ResolvedShoulder
214
+ - blendShoulder (function): function blendShoulder(hip: ResolvedShoulder, ads: ResolvedShoulder, t: number): ResolvedShoulder — Blend two shoulder framings by an ADS factor in [0,1].
215
+ - 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.
216
+ - 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`.
217
+ - resolveChase (function): function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase
218
+ - resolveObserver (function): function resolveObserver(config: ObserverCameraConfig | undefined): ResolvedObserver
219
+ - 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.
220
+ - chaseDesiredPosition (function): function chaseDesiredPosition(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
221
+ - chaseLookAt (function): function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Look point ahead of / above a chased vehicle.
222
+ - 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.
223
+ - createTrauma (function): function createTrauma(): TraumaState
224
+ - addTrauma (function): function addTrauma(state: TraumaState, amount: number): void — Add trauma (0..1) and clamp; larger hits raise the ceiling toward 1.
225
+ - stepTrauma (function): function stepTrauma(state: TraumaState, decayPerSecond: number, dt: number): void — Decay trauma linearly toward 0 and advance the shake clock.
226
+ - 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).
227
+ - crossfadePose (function): function crossfadePose(from: CameraPose, to: CameraPose, t: number): CameraPose — Linear cross-fade between two full camera poses (position, lookAt, fov).
228
+ - 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.
229
+ - 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).
230
+ - CameraPose (interface): interface CameraPose
231
+ - ResolvedTopDown (interface): interface ResolvedTopDown
232
+ - ResolvedSideScroll (interface): interface ResolvedSideScroll
233
+ - ResolvedShoulder (interface): interface ResolvedShoulder
234
+ - ResolvedChase (interface): interface ResolvedChase
235
+ - ResolvedObserver (interface): interface ResolvedObserver
236
+ - TraumaState (interface): interface TraumaState
237
+ - ShakeOffset (interface): interface ShakeOffset
238
+ - DirectorCameraValues (interface): interface DirectorCameraValues
239
+ - StaticCameraValues (interface): interface StaticCameraValues
240
+ - ResolvedDirectedCamera (interface): interface ResolvedDirectedCamera
241
+ - CinematicSample (interface): interface CinematicSample
242
+
243
+ ### @jgengine/shell/camera/rigResolve
244
+
245
+ - 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.
246
+ - 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.
247
+
248
+ ### @jgengine/shell/camera/shakeChannel
249
+
250
+ - createCameraShakeChannel (function): function createCameraShakeChannel(defaultDecayPerSecond = 1.6): CameraShakeChannel
251
+ - cameraShake (function): function cameraShake(amplitude: number, decayPerSecond?: number): void — Feed the default camera-shake channel from anywhere (see G7 hitstop cross-cut).
252
+ - useCameraShake (function): function useCameraShake(): CameraShakeChannel — The active rig's shake channel — call `.shake(...)` to add trauma from React UI.
253
+ - CameraShakeChannel (interface): interface CameraShakeChannel
254
+ - 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.
255
+ - CameraShakeContext (const): const CameraShakeContext: React.Context<import("/home/runner/work/jgengine/jgengine/packages/shell/src/camera/shakeChannel").CameraShakeChannel>
256
+
257
+ ### @jgengine/shell/defineGame
258
+
259
+ - defineGame (function): function defineGame<TAssetRef extends ModelAssetRef = ModelAssetRef>(config: GameConfig<TAssetRef>): PlayableGame
260
+ - GameConfig (type): type GameConfig<TAssetRef extends ModelAssetRef = ModelAssetRef> = EngineFields<TAssetRef> & PresentationFields
261
+
262
+ ### @jgengine/shell/devtools/DevtoolsOverlay
263
+
264
+ - persistDevtoolsOverrides (function): function persistDevtoolsOverrides(gameName: string): DevtoolsOverrides
265
+ - applyStoredDevtoolsOverrides (function): function applyStoredDevtoolsOverrides(gameName: string): void
266
+ - withDevtoolsLatency (function): function withDevtoolsLatency(multiplayer: ShellMultiplayer): ShellMultiplayer
267
+ - DevtoolsRendererProbe (function): function DevtoolsRendererProbe(): null
268
+ - DevtoolsOverlay (function): function DevtoolsOverlay({ open, ctx, playable, multiplayer, }: { open: boolean; ctx: GameContext; playable: PlayableGame; multiplayer: ShellMultiplayer | null; }): React.JSX.Element | null
269
+
270
+ ### @jgengine/shell/environment
271
+
272
+ - Daylight (function): function Daylight({ sky, fog, sun, ambient }: DaylightProps = {}): React.JSX.Element
273
+ - SkyDaylight (function): function SkyDaylight({ sky }: { sky: SkyEnvironmentDescriptor }): 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.
274
+ - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
275
+ - TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock }: TimeOfDayDaylightProps): React.JSX.Element — Drives `Daylight`'s sun/sky/fog from the world clock when `sky.timeOfDay` and `clock` are both present, sampling `daylightStateAt` every frame; otherwise renders the static preset look via `SkyDaylight`.
276
+ - DaylightProps (interface): interface DaylightProps
277
+ - SkyDomeProps (interface): interface SkyDomeProps
278
+ - TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
279
+ - EnvironmentScene (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element
280
+ - EnvironmentSceneProps (interface): interface EnvironmentSceneProps
281
+ - 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.
282
+ - lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
283
+ - SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
284
+ - DaylightCycleConfig (interface): interface DaylightCycleConfig
285
+ - DaylightState (interface): interface DaylightState
286
+
287
+ ### @jgengine/shell/environment/Daylight
288
+
289
+ - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
290
+ - Daylight (function): function Daylight({ sky, fog, sun, ambient }: DaylightProps = {}): React.JSX.Element
291
+ - SkyDaylight (function): function SkyDaylight({ sky }: { sky: SkyEnvironmentDescriptor }): 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.
292
+ - TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock }: TimeOfDayDaylightProps): React.JSX.Element — Drives `Daylight`'s sun/sky/fog from the world clock when `sky.timeOfDay` and `clock` are both present, sampling `daylightStateAt` every frame; otherwise renders the static preset look via `SkyDaylight`.
293
+ - SkyDomeProps (interface): interface SkyDomeProps
294
+ - DaylightProps (interface): interface DaylightProps
295
+ - TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
296
+
297
+ ### @jgengine/shell/environment/daylightCycle
298
+
299
+ - lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
300
+ - 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.
301
+ - DaylightCycleConfig (interface): interface DaylightCycleConfig
302
+ - DaylightState (interface): interface DaylightState
303
+ - DEFAULT_DAY_SUN_INTENSITY (const): const DEFAULT_DAY_SUN_INTENSITY: 1
304
+ - DEFAULT_DAY_AMBIENT_INTENSITY (const): const DEFAULT_DAY_AMBIENT_INTENSITY: 0.6
305
+ - DEFAULT_DAY_SKY_TOP (const): const DEFAULT_DAY_SKY_TOP: "#3fa4f2"
306
+ - DEFAULT_DAY_SKY_BOTTOM (const): const DEFAULT_DAY_SKY_BOTTOM: "#e3f4ff"
307
+ - SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
308
+
309
+ ### @jgengine/shell/environment/EnvironmentScene
310
+
311
+ - EnvironmentScene (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element
312
+ - EnvironmentSceneProps (interface): interface EnvironmentSceneProps
313
+
314
+ ### @jgengine/shell/environment/GroundPad
315
+
316
+ - GroundPad (function): function GroundPad({ pad, field }: GroundPadProps): React.JSX.Element
317
+ - GroundPadProps (interface): interface GroundPadProps
318
+
319
+ ### @jgengine/shell/environment/groundPadMath
320
+
321
+ - resolvePadShape (function): function resolvePadShape(size: PadSize): PadShape
322
+ - resolvePadSurfaceY (function): function resolvePadSurfaceY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height">): number
323
+ - resolvePadMeshY (function): function resolvePadMeshY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height">): number
324
+ - PAD_THICKNESS (const): const PAD_THICKNESS: 0.1
325
+ - PadShape (type): type PadShape = { circular: true; radius: number } | { circular: false; width: number; depth: number }
326
+
327
+ ### @jgengine/shell/environment/index
328
+
329
+ - Daylight (function): function Daylight({ sky, fog, sun, ambient }: DaylightProps = {}): React.JSX.Element
330
+ - SkyDaylight (function): function SkyDaylight({ sky }: { sky: SkyEnvironmentDescriptor }): 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.
331
+ - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
332
+ - TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock }: TimeOfDayDaylightProps): React.JSX.Element — Drives `Daylight`'s sun/sky/fog from the world clock when `sky.timeOfDay` and `clock` are both present, sampling `daylightStateAt` every frame; otherwise renders the static preset look via `SkyDaylight`.
333
+ - DaylightProps (interface): interface DaylightProps
334
+ - SkyDomeProps (interface): interface SkyDomeProps
335
+ - TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
336
+ - EnvironmentScene (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element
337
+ - EnvironmentSceneProps (interface): interface EnvironmentSceneProps
338
+ - 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.
339
+ - lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
340
+ - SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
341
+ - DaylightCycleConfig (interface): interface DaylightCycleConfig
342
+ - DaylightState (interface): interface DaylightState
343
+
344
+ ### @jgengine/shell/GameHost
345
+
346
+ - GameHost (function): function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer }: GameHostProps): React.JSX.Element
347
+ - GameHostProps (interface): interface GameHostProps
348
+
349
+ ### @jgengine/shell/GamePlayer
350
+
351
+ - GamePlayer (function): function GamePlayer({ gameId, registry, fallbackGameId, loading = null, multiplayer = null }: GamePlayerProps): React.JSX.Element
352
+ - GamePlayerProps (type): type GamePlayerProps = { gameId: string; registry: GameRegistry; fallbackGameId?: string; loading?: ReactNode; multiplayer?: ShellMultiplayer | null; }
353
+
354
+ ### @jgengine/shell/GamePlayerShell
355
+
356
+ - 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.
357
+ - 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`).
358
+ - 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`).
359
+ - 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).
360
+ - 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).
361
+ - 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).
362
+ - 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.
363
+ - 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.
364
+ - GamePlayerShell (function): function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, }: { playable: PlayableGame; multiplayer?: ShellMultiplayer | null; }): React.JSX.Element
365
+
366
+ ### @jgengine/shell/GameUiPreview
367
+
368
+ - GameUiPreview (function): function GameUiPreview({ playable, scenario = defaultUiScenario, }: { playable: PlayableGame; scenario?: UiPreviewScenario; }): React.JSX.Element
369
+ - UiPreviewScenario (type): type UiPreviewScenario = (ctx: GameContext, playable: PlayableGame) => void
370
+ - defaultUiScenario (const): const defaultUiScenario: UiPreviewScenario
371
+
372
+ ### @jgengine/shell/map/index
373
+
374
+ - 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.
375
+ - BakedMap (interface): interface BakedMap
376
+ - BakeTerrainMapOptions (interface): interface BakeTerrainMapOptions
377
+ - MapBakeBounds (interface): interface MapBakeBounds
378
+ - 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`.
379
+ - MapMarkerBeaconsProps (interface): interface MapMarkerBeaconsProps
380
+
381
+ ### @jgengine/shell/map/MapMarkerBeacons
382
+
383
+ - 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`.
384
+ - MapMarkerBeaconsProps (interface): interface MapMarkerBeaconsProps
385
+
386
+ ### @jgengine/shell/map/terrainMap
387
+
388
+ - 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.
389
+ - MapBakeBounds (interface): interface MapBakeBounds
390
+ - BakeTerrainMapOptions (interface): interface BakeTerrainMapOptions
391
+ - BakedMap (interface): interface BakedMap
392
+
393
+ ### @jgengine/shell/materialOverride
394
+
395
+ - applyMaterialOverride (function): function applyMaterialOverride(root: THREE.Object3D, override: ModelMaterialOverride): void — Clones each `MeshStandardMaterial` under `root` and applies `override`'s color/finish onto the clone, so shared GLTF-cache scenes are never mutated in place (#151.3).
396
+
397
+ ### @jgengine/shell/multiplayer
398
+
399
+ - randomPlayerId (function): function randomPlayerId(): string
400
+ - resolveShellMultiplayer (function): function resolveShellMultiplayer(args: ResolveShellMultiplayerArgs): ShellMultiplayer | null
401
+ - resolvePeerShellMultiplayer (function): function resolvePeerShellMultiplayer(args: { gameId: string; role: "host" | "join"; room?: string; userId?: string; feedActions?: string[]; }): Promise<ShellMultiplayer & { close: () => void }>
402
+ - ShellMultiplayer (type): type ShellMultiplayer = MultiplayerSession
403
+ - DEFAULT_FEED_ACTIONS (const): const DEFAULT_FEED_ACTIONS: string[]
404
+ - ResolveShellMultiplayerArgs (type): type ResolveShellMultiplayerArgs = { game: GameDefinition; gameId: string; url?: string; userId?: string; force?: boolean; feedActions?: string[]; }
405
+
406
+ ### @jgengine/shell/pointer/PointerOverlays
407
+
408
+ - MarqueeBox (function): function MarqueeBox({ rect }: { rect: ScreenRect }): React.JSX.Element
409
+ - ContextMenuView (function): function ContextMenuView({ menu, x, y, onPick, onClose, }: { menu: ContextMenu; x: number; y: number; onPick: (verb: ContextVerb) => void; onClose: () => void; }): React.JSX.Element
410
+
411
+ ### @jgengine/shell/pointer/PointerProbe
412
+
413
+ - PointerProbe (function): function PointerProbe({ service }: { service: PointerService }): null
414
+
415
+ ### @jgengine/shell/pointer/pointerService
416
+
417
+ - createPointerService (function): function createPointerService(): PointerService
418
+ - POINTER_ENTITY_KEY (const): const POINTER_ENTITY_KEY: "jgEntityId"
419
+ - POINTER_OBJECT_KEY (const): const POINTER_OBJECT_KEY: "jgObjectId"
420
+ - PointerService (interface): interface PointerService
421
+
422
+ ### @jgengine/shell/registry
423
+
424
+ - resolveGameLoader (function): function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined
425
+ - RenderEntity (type): type RenderEntity = (entity: SceneEntity) => ReactNode
426
+ - RenderObject (type): type RenderObject = (object: SceneObject) => ReactNode
427
+ - PlayableGame (type): type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity, RenderObject>
428
+ - GameRegistry (type): type GameRegistry = Record<string, () => Promise<PlayableGame>>
429
+
430
+ ### @jgengine/shell/render/modelRender
431
+
432
+ - cloneModelScene (function): function cloneModelScene(source: THREE.Object3D): THREE.Object3D
433
+ - standardMaterialsOf (function): function standardMaterialsOf(root: THREE.Object3D): THREE.MeshStandardMaterial[]
434
+ - createPaintCanvas (function): function createPaintCanvas(seed: THREE.MeshStandardMaterial, size = PAINT_TEXTURE_SIZE): PaintCanvas
435
+ - drawPaintStrokes (function): function drawPaintStrokes(paint: PaintCanvas, strokes: readonly PaintStroke[]): void
436
+ - applyPaintTexture (function): function applyPaintTexture(root: THREE.Object3D, paint: PaintCanvas): void
437
+ - syncPaintCanvas (function): function syncPaintCanvas(paint: PaintCanvas, seedColor: THREE.Color, strokes: readonly PaintStroke[], drawnCount: number): number
438
+ - PAINT_TEXTURE_SIZE (const): const PAINT_TEXTURE_SIZE: 512
439
+ - PaintCanvas (interface): interface PaintCanvas
440
+
441
+ ### @jgengine/shell/replay/useSessionRecorder
442
+
443
+ - 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.
444
+ - RecordedPose (interface): interface RecordedPose
445
+
446
+ ### @jgengine/shell/structures
447
+
448
+ - BuildingBlock (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element
449
+ - GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
450
+ - BuildingBlockProps (interface): interface BuildingBlockProps
451
+ - GeneratedBuildingProps (interface): interface GeneratedBuildingProps
452
+ - PlacementGhost (function): function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }: PlacementGhostProps): React.JSX.Element | null
453
+ - PlacementGhostProps (interface): interface PlacementGhostProps
454
+
455
+ ### @jgengine/shell/structures/GeneratedBuilding
456
+
457
+ - BuildingBlock (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element
458
+ - GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
459
+ - BuildingFacade (type): type BuildingFacade = "front" | "back" | "left" | "right" | "roof"
460
+ - BuildingPartKind (type): type BuildingPartKind = | "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner"
461
+ - BuildingPartPlacement (interface): interface BuildingPartPlacement
462
+ - GeneratedBuildingData (interface): interface GeneratedBuildingData
463
+ - BuildingMaterialPalette (interface): interface BuildingMaterialPalette
464
+ - BuildingKitRenderer (interface): interface BuildingKitRenderer
465
+ - BuildingBlockProps (interface): interface BuildingBlockProps
466
+ - GeneratedBuildingProps (interface): interface GeneratedBuildingProps
467
+
468
+ ### @jgengine/shell/structures/index
469
+
470
+ - BuildingBlock (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element
471
+ - GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
472
+ - BuildingBlockProps (interface): interface BuildingBlockProps
473
+ - GeneratedBuildingProps (interface): interface GeneratedBuildingProps
474
+ - PlacementGhost (function): function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }: PlacementGhostProps): React.JSX.Element | null
475
+ - PlacementGhostProps (interface): interface PlacementGhostProps
476
+
477
+ ### @jgengine/shell/structures/PlacementGhost
478
+
479
+ - PlacementGhost (function): function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }: PlacementGhostProps): React.JSX.Element | null
480
+ - PlacementGhostProps (interface): interface PlacementGhostProps
481
+
482
+ ### @jgengine/shell/terrain
483
+
484
+ - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, 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.
485
+ - CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
486
+ - GrassField (function): function GrassField({ count = 6000, density = 1, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = false, ...meshProps }: GrassFieldProps): React.JSX.Element
487
+ - GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
488
+ - ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
489
+ - ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
490
+ - EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
491
+ - EditableGroundProps (interface): interface EditableGroundProps
492
+ - TerraformBrushCursor (function): function TerraformBrushCursor({ center, y = 0.05, radius, mode }: TerraformBrushCursorProps): React.JSX.Element | null
493
+ - TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
494
+ - createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
495
+ - resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
496
+ - resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
497
+ - GrassBladeGeometryOptions (interface): interface GrassBladeGeometryOptions
498
+ - GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
499
+ - ResolvedGrassBladeGeometryOptions (interface): interface ResolvedGrassBladeGeometryOptions
500
+ - createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
501
+ - DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
502
+ - resolveGrassWind (function): function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>
503
+ - GrassMaterialHandle (interface): interface GrassMaterialHandle
504
+ - GrassMaterialOptions (interface): interface GrassMaterialOptions
505
+ - GrassShaderUniforms (interface): interface GrassShaderUniforms
506
+ - GrassWindOptions (interface): interface GrassWindOptions
507
+ - createSeededRandom (function): function createSeededRandom(seed: TerrainSeed = 1): () => number
508
+ - hashNoise2 (function): function hashNoise2(x: number, z: number, seed: TerrainSeed = 1): number
509
+ - seedToUint32 (function): function seedToUint32(seed: TerrainSeed = 1): number
510
+ - TerrainSeed (type): type TerrainSeed = number | string
511
+ - 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).
512
+ - createProceduralGroundGeometry (function): function createProceduralGroundGeometry(config: ProceduralTerrainConfig = {}, colors: TerrainVertexColorOptions = {}): THREE.BufferGeometry
513
+ - createProceduralTerrainSampler (function): function createProceduralTerrainSampler(config: ProceduralTerrainConfig = {}): TerrainHeightSampler
514
+ - normalizeHeightBlend (function): function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number
515
+ - resolveTerrainSegments (function): function resolveTerrainSegments(segments: ProceduralTerrainConfig["segments"] = 96): ResolvedTerrainSegments
516
+ - resolveTerrainSize (function): function resolveTerrainSize(size: TerrainArea = 40): ResolvedTerrainSize
517
+ - toNoiseFieldConfig (function): function toNoiseFieldConfig(config: ProceduralTerrainConfig = {}): NoiseFieldConfig
518
+ - FieldGroundOptions (interface): interface FieldGroundOptions
519
+ - ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
520
+ - ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
521
+ - ResolvedTerrainSize (interface): interface ResolvedTerrainSize
522
+ - TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
523
+ - TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
524
+ - TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
525
+
526
+ ### @jgengine/shell/terrain/CarvedTerrain
527
+
528
+ - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, 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.
529
+ - CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
530
+
531
+ ### @jgengine/shell/terrain/EditableGround
532
+
533
+ - EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
534
+ - EditableGroundProps (interface): interface EditableGroundProps
535
+
536
+ ### @jgengine/shell/terrain/GrassField
537
+
538
+ - GrassField (function): function GrassField({ count = 6000, density = 1, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = false, ...meshProps }: GrassFieldProps): React.JSX.Element
539
+ - GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
540
+
541
+ ### @jgengine/shell/terrain/grassGeometry
542
+
543
+ - resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
544
+ - resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
545
+ - createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
546
+ - GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
547
+ - GrassBladeGeometryOptions (interface): interface GrassBladeGeometryOptions
548
+ - ResolvedGrassBladeGeometryOptions (interface): interface ResolvedGrassBladeGeometryOptions
549
+
550
+ ### @jgengine/shell/terrain/grassMaterial
551
+
552
+ - resolveGrassWind (function): function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>
553
+ - createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
554
+ - GrassWindOptions (interface): interface GrassWindOptions
555
+ - GrassMaterialOptions (interface): interface GrassMaterialOptions
556
+ - GrassShaderUniforms (interface): interface GrassShaderUniforms
557
+ - GrassMaterialHandle (interface): interface GrassMaterialHandle
558
+ - DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
559
+
560
+ ### @jgengine/shell/terrain/index
561
+
562
+ - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, 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.
563
+ - CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
564
+ - GrassField (function): function GrassField({ count = 6000, density = 1, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = false, ...meshProps }: GrassFieldProps): React.JSX.Element
565
+ - GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
566
+ - ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
567
+ - ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
568
+ - EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
569
+ - EditableGroundProps (interface): interface EditableGroundProps
570
+ - TerraformBrushCursor (function): function TerraformBrushCursor({ center, y = 0.05, radius, mode }: TerraformBrushCursorProps): React.JSX.Element | null
571
+ - TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
572
+ - createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
573
+ - resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
574
+ - resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
575
+ - GrassBladeGeometryOptions (interface): interface GrassBladeGeometryOptions
576
+ - GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
577
+ - ResolvedGrassBladeGeometryOptions (interface): interface ResolvedGrassBladeGeometryOptions
578
+ - createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
579
+ - DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
580
+ - resolveGrassWind (function): function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>
581
+ - GrassMaterialHandle (interface): interface GrassMaterialHandle
582
+ - GrassMaterialOptions (interface): interface GrassMaterialOptions
583
+ - GrassShaderUniforms (interface): interface GrassShaderUniforms
584
+ - GrassWindOptions (interface): interface GrassWindOptions
585
+ - createSeededRandom (function): function createSeededRandom(seed: TerrainSeed = 1): () => number
586
+ - hashNoise2 (function): function hashNoise2(x: number, z: number, seed: TerrainSeed = 1): number
587
+ - seedToUint32 (function): function seedToUint32(seed: TerrainSeed = 1): number
588
+ - TerrainSeed (type): type TerrainSeed = number | string
589
+ - 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).
590
+ - createProceduralGroundGeometry (function): function createProceduralGroundGeometry(config: ProceduralTerrainConfig = {}, colors: TerrainVertexColorOptions = {}): THREE.BufferGeometry
591
+ - createProceduralTerrainSampler (function): function createProceduralTerrainSampler(config: ProceduralTerrainConfig = {}): TerrainHeightSampler
592
+ - normalizeHeightBlend (function): function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number
593
+ - resolveTerrainSegments (function): function resolveTerrainSegments(segments: ProceduralTerrainConfig["segments"] = 96): ResolvedTerrainSegments
594
+ - resolveTerrainSize (function): function resolveTerrainSize(size: TerrainArea = 40): ResolvedTerrainSize
595
+ - toNoiseFieldConfig (function): function toNoiseFieldConfig(config: ProceduralTerrainConfig = {}): NoiseFieldConfig
596
+ - FieldGroundOptions (interface): interface FieldGroundOptions
597
+ - ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
598
+ - ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
599
+ - ResolvedTerrainSize (interface): interface ResolvedTerrainSize
600
+ - TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
601
+ - TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
602
+ - TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
603
+
604
+ ### @jgengine/shell/terrain/ProceduralGround
605
+
606
+ - ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
607
+ - ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
608
+
609
+ ### @jgengine/shell/terrain/random
610
+
611
+ - seedToUint32 (function): function seedToUint32(seed: TerrainSeed = 1): number
612
+ - createSeededRandom (function): function createSeededRandom(seed: TerrainSeed = 1): () => number
613
+ - hashNoise2 (function): function hashNoise2(x: number, z: number, seed: TerrainSeed = 1): number
614
+ - TerrainSeed (type): type TerrainSeed = number | string
615
+
616
+ ### @jgengine/shell/terrain/TerraformBrushCursor
617
+
618
+ - TerraformBrushCursor (function): function TerraformBrushCursor({ center, y = 0.05, radius, mode }: TerraformBrushCursorProps): React.JSX.Element | null
619
+ - TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
620
+
621
+ ### @jgengine/shell/terrain/terrainMath
622
+
623
+ - normalizeHeightBlend (function): function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number
624
+ - resolveTerrainSize (function): function resolveTerrainSize(size: TerrainArea = 40): ResolvedTerrainSize
625
+ - resolveTerrainSegments (function): function resolveTerrainSegments(segments: ProceduralTerrainConfig["segments"] = 96): ResolvedTerrainSegments
626
+ - toNoiseFieldConfig (function): function toNoiseFieldConfig(config: ProceduralTerrainConfig = {}): NoiseFieldConfig
627
+ - createProceduralTerrainSampler (function): function createProceduralTerrainSampler(config: ProceduralTerrainConfig = {}): TerrainHeightSampler
628
+ - 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).
629
+ - createProceduralGroundGeometry (function): function createProceduralGroundGeometry(config: ProceduralTerrainConfig = {}, colors: TerrainVertexColorOptions = {}): THREE.BufferGeometry
630
+ - TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
631
+ - TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
632
+ - ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
633
+ - ResolvedTerrainSize (interface): interface ResolvedTerrainSize
634
+ - ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
635
+ - TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
636
+ - FieldGroundOptions (interface): interface FieldGroundOptions
637
+
638
+ ### @jgengine/shell/touch/TouchControlsOverlay
639
+
640
+ - 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
641
+ - TouchControlsDock (function): function TouchControlsDock({ scheme, sink }: { scheme: TouchScheme; sink: TouchCodeSink }): React.JSX.Element
642
+ - TouchCodeSink (interface): interface TouchCodeSink
643
+
644
+ ### @jgengine/shell/vision/FrustumSensorHud
645
+
646
+ - useFrustumSensor (function): function useFrustumSensor(options: FrustumSensorProbeOptions): FrustumSample | null — View-frustum sensor (#117) driven by the live render camera: which held-camera subjects are in frame, how well framed, and their dwell time on-screen (Content Warning-style "is this monster filmed well" scoring). Returns the best-framed in-view subject each frame for a photo-mode HUD readout.
647
+ - 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.
648
+ - FrustumSensorProbeOptions (interface): interface FrustumSensorProbeOptions extends FramingConfig
649
+
650
+ ### @jgengine/shell/vision/HiddenStateProbeHud
651
+
652
+ - 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).
653
+ - 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.
654
+ - SensorReadoutMeterProps (interface): interface SensorReadoutMeterProps
655
+
656
+ ### @jgengine/shell/vision/RevealVision
657
+
658
+ - useRevealHits (function): function useRevealHits(options: RevealVisionOptions): readonly RevealHit[] — Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene.
659
+ - 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.
660
+ - 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).
661
+ - RevealVisionOptions (interface): interface RevealVisionOptions
662
+ - RevealHighlightsProps (interface): interface RevealHighlightsProps extends RevealVisionOptions
663
+ - RevealScreenTintProps (interface): interface RevealScreenTintProps
664
+
665
+ ### @jgengine/shell/water
666
+
667
+ - Ocean (function): function Ocean({ config, ...meshProps }: OceanProps): React.JSX.Element
668
+ - OceanProps (interface): interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
669
+ - DEFAULT_OCEAN_CONFIG (const): const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig
670
+ - MAX_OCEAN_WAVES (const): const MAX_OCEAN_WAVES: 6
671
+ - OCEAN_QUALITY_PRESETS (const): const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, { size: number; resolution: number }>
672
+ - buildOceanWaveUniforms (function): function buildOceanWaveUniforms(config: ResolvedOceanConfig): { directions: THREE.Vector2[]; params: THREE.Vector4[]; }
673
+ - createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
674
+ - OceanColorConfig (interface): interface OceanColorConfig
675
+ - OceanConfig (interface): interface OceanConfig
676
+ - OceanDirectionVector (interface): interface OceanDirectionVector
677
+ - OceanFoamConfig (interface): interface OceanFoamConfig
678
+ - OceanQualityPreset (type): type OceanQualityPreset = "low" | "medium" | "high" | "ultra"
679
+ - OceanWaveConfig (interface): interface OceanWaveConfig
680
+ - OceanWaveDirection (type): type OceanWaveDirection = number | OceanDirectionVector
681
+ - ResolvedOceanColorConfig (interface): interface ResolvedOceanColorConfig
682
+ - ResolvedOceanConfig (interface): interface ResolvedOceanConfig
683
+ - ResolvedOceanFoamConfig (interface): interface ResolvedOceanFoamConfig
684
+ - ResolvedOceanWaveConfig (interface): interface ResolvedOceanWaveConfig
685
+ - createOceanMaterial (function): function createOceanMaterial(config: ResolvedOceanConfig): OceanShaderMaterial
686
+ - syncOceanMaterial (function): function syncOceanMaterial(material: OceanShaderMaterial, config: ResolvedOceanConfig, elapsedSeconds: number): void
687
+ - OceanMaterialUniforms (interface): interface OceanMaterialUniforms
688
+ - OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
689
+
690
+ ### @jgengine/shell/water/index
691
+
692
+ - Ocean (function): function Ocean({ config, ...meshProps }: OceanProps): React.JSX.Element
693
+ - OceanProps (interface): interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
694
+ - DEFAULT_OCEAN_CONFIG (const): const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig
695
+ - MAX_OCEAN_WAVES (const): const MAX_OCEAN_WAVES: 6
696
+ - OCEAN_QUALITY_PRESETS (const): const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, { size: number; resolution: number }>
697
+ - buildOceanWaveUniforms (function): function buildOceanWaveUniforms(config: ResolvedOceanConfig): { directions: THREE.Vector2[]; params: THREE.Vector4[]; }
698
+ - createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
699
+ - OceanColorConfig (interface): interface OceanColorConfig
700
+ - OceanConfig (interface): interface OceanConfig
701
+ - OceanDirectionVector (interface): interface OceanDirectionVector
702
+ - OceanFoamConfig (interface): interface OceanFoamConfig
703
+ - OceanQualityPreset (type): type OceanQualityPreset = "low" | "medium" | "high" | "ultra"
704
+ - OceanWaveConfig (interface): interface OceanWaveConfig
705
+ - OceanWaveDirection (type): type OceanWaveDirection = number | OceanDirectionVector
706
+ - ResolvedOceanColorConfig (interface): interface ResolvedOceanColorConfig
707
+ - ResolvedOceanConfig (interface): interface ResolvedOceanConfig
708
+ - ResolvedOceanFoamConfig (interface): interface ResolvedOceanFoamConfig
709
+ - ResolvedOceanWaveConfig (interface): interface ResolvedOceanWaveConfig
710
+ - createOceanMaterial (function): function createOceanMaterial(config: ResolvedOceanConfig): OceanShaderMaterial
711
+ - syncOceanMaterial (function): function syncOceanMaterial(material: OceanShaderMaterial, config: ResolvedOceanConfig, elapsedSeconds: number): void
712
+ - OceanMaterialUniforms (interface): interface OceanMaterialUniforms
713
+ - OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
714
+
715
+ ### @jgengine/shell/water/Ocean
716
+
717
+ - Ocean (function): function Ocean({ config, ...meshProps }: OceanProps): React.JSX.Element
718
+ - OceanProps (interface): interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
719
+
720
+ ### @jgengine/shell/water/OceanConfig
721
+
722
+ - createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
723
+ - buildOceanWaveUniforms (function): function buildOceanWaveUniforms(config: ResolvedOceanConfig): { directions: THREE.Vector2[]; params: THREE.Vector4[]; }
724
+ - MAX_OCEAN_WAVES (const): const MAX_OCEAN_WAVES: 6
725
+ - OceanQualityPreset (type): type OceanQualityPreset = "low" | "medium" | "high" | "ultra"
726
+ - OceanDirectionVector (interface): interface OceanDirectionVector
727
+ - OceanWaveDirection (type): type OceanWaveDirection = number | OceanDirectionVector
728
+ - OceanWaveConfig (interface): interface OceanWaveConfig
729
+ - OceanColorConfig (interface): interface OceanColorConfig
730
+ - OceanFoamConfig (interface): interface OceanFoamConfig
731
+ - OceanConfig (interface): interface OceanConfig
732
+ - ResolvedOceanColorConfig (interface): interface ResolvedOceanColorConfig
733
+ - ResolvedOceanFoamConfig (interface): interface ResolvedOceanFoamConfig
734
+ - ResolvedOceanWaveConfig (interface): interface ResolvedOceanWaveConfig
735
+ - ResolvedOceanConfig (interface): interface ResolvedOceanConfig
736
+ - OCEAN_QUALITY_PRESETS (const): const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, { size: number; resolution: number }>
737
+ - DEFAULT_OCEAN_CONFIG (const): const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig
738
+
739
+ ### @jgengine/shell/water/OceanMaterial
740
+
741
+ - createOceanMaterial (function): function createOceanMaterial(config: ResolvedOceanConfig): OceanShaderMaterial
742
+ - syncOceanMaterial (function): function syncOceanMaterial(material: OceanShaderMaterial, config: ResolvedOceanConfig, elapsedSeconds: number): void
743
+ - OceanMaterialUniforms (interface): interface OceanMaterialUniforms
744
+ - OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
745
+
746
+ ### @jgengine/shell/water/OceanShader
747
+
748
+ - 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…
749
+ - 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…
750
+
751
+ ### @jgengine/shell/weather/FireSpreadLayer
752
+
753
+ - FireSpreadLayer (function): function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }: FireSpreadLayerProps): React.JSX.Element
754
+ - FireSpreadLayerProps (interface): interface FireSpreadLayerProps
755
+
756
+ ### @jgengine/shell/weather/index
757
+
758
+ - FireSpreadLayer (function): function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }: FireSpreadLayerProps): React.JSX.Element
759
+ - FireSpreadLayerProps (interface): interface FireSpreadLayerProps
760
+ - 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
761
+ - LightningStrikeProps (interface): interface LightningStrikeProps
762
+ - RainField (function): function RainField({ count = 8000, density = 0.45, 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 = 10, }: RainFieldProps): React.JSX.El…
763
+ - RainFieldProps (interface): interface RainFieldProps
764
+ - SnowField (function): function SnowField({ count = 6000, density = 0.5, 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, }: SnowFieldProps): React.JSX.Element
765
+ - SnowFieldProps (interface): interface SnowFieldProps
766
+ - WeatherLayer (function): function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, rain, snow, enabled = true, children, }: WeatherLayerProps): React.JSX.Element | null
767
+ - WeatherLayerMode (type): type WeatherLayerMode = "clear" | "rain" | "snow" | "mixed"
768
+ - WeatherLayerProps (interface): interface WeatherLayerProps
769
+ - createWeatherUniformSet (function): function createWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
770
+ - WeatherUniformProvider (function): function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & { children: ReactNode }): React.JSX.Element
771
+ - useWeatherUniformSet (function): function useWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
772
+ - WeatherUniformOptions (interface): interface WeatherUniformOptions
773
+ - WeatherUniformSet (interface): interface WeatherUniformSet
774
+ - WeatherVector (type): type WeatherVector = readonly [number, number, number]
775
+
776
+ ### @jgengine/shell/weather/LightningStrike
777
+
778
+ - 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
779
+ - LightningStrikeProps (interface): interface LightningStrikeProps
780
+
781
+ ### @jgengine/shell/weather/RainField
782
+
783
+ - RainField (function): function RainField({ count = 8000, density = 0.45, 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 = 10, }: RainFieldProps): React.JSX.El…
784
+ - RainFieldProps (interface): interface RainFieldProps
785
+
786
+ ### @jgengine/shell/weather/SnowField
787
+
788
+ - SnowField (function): function SnowField({ count = 6000, density = 0.5, 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, }: SnowFieldProps): React.JSX.Element
789
+ - SnowFieldProps (interface): interface SnowFieldProps
790
+
791
+ ### @jgengine/shell/weather/weatherGeometry
792
+
793
+ - createWeatherQuadGeometry (function): function createWeatherQuadGeometry(maxCount: number, seed: number): THREE.InstancedBufferGeometry
794
+
795
+ ### @jgengine/shell/weather/WeatherLayer
796
+
797
+ - WeatherLayer (function): function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, rain, snow, enabled = true, children, }: WeatherLayerProps): React.JSX.Element | null
798
+ - WeatherLayerMode (type): type WeatherLayerMode = "clear" | "rain" | "snow" | "mixed"
799
+ - WeatherLayerProps (interface): interface WeatherLayerProps
800
+
801
+ ### @jgengine/shell/weather/weatherMath
802
+
803
+ - clampWeatherRatio (function): function clampWeatherRatio(value: number): number
804
+ - resolveWeatherInstanceCount (function): function resolveWeatherInstanceCount(maxCount: number, density: number): number
805
+ - createWeatherSeedAttributes (function): function createWeatherSeedAttributes(maxCount: number, seed: number): WeatherSeedAttributes
806
+ - WeatherSeedAttributes (interface): interface WeatherSeedAttributes
807
+
808
+ ### @jgengine/shell/weather/weatherUniforms
809
+
810
+ - createWeatherUniformSet (function): function createWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
811
+ - WeatherUniformProvider (function): function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & { children: ReactNode }): React.JSX.Element
812
+ - useWeatherUniformSet (function): function useWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
813
+ - WeatherVector (type): type WeatherVector = readonly [number, number, number]
814
+ - WeatherUniformSet (interface): interface WeatherUniformSet
815
+ - WeatherUniformOptions (interface): interface WeatherUniformOptions
816
+
817
+ ### @jgengine/shell/world/DataObjects
818
+
819
+ - 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
820
+ - 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.
821
+
822
+ ### @jgengine/shell/world/floatTextStyle
823
+
824
+ - resolveFloatTextStyle (function): function resolveFloatTextStyle(info: FloatTextInfo): FloatTextStyle
825
+ - FloatTextInfo (interface): interface FloatTextInfo
826
+ - FloatTextStyle (interface): interface FloatTextStyle
827
+
828
+ ### @jgengine/shell/world/GridWorldScene
829
+
830
+ - 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`.
831
+ - GridWorldSceneProps (interface): interface GridWorldSceneProps
832
+
833
+ ### @jgengine/shell/world/InstancedBodies
834
+
835
+ - 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.
836
+ - InstancedBodiesProps (interface): interface InstancedBodiesProps
837
+
838
+ ### @jgengine/shell/world/InstancedJoints
839
+
840
+ - 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.
841
+ - InstancedJointsProps (interface): interface InstancedJointsProps
842
+
843
+ ### @jgengine/shell/world/WorldHud
844
+
845
+ - 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
846
+ - WorldFloatText (function): function WorldFloatText({ height = 1.9, lifeMs = 950 }: { height?: number; lifeMs?: number }): React.JSX.Element
847
+ - WorldTelegraphs (function): function WorldTelegraphs(): React.JSX.Element
848
+ - CombatCameraShake (function): function CombatCameraShake(): null
849
+ - ProjectileTracers (function): function ProjectileTracers({ lifeMs = 130 }: { lifeMs?: number }): React.JSX.Element
850
+ - Reticle (function): function Reticle({ className }: { className?: string }): React.JSX.Element
851
+
852
+ ### @jgengine/shell/world/WorldItems
853
+
854
+ - 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`.
855
+
856
+ ## Guides
857
+
858
+ # JGengine — API Reference
859
+
860
+ The engine ships **verbs and primitives**; your game ships **nouns** (catalogs) and thin handlers. Read this before writing `game.config.ts` or any game content. Companion skills: **`jgengine-newgame`** (master blueprint + phased build to completion) and **`jgengine-verify`** (browserless scene gate) — read them before building. The UI quality bar lives in [`reference/ui-react.md`](reference/ui-react.md); asset sourcing lives in the **Assets** section below.
861
+
862
+ The heaviest domains live in on-demand reference modules under [`reference/`](reference/) — load one only when you're building in that domain: [`reference/combat.md`](reference/combat.md) (effects, projectiles, death, feel, abilities), [`reference/world.md`](reference/world.md) (terrain, environment, physics, vehicles, spawn), [`reference/multiplayer.md`](reference/multiplayer.md) (transport, host, persistence, presence), [`reference/ui-react.md`](reference/ui-react.md) (`@jgengine/react` hooks, headless + styled UI kits, the registry install path, and the UI quality bar). Each domain's section below is a one-line pointer to its module.
863
+
864
+ ## Packages
865
+
866
+ All published on npm, source at [github.com/Noisemaker111/jgengine](https://github.com/Noisemaker111/jgengine) (AGPL-3.0):
867
+
868
+ | Package | Role | May import |
869
+ |---------|------|------------|
870
+ | `@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 |
871
+ | `@jgengine/react` | `GameProvider`, hooks, headless UI primitives | react + core |
872
+ | `@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 |
873
+ | `@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 |
874
+ | `@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 |
875
+ | `@jgengine/sql` | `HostPersistence` on Postgres (structural pool, no hard `pg` dep) | core |
876
+ | `@jgengine/convex` | The Convex **adapter** behind the `GameBackend` seam | react + convex + core |
877
+
878
+ Import by deep path: `@jgengine/core/<domain>/<file>` (e.g. `@jgengine/core/runtime/gameContext`).
879
+
880
+ ## Hit a snag? File an issue
881
+
882
+ 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.
883
+
884
+ ## Upgrading? Read the changelog
885
+
886
+ 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.
887
+
888
+ ## Concept → Type Reference
889
+
890
+ 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>`.
891
+
892
+ | Concept | Import path (`@jgengine/core/…`) | Export(s) |
893
+ |---------|----------------------------------|-----------|
894
+ | Game boot | `game/defineGame` | `defineGame`, `GameDefinition`, `GameLoop`, `InventoryDeclaration`, `PhysicsConfig`, `GameServerConfig`, `TimeConfig` |
895
+ | Simulation clock | `time/simClock` | `createSimClock`, `SimClock`, `TimeConfig`, `ClockSnapshot`, `CalendarTime` |
896
+ | Runner contract | `game/playableGame` | `PlayableGame`, `GameCameraConfig`, `CameraRigKind`, `TopDownCameraConfig`, `RtsCameraConfig`, `ShoulderCameraConfig`, `LockOnCameraConfig`, `ChaseCameraConfig`, `ObserverCameraConfig`, `CameraShakeConfig`, `CinematicCameraConfig`, `CameraKeyframe`, `EntitySpriteConfig` |
897
+ | Runtime ctx | `runtime/gameContext` | `createGameContext`, `GameContext`, `GameContextContent`, `GameContextItemEntry`, `GameContextEntityEntry`, `GameContextObjectEntry`, `CatalogEntityRole` |
898
+ | 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` |
899
+ | Reactive keyed store | `store/observableKeyedStore` | `createObservableKeyedStore`, `ObservableKeyedStore` — backs `ctx.game.store` |
900
+ | Scene instance role | `scene/entityStore` | `EntityRole`, `SceneEntity`, `SpawnOptions`, `EntityPose` |
901
+ | Object spatial queries | `scene/objectQuery` | `raycastObjects`, `raycastObjectsAll`, `ObjectRaycastInput`, `ObjectRaycastHit` — backs `ctx.scene.object.raycast`/`raycastAll` |
902
+ | Runtime paint layer | `scene/paintLayer` | `createPaintLayer`, `PaintLayer`, `PaintStroke` — backs `ctx.scene.entity.paint` |
903
+ | Possession | `scene/possession` | `createPossession`, `Possession`, `PossessionDeps`, `PossessionSwappedEvent` |
904
+ | Form / shapeshift | `scene/form` | `createForms`, `Forms`, `FormDef`, `FormsDeps`, `FormChangedEvent` |
905
+ | Multiplayer adapters | `runtime/adapter` | `offline`, `ws`, `convex`, `socketIo`, `p2p`, `lan`, `fly`, `servers`, `MultiplayerTopology`, `ServersPoolConfig` |
906
+ | Loot | `game/lootTable` | `lootTable`, `LootTableDef`, `LootEntry`, `Drop` |
907
+ | 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` |
908
+ | Loot filter | `game/lootFilter` | `lootFilter`, `evaluateLootFilter`, `LootFilterRule`, `LootFilterCondition`, `LootFilterItem`, `LootFilterOverride` |
909
+ | Loadout | `game/loadout` | `LoadoutDef`, `LoadoutItemEntry`, `Loadouts` |
910
+ | Cosmetic loadout | `game/cosmetics` | `createCosmetics`, `Cosmetics`, `CosmeticLoadoutDef` |
911
+ | Quest | `game/quest` | `QuestDef`, `QuestRewards`, `QuestObjective`, `QuestJournal` |
912
+ | World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building` |
913
+ | 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 |
914
+ | Terrain field | `world/terrain` | `TerrainField`, `noiseField`, `resolveTerrainField`, `rollingField`, `fractalNoise`, `valueNoise`, `withNormal`, `arenaField`, `flatField`, `resolveGroundStep`, `snapToGround`, `snapEntityToGround`, `resolveTerrainPalette`, `TERRAIN_MATERIAL_PALETTES` |
915
+ | Seeded RNG | `random/rng` | `seededRng`, `seededStreams` |
916
+ | Name generator | `random/nameGen` | `createNameGenerator`, `pickFrom`, `fillTemplate`, `NameGenerator`, `NameGeneratorOptions`, `SyllableBank` |
917
+ | Regions | `world/regions` | `createRegionField`, `isRegionField`, `RegionDef`, `RegionField`, `RegionSample` |
918
+ | Wind field | `world/wind` | `windField`, `WindField`, `WindFieldConfig`, `WindVector` |
919
+ | Water surface | `world/water` | `waterSurface`, `waterSurfaceFromDescriptor`, `synthesizeWaves`, `WaterSurface`, `GerstnerWave` |
920
+ | Scatter | `world/scatter` | `scatter`, `scatterAabb`, `ScatterConfig`, `ScatterPoint` |
921
+ | Content scatter | `world/scatterItems` | `scatterItems`, `pickWeighted`, `ScatterLayer`, `ScatterInstance` |
922
+ | Building generator | `world/buildings` | `generateBuilding`, `generateBuildingDistrict`, `createBuildingGrid`, `GeneratedBuilding` |
923
+ | Building index | `world/buildingIndex` | `buildingIndex`, `BuildingIndex`, `BuildingHit` |
924
+ | Scene summary | `world/environmentSummary` | `summarizeEnvironment`, `resolveStructureBuildings`, `EnvironmentSummary` |
925
+ | Map markers | `world/markers` | `createMarkerSet`, `MarkerSet`, `MapMarker`, `MarkerInput`, `MarkerKindStyle`, `DEFAULT_MARKER_KINDS`, `markerKindStyle` |
926
+ | Fog of war | `world/fog` | `createFogField`, `FogField`, `FogConfig`, `FogBounds`, `FogCells` |
927
+ | Minimap math | `world/minimap` | `projectToMinimap`, `clampToMinimapEdge`, `compassBearing`, `headingToBearing`, `bearingToCardinal`, `relativeBearing`, `MinimapView` |
928
+ | Ping | `game/ping` | `createPingSystem`, `classifyPing`, `PingSystem`, `PingPayload`, `PingCategory`, `PingCategoryDef`, `DEFAULT_PING_CATEGORIES`, `PING_FEED_ACTION` |
929
+ | Proximity prompt | `interaction/proximityPrompt` | `proximityPrompt`, `ProximityPrompt`, `ProximityPromptDisplay`, `keybind`, `gauge`, `label`, `command` |
930
+ | Skill-check minigame | `interaction/skillCheck` | `evaluateSkillCheck`, `skillCheckMarkerPosition`, `skillCheckZoneAt`, `SkillCheckConfig`, `SkillCheckZone`, `SkillCheckResult` |
931
+ | QTE sequencer | `interaction/qte` | `evaluateQteSequence`, `pendingQteStep`, `qteProgress`, `QteStep`, `QteInputEvent`, `QteOutcome` |
932
+ | Item use | `item/use` | `createItemUse`, `ItemUseHandler`, `ItemUseInput`, `ItemUseResult`, `ItemUseRejection` |
933
+ | Durability | `item/durability` | `createDurability`, `wear`, `repairQuote`, `isDisabled`, `createDurabilityTracker`, `DurabilitySpec`, `DurabilityState`, `RepairSpec`, `RepairQuote` |
934
+ | Affix roller | `item/affix` | `createAffixRoller`, `seededRng`, `AffixRoller`, `RollerConfig`, `AffixPool`, `AffixDef`, `RarityTier`, `ItemBaseDef`, `RolledItem`, `RolledAffix` |
935
+ | Modular item | `item/modularItem` | `createModularItem`, `install`, `computeEffectiveStats`, `missingRequiredSlots`, `ModularItemDef`, `MountSlotDef`, `PartDef`, `InstalledPart` |
936
+ | Storage tier | `inventory/storageTier` | `partitionOnDeath`, `createDeliveryQueue`, `insureLost`, `resolveConsolation`, `tierOf`, `StorageTier`, `ContainerSnapshot`, `DeathPartition`, `DeliveryQueue`, `InsurancePolicy`, `ConsolationPolicy` |
937
+ | Contested channel | `session/contestedChannel` | `createContestedChannel`, `ContestedChannel`, `ContestedChannelConfig`, `ContestedEvent`, `ContestedPhase`, `ContestedSnapshot` |
938
+ | Round state | `session/roundState` | `createRoundState`, `lossBonusFor`, `RoundState`, `RoundConfig`, `RoundPhase`, `RoundTeam`, `RoundEvent`, `RoundEconomy`, `RoundSnapshot`, `LossBonusRule` |
939
+ | Shrinking ring | `session/ring` | `createRing`, `ringSampleAt`, `Ring`, `RingConfig`, `RingPhase`, `RingSample`, `RingHit`, `RingPoint` |
940
+ | Extraction session | `session/extraction` | `createRaidSession`, `RaidSession`, `RaidSessionConfig`, `ExtractPoint`, `ExtractionResult`, `DeathResult`, `RaidStatus` |
941
+ | Role assignment | `session/roles` | `assignRoles`, `RoleSpec` |
942
+ | Downed / revive | `combat/downed` | `createDownedState`, `DownedState`, `DownedConfig`, `DownedPhase`, `DownedEntry`, `DownedEvent` |
943
+ | Persistence scopes | `runtime/persistenceScope` | `partitionScopes`, `resetRun`, `mergeScopes`, `clearRunFields`, `applyRunReset`, `planScenarioReset`, `ScopeSchema`, `ScenarioReset`, `PersistenceScope` |
944
+ | Inventory | `inventory/inventoryModel` | `InventoryLayout`, `InventorySet`, `ItemTraits` |
945
+ | Progression | `game/progression` | `curve`, `evalCurve`, `leveling`, `Curve`, `LevelingTrack`, `LevelProgress` |
946
+ | Inventory slots | `inventory/slotModel` | `createSlots`, `placeAt`, `removeAt`, `moveSlot`, `firstEmpty`, `compactSlots`, `Slot`, `SlotGrid` |
947
+ | Shaped inventory | `inventory/shapedGrid` | `createShapedGrid`, `placeShaped`, `moveShaped`, `removeShaped`, `canPlace`, `rotateFootprint`, `occupiedCells`, `gridAdjacencyQuery`, `cellFromPoint`, `ShapedGrid`, `Footprint`, `Placement`, `Rotation` |
948
+ | Card piles | `cards/cardPile` | `createCardPile`, `createCardPileState`, `draw`, `moveCards`, `shuffleZone`, `pileRng`, `CardPile`, `CardPileState`, `CardPileConfig` |
949
+ | Modifier pipeline | `cards/modifierPipeline` | `createModifierPipeline`, `runPipeline`, `Modifier`, `TraceStep`, `PipelineResult` |
950
+ | Lane board | `board/laneBoard` | `createLaneBoard`, `laneAggregate`, `laneOutcome`, `boardTotals`, `lanesWon`, `LaneBoard`, `LaneRule`, `LaneBoardConfig` |
951
+ | Timeline board | `board/timelineBoard` | `createTimelineBoard`, `tickTimeline`, `TimelineBoard`, `TimelineSlot`, `TimelineFire` |
952
+ | World geometry | `world/geometry` | `footprintAabb`, `aabbOverlap`, `snapToGrid`, `resolveMove`, `Aabb`, `Footprint` |
953
+ | Placement | `world/placement` | `validatePlacement`, `footprintObstacle`, `PlacementRules`, `PlacementResult` |
954
+ | Placement ghost | `world/placementController` | `createPlacementController`, `PlacementController`, `PlacementPreview`, `PlacementCommit`, `SnapMode`, `quarterTurnsToRotationY` |
955
+ | Connector sockets | `world/connectors` | `snapToNearest`, `socketsCompatible`, `worldSockets`, `socketWorldPosition`, `ConnectorSocket`, `ConnectorPieceDef`, `PlacedPiece`, `SnapResult` |
956
+ | Structural support | `world/support` | `solveSupport`, `toDebrisBodies`, `SupportPiece`, `SupportLink`, `SupportResult` |
957
+ | Wall/roof authoring | `world/walls` | `createWallDrawTool`, `footprintFromWalls`, `autoRoof`, `wallSegments`, `createSurfacePaint`, `WallDrawTool`, `RoofPlan`, `EnclosedFootprint` |
958
+ | Placed structures | `world/placedStructureStore` | `createPlacedStructureStore`, `PlacedStructure`, `PlacedStructureStore`, `PlacedStructureSnapshot` |
959
+ | Terraform | `world/terraform` | `createEditableTerrain`, `createTerraformBrush`, `brushWeight`, `EditableTerrain`, `TerraformBrush`, `TerraformEdit`, `TerraformMode` |
960
+ | Build permissions | `world/buildPermissions` | `createPlotPermissions`, `createContributionPool`, `PlotPermissions`, `ContributionPool`, `BuildRole`, `ContributionGoal` |
961
+ | Interiors | `world/interiors` | `createInteriors`, `Interior`, `Exterior`, `SpaceRef` |
962
+ | Game clock | `time/gameClock` | `getScaledElapsedMs`, `computeGameDay`, `SECONDS_PER_GAME_DAY` |
963
+ | Scene behaviors | `scene/behaviors` | `wander`, `patrol`, `promptable`, `talkable`, `player` |
964
+ | Capture check | `scene/captureCheck` | `captureChance`, `rollCapture`, `CaptureCheckInput` |
965
+ | Owned roster | `scene/roster` | `createRoster`, `Roster`, `RosterEntry`, `RosterCaptureOptions` |
966
+ | Economy wallet | `economy/wallet` | `createEmptyWallet`, `balance`, `grant`, `charge`, `canAfford`, `chargeAll` |
967
+ | Tech tree | `economy/techTree` | `createTechTree`, `TechTree`, `TechNodeDef`, `canUnlockTech`, `availableTech`, `unlockedRecipes`, `grantTech`, `techPrerequisitesMet` |
968
+ | Recipe graph | `crafting/recipe` | `createRecipeGraph`, `RecipeGraph`, `RecipeDef`, `RecipeItem`, `canCraft`, `craft`, `missingInputs`, `stationSatisfied`, `craftSeconds` |
969
+ | Production building | `crafting/production` | `productionBuilding`, `ProductionBuildingDef`, `createProductionState`, `tickProduction`, `feedProduction`, `drainOutput`, `advanceTransport`, `resolvePowerGrid` |
970
+ | Crop tile / farming | `crafting/crop` | `createCropField`, `CropField`, `CropDef`, `CropTileState`, `tillTile`, `plantCrop`, `waterTile`, `advanceCropDay`, `harvestCrop`, `applyToolToTiles`, `squarePattern`, `diamondPattern`, `createDayTicker` |
971
+ | Skill-check roll | `stats/rollCheck` | `rollCheck`, `CheckInput`, `CheckResult`, `CheckAdvantage` |
972
+ | Input bindings (full) | `input/actionBindings` | `hotbarSlotBindings`, `actionLabel`, `bindingLabel`, `resolveActionCommand`, `bindingMatches`, `createActionStateTracker` |
973
+ | Touch controls | `input/touchScheme` | `deriveTouchScheme`, `touchCode`, `touchActionLabel`, `withTouchCodes`, `TouchControlsConfig`, `TouchGestureBindings`, `TouchDragBinding`, `TouchButtonSpec`, `TouchScheme`, `TouchJoystick`, `TouchButton` |
974
+ | Pointer hit | `input/pointer` | `PointerHit`, `PointerButton`, `aimToPoint`, `moveTargetFromHit`, `groundOf`, `PointerVec3` |
975
+ | Navmesh + A* | `nav/navGrid` | `createNavGrid`, `findPath`, `smoothPath`, `NavGrid`, `NavGridConfig`, `NavPoint`, `FindPathOptions` |
976
+ | Path follow | `nav/pathFollow` | `createPathFollow`, `advancePathFollow`, `pathFromNav`, `PathFollowConfig`, `PathFollowState`, `Waypoint` |
977
+ | 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 |
978
+ | Selection set | `scene/selection` | `createSelectionSet`, `SelectionSet`, `screenRect`, `selectWithinRect`, `rectContainsPoint`, `isMarquee`, `ScreenRect` |
979
+ | Context menu | `interaction/contextMenu` | `contextVerb`, `buildContextMenu`, `contextVerbInput`, `ContextVerb`, `ContextMenu` |
980
+ | Shared / group wallet | `economy/sharedWallet` | `createWalletBook`, `WalletBook`, `WalletScope`, `userScope`, `groupScope`, `balanceIn`, `grantTo`, `chargeFrom`, `contributionOf`, `contributorsOf` |
981
+ | Analog axis input | `input/axisInput` | `AxisInput`, `AxisChannel`, `AxisBindingMap`, `DRIVE_AXIS_BINDINGS`, `clampAxis`, `rampToward`, `NEUTRAL_AXIS` |
982
+ | Raw control polling | `runtime/inputSnapshot` | `createInputSnapshot`, `InputSnapshot` — backs `ctx.input` |
983
+ | Physics world | `physics/physicsWorld` | `PhysicsWorld`, `PhysicsWorldConfig`, `PhysicsBounds`, `PhysicsStats`, `AddBodyOptions` (`{ shape: "box", halfExtents }` \| `{ shape: "sphere", radius }`), `JointOptions`, `JointKind`, `CollisionEvent` |
984
+ | Ballistic collision sweep | `physics/ballisticSweep` | `createBallisticSweep`, `BallisticSweep`, `BallisticSweepHit`, `BallisticSweepOptions` |
985
+ | Tweening / easing | `anim/easing` | `Easing`, `lerp`, `clamp01`, `smoothstep`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeOutBack`, `easeOutElastic`, `tween`, `timedProgress` |
986
+ | Async data source | `data/dataSource` | `createDataSource`, `DataSource`, `DataSourceState`, `DataSourceStatus`, `DataSourceOptions`, `DataSourceClock`, `RefreshOptions` |
987
+ | JSON fetch | `data/fetchJson` | `fetchJson`, `FetchJsonOptions`, `FetchImpl`, `HttpStatusError`, `JsonParseError` |
988
+ | JSON data source | `data/jsonDataSource` | `createJsonDataSource`, `JsonDataSourceOptions` |
989
+ | Dev proxy routing | `data/devProxy` | `proxiedUrl`, `parseDevProxyTable`, `DevProxyTable`, `ProxiedUrlOptions`, `DEFAULT_DEV_PROXY_PREFIX` |
990
+ | Grid-cell world rendering | `world/gridInstances` | `resolveGridInstances`, `GridInstanceTransform` |
991
+ | Turn loop | `turn/turnLoop` | `createTurnLoop`, `TurnLoop`, `TurnLoopConfig`, `TurnState`, `PoolConfig`, `PoolState`, `TurnLoopSnapshot` |
992
+ | Declared-action intent board | `turn/intent` | `createIntentBoard`, `IntentBoard`, `DeclaredIntent` — `declare(participantId, intent)`, `peek`, `all`, `consume`, `clear` |
993
+ | Commit modes | `turn/commit` | `createCommitController`, `CommitController`, `CommitMode`, `CommitOutcome`, `SubmittedAction` |
994
+ | Intent board | `turn/intent` | `createIntentBoard`, `IntentBoard`, `DeclaredIntent` |
995
+ | Tactical grid | `tactics/tacticalGrid` | `createTacticalGrid`, `TacticalGrid`, `TacticalGridConfig`, `Tile`, `ReachableTile`, `PushResult`, `PushCollision` |
996
+ | Predictive query | `tactics/predictiveQuery` | `predictAreaEffect`, `predictArcEffect`, `predictTiles`, `PredictiveDeps`, `PredictedTarget` |
997
+ | Sim snapshot | `tactics/snapshot` | `createSnapshotStore`, `SnapshotStore`, `SnapshotSlice`, `Snapshot`, `deepClone` |
998
+ | Surfaces | `tactics/surface` | `createSurfaceLayer`, `SurfaceLayer`, `SurfaceLayerConfig`, `SurfaceKindDef`, `SurfaceReaction`, `SurfaceEvent` |
999
+ | Area targeting | `combat/effects` | `resolveAreaTargets`, `AreaTarget`, `AreaTargetInput` (shared AoE targeting behind `effect` + the predictive query) |
1000
+ | Environment field | `world/envField` | `createEnvironmentField`, `EnvironmentField`, `EnvironmentSample`, `EnvironmentFieldConfig`, `OccluderRect`, `HeatSource` |
1001
+ | Weather + fire | `world/weather` | `resolveWeather`, `WeatherState`, `WeatherModifier`, `WeatherModifierTable`, `ResolvedWeather`, `createFireGrid`, `FireGrid`, `FireCell`, `FireGridConfig` |
1002
+ | Realm composition | `world/realm` | `composeRealm`, `RealmCard`, `RealmBase`, `ComposedRealm`, `RealmEnvironmentParams`, `SpawnTableOverride` |
1003
+ | Decay meters | `survival/decayMeter` | `createDecayMeterSet`, `DecayMeterSet`, `DecayMeterConfig`, `MeterThreshold`, `DecayMeterState` |
1004
+ | Status moodles | `survival/moodle` | `createMoodleStack`, `stackMoodles`, `MoodleStack`, `Moodle`, `MoodleSeverity`, `TimedMoodleInput` |
1005
+ | Multi-region health | `survival/regionHealth` | `createMultiRegionHealth`, `MultiRegionHealth`, `HealthRegionConfig`, `AilmentConfig`, `RegionHealthState`, `AilmentInstance` |
1006
+ | Audio contract | `audio/audioFalloff` | `computeFalloffGain`, `resolveEmitterGain`, `distance3`, `AudioFalloffConfig`, `FalloffCurve`, `SoundDef`, `AudioBusDef`, `AudioBusId` |
1007
+ | Beat clock | `time/beatClock` | `createBeatClock`, `createBeatInputBuffer`, `nextBeatTime`, `BeatClock`, `BeatClockConfig`, `BeatSnapshot`, `BeatInputBuffer`, `BufferedAction` |
1008
+ | Spawn director | `ai/spawnDirector` | `createSpawnDirectorState`, `advanceSpawnDirector`, `advanceWave`, `raiseAlert`, `pickSpawnPoint`, `SpawnDirectorConfig`, `WaveManifest`, `SpawnEntry`, `SpawnRequest`, `DirectorContext` |
1009
+ | Threat table | `ai/threat` | `createThreatTable`, `ThreatTable`, `ThreatTableConfig`, `ThreatEntry`, `HighestThreatOptions` |
1010
+ | Job board | `ai/jobBoard` | `createJobBoard`, `JobBoard`, `JobDef`, `Job`, `JobPhase`, `WorkerState`, `JobReport`, `JobTickContext` |
1011
+ | Crowd flow | `ai/crowd` | `computeFlowField`, `createCrowdField`, `selectPoi`, `FlowField`, `FlowFieldOptions`, `CrowdField`, `Poi`, `SelectPoiOptions` |
1012
+ | Physics actors | `physics/ragdoll`, `physics/carryable`, `physics/forceVolume`, `physics/spatialGrid` | `createRagdoll`, `Ragdoll`, `Carryable`, `carrySpeedMultiplier`, `ForceVolume`, `PlatformCarry`, `SpatialGrid` |
1013
+ | Traversal (grapple/glide) | `physics/traversal` | `Grapple`, `GrappleConfig`, `Glide`, `GlideConfig` |
1014
+ | Structural destruction | `physics/structure` | `StructureGraph`, `StructureNodeSpec`, `StructureEdgeSpec`, `StructureMaterial`, `StructureMaterialTable`, `CollapseEvent`, `DebrisConfig` |
1015
+ | Destructible terrain | `world/carve` | `VoxelVolume`, `VoxelMaterial`, `VoxelMaterialTable`, `CarvableField`, `carvableTerrain`, `CarveOp`, `DepositOp`, `CraterOp`, `MoundOp`, `EMPTY_VOXEL` |
1016
+ | Vehicle body | `physics/vehicleBody` | `createVehicleBody`, `VehicleBody`, `VehicleBodyConfig`, `WheelSpec`, `GripCurve`, `sampleGripCurve`, `DEFAULT_GRIP_CURVE` |
1017
+ | Buoyant boat | `physics/buoyancy` | `createBuoyantBody`, `BuoyantBody`, `BuoyantBodyConfig` |
1018
+ | Crash damage | `physics/damageZones` | `createDamageModel`, `DamageModel`, `DamageZoneDef`, `DamageTransition` |
1019
+ | Mounts / rideables | `scene/mount` | `createMountController`, `MountController`, `MountKit`, `MountSeat`, `RideableConfig` |
1020
+ | Shared-vehicle stations | `scene/stationClaim` | `createStationClaim`, `StationClaim`, `Station`, `SharedVehicleConfig`, `ClaimResult` |
1021
+ | Lag compensation | `multiplayer/lagCompensation` | `createPositionHistory`, `PositionHistory`, `rewindTimestamp`, `resolveHitscan`, `raySphereDistance`, `HitscanRay`, `HitscanTarget` |
1022
+ | Simultaneous commit | `multiplayer/simultaneousCommit` | `createCommitRound`, `CommitRound`, `SealedCommit`, `resolveCommits` |
1023
+ | Combat-snapshot replay | `multiplayer/combatSnapshot` | `serializeBoard`, `cloneSnapshot`, `replayCombat`, `BoardSnapshot`, `SnapshotUnit`, `CombatRules`, `ReplayResult` |
1024
+ | Session matchmaking | `multiplayer/matchmaking` | `browseSessions`, `findByJoinCode`, `quickMatch`, `matchesFilter`, `normalizeJoinCode`, `generateJoinCode`, `SessionListing`, `MatchFilter` |
1025
+ | Auth identity | `multiplayer/identity` | `AuthSession`, `PlayerIdentity`, `sessionPlayer`, `resolveGuestSession` |
1026
+ | Text chat | `game/chat`, `multiplayer/chatContract` | `createChat`, `Chat`, `ChatMessage`, `ChatChannelDef`, `whisperChannelId`, `createChatRateLimiter`, `ChatTransport`, `ChatSync`, `createLocalChatTransport` |
1027
+ | Voice seam | `multiplayer/voiceContract` | `VoiceTransport`, `VoiceParticipant`, `VoiceRoute`, `createLocalVoiceTransport`, `createPushToTalk`, `PushToTalkMode` |
1028
+ | Race state | `game/race` | `raceTrack`, `RaceTrack`, `createRaceState`, `RaceState`, `RaceEvent`, `RaceWinCondition`, `firstPastPost`, `topK`, `lastStanding`, `everyoneFinishes` |
1029
+ | Reveal query | `sensor/revealQuery` | `createRevealQuery`, `RevealQuery`, `RevealQueryOptions`, `RevealHit` |
1030
+ | Hidden-state probe | `sensor/hiddenStateProbe` | `probeHiddenState`, `probeHiddenStateAll`, `HiddenStateSource`, `HiddenStateValue`, `SensorProbeOptions`, `SensorReading` |
1031
+ | View-frustum sensor | `sensor/frustumSensor` | `createFrustumSensor`, `projectToView`, `framingScore`, `FrustumCamera`, `FrustumTarget`, `FrustumProjection`, `FrustumSample`, `FrustumSensor`, `FramingConfig` |
1032
+ | Recording buffer | `sensor/recordingBuffer` | `createRecordingBuffer`, `RecordingBuffer`, `RecordingFrame`, `RecordingBufferOptions` |
1033
+ | Concealment scoring | `sensor/concealment` | `colorDistance`, `concealmentScore`, `createConcealmentSensor`, `ColorHex`, `ConcealmentTarget`, `ConcealmentSample`, `ConcealmentSensor` |
1034
+ | Freeze violation monitor | `sensor/freezeMonitor` | `createFreezeMonitor`, `FreezeMonitor`, `FreezeSubject`, `FreezeViolation` |
1035
+ | Animation SM | `combat/animationState` | `createAnimationState`, `AnimationState`, `AnimationClip`, `FramePhase`, `FrameRange`, `phasesAtFrame`, `activeRangeAtFrame`, `frameAtMs` |
1036
+ | Accumulator meter | `stats/accumulatorMeter` | `createAccumulatorMeter`, `AccumulatorMeter`, `AccumulatorMeterConfig`, `MeterTier`, `MeterAddResult`, `tierAt` |
1037
+ | Stagger / buildup | `combat/breakMeters` | `createStaggerMeter`, `createBuildupMeter`, `StaggerMeter`, `BuildupMeter`, `BuildupProc` |
1038
+ | Attack tags | `combat/attackTags` | `attackMeta`, `AttackTag`, `AttackMeta`, `hasTag`, `isBlockable`, `isParryable`, `isDodgeable`, `counters` |
1039
+ | Defensive window | `combat/defensiveWindow` | `createDefensiveWindow`, `resolveDefense`, `DefensiveWindowConfig`, `DefenseKind`, `DefenseOutcome`, `windowActiveAt`, `iframeActiveAt` |
1040
+ | Combo string | `combat/comboString` | `createComboRunner`, `advanceCombo`, `ComboString`, `ComboStep`, `AdvanceComboResult` |
1041
+ | Hit reaction | `combat/hitReaction` | `resolveHitReaction`, `HitReaction`, `HitReactionConfig`, `CameraShake`, `applyImpulse` |
1042
+ | Telegraph | `combat/telegraph` | `pointInTelegraph`, `telegraphProgress`, `telegraphFired`, `telegraphTurnProgress`, `telegraphFiredAtTurn`, `telegraphTurnsRemaining`, `TelegraphShape`, `TelegraphConfig` |
1043
+ | Dash / dodge | `movement/dash` | `createDashState`, `DashState`, `DashConfig`, `DashBurst`, `iframeActive`, `dashOffset` |
1044
+ | Ability kit | `combat/abilityKit` | `createAbilityKit`, `AbilityKit`, `AbilitySlotConfig`, `AbilitySlotSnapshot`, `AbilitySlotState`, `AbilityCastType`, `AbilityCastResult`, `AbilitySlotRetune` |
1045
+ | Event meter | `stats/eventMeter` | `createEventMeter`, `EventMeter`, `EventMeterConfig`, `EventMeterFeedResult` |
1046
+ | Auto-target policy | `scene/autoTarget` | `selectAutoTarget`, `createAutoTargeter`, `AutoTargetPolicy`, `AutoTargeter`, `AutoTargetDeps` |
1047
+ | Resistance matrix | `combat/resistance` | `resolveResistance`, `resistanceScale`, `ResistanceMatrix`, `ResistVerdict`, `ResistanceResult` |
1048
+ | Run draft | `game/runDraft` | `createRunDraft`, `createRunModifierStack`, `RunDraft`, `RunModifierStack`, `RunModifierOffer` |
1049
+ | Uniform-cell grid | `puzzle/cellGrid` | `CellGrid`, `CellRun`, `createCellGrid`, `cellAt`, `inGridBounds`, `withCell`, `withCells`, `fullRows`, `clearRows`, `collapseColumns`, `findRuns` |
1050
+ | Falling piece | `puzzle/fallingPiece` | `FallingPiece`, `ShapeTable`, `LockDelayState`, `pieceCells`, `pieceCollides`, `mergePiece`, `dropDistance`, `gravityInterval`, `levelForLines`, `lineScore`, `createLockDelay`, `stepLockDelay` |
1051
+ | 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 |
1052
+ | Spawn/respawn points | `game/spawnPoints` | `createSpawnPoints`, `SpawnPoints`, `SpawnPointPose`, `RespawnTarget` |
1053
+ | Level sequence | `game/levelSequence` | `createLevelSequence`, `LevelSequence`, `LevelSequenceConfig`, `LevelDescriptor`, `CurrentLevel`, `LevelSequenceStatus`, `LevelSequenceProgress` |
1054
+ | Devtools overlay + tunables | `devtools/devtools` | `devtools`, `createDevtools`, `tunable`, `snapshotDevtools`, `instrumentLatency`, `Tunable`, `TunableOptions`, `TunableAccessor`, `DevtoolsControl`, `DiscoveredEntry`, `DevtoolsOverrides`, `DevtoolsSnapshot` |
1055
+ | Tunable auto-discovery | `devtools/transformTunables` | `transformTunableExports`, `tunableModuleTable`, `tunableDiscoveryPlugin`, `TunableTransformResult` |
1056
+
1057
+ ## Getting started (new project)
1058
+
1059
+ ```sh
1060
+ bun add @jgengine/core @jgengine/react @jgengine/shell react react-dom three three-stdlib @react-three/fiber @react-three/drei
1061
+ bun add -d @tailwindcss/vite tailwindcss # HUD styling (Vite + Tailwind v4)
1062
+ ```
1063
+
1064
+ 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:
1065
+
1066
+ ```html
1067
+ <!-- index.html -->
1068
+ <!doctype html>
1069
+ <html lang="en">
1070
+ <head>
1071
+ <meta charset="UTF-8" />
1072
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1073
+ <title>My Game</title>
1074
+ </head>
1075
+ <body>
1076
+ <div id="root"></div>
1077
+ <script type="module" src="/src/main.tsx"></script>
1078
+ </body>
1079
+ </html>
1080
+ ```
1081
+
1082
+ ```ts
1083
+ // vite.config.ts — monorepo-aware: the alias branch only fires when this folder
1084
+ // sits inside the engine repo checkout; copied anywhere else, @jgengine/* resolves
1085
+ // from npm dist and the alias list is empty
1086
+ import { existsSync } from "node:fs";
1087
+ import { fileURLToPath } from "node:url";
1088
+ import tailwindcss from "@tailwindcss/vite";
1089
+ import react from "@vitejs/plugin-react";
1090
+ import { defineConfig } from "vite";
1091
+
1092
+ const engineSrc = (pkg: string) => fileURLToPath(new URL(`../../packages/${pkg}/src`, import.meta.url));
1093
+
1094
+ export default defineConfig({
1095
+ plugins: [react(), tailwindcss()],
1096
+ resolve: {
1097
+ alias: existsSync(engineSrc("core"))
1098
+ ? [
1099
+ { find: /^@jgengine\/core\/(.*)$/, replacement: `${engineSrc("core")}/$1` },
1100
+ { find: /^@jgengine\/react\/(.*)$/, replacement: `${engineSrc("react")}/$1` },
1101
+ { find: /^@jgengine\/ws\/(.*)$/, replacement: `${engineSrc("ws")}/$1` },
1102
+ { find: /^@jgengine\/shell\/(.*)$/, replacement: `${engineSrc("shell")}/$1` },
1103
+ { find: /^@jgengine\/assets$/, replacement: `${engineSrc("assets")}/index.ts` },
1104
+ { find: /^@jgengine\/assets\/(.*)$/, replacement: `${engineSrc("assets")}/$1` },
1105
+ ]
1106
+ : [],
1107
+ },
1108
+ });
1109
+ ```
1110
+
1111
+ ```css
1112
+ /* src/index.css */
1113
+ @import "tailwindcss";
1114
+ @source "../node_modules/@jgengine/react/dist";
1115
+ @source "../node_modules/@jgengine/shell/dist";
1116
+ ```
1117
+
1118
+ 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.
1119
+
1120
+ ```tsx
1121
+ // main.tsx
1122
+ import "./index.css";
1123
+
1124
+ import { createRoot } from "react-dom/client";
1125
+ import { GameHost } from "@jgengine/shell/GameHost";
1126
+ import { game } from "./game.config";
1127
+
1128
+ const root = document.getElementById("root");
1129
+ if (root === null) throw new Error("main: missing #root mount element");
1130
+ createRoot(root).render(<GameHost playable={game} />);
1131
+ ```
1132
+
1133
+ Add `"dev": "vite"` to `package.json`'s `scripts` — `bun dev` then launches the game standalone.
1134
+
1135
+ `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.
1136
+
1137
+ A multi-game host (a launcher, a dev registry) wires `GamePlayer` over a `GameRegistry`:
1138
+
1139
+ ```tsx
1140
+ import { GamePlayer } from "@jgengine/shell/GamePlayer";
1141
+ import type { GameRegistry } from "@jgengine/shell/registry";
1142
+
1143
+ const games: GameRegistry = {
1144
+ "my-game": () => import("./my-game").then((m) => m.game),
1145
+ };
1146
+
1147
+ function App() {
1148
+ return <GamePlayer gameId="my-game" registry={games} loading={<p>Loading…</p>} />;
1149
+ }
1150
+ ```
1151
+
1152
+ `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.
1153
+
1154
+ 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`.
1155
+
1156
+ ## Scope
1157
+
1158
+ 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.
1159
+
1160
+ | Engine owns | Your game owns |
1161
+ |-------------|----------------|
1162
+ | 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 |
1163
+
1164
+ **Rules:**
1165
+
1166
+ 1. **Catalog-first** — shape and behavior of every id lives in game-owned catalog files. Runtime calls pass ids, positions, instance keys.
1167
+ 2. **Three buckets** — inventory items, scene objects, scene entities. Never merge them.
1168
+ 3. **Dumb place/spawn** — no behaviors on `place()`/`spawn()`; the catalog owns them.
1169
+ 4. **Commands for verbs** — input maps to actions, actions to commands/handlers; no raw keys in game logic.
1170
+ 5. **Primitives over glue** — a loop several games need (loot roll, shop buy, kit seeding) belongs in the engine, not copy-pasted per game.
1171
+ 6. **No speculative config** — `defineGame` fields exist only with a live engine consumer.
1172
+ 7. **This file stays domain-free.**
1173
+
1174
+ ## The three buckets
1175
+
1176
+ | Bucket | What | API |
1177
+ |--------|------|-----|
1178
+ | **Inventory** | Stackable ids in containers | `ctx.player.inventory.put / take / move / has / count` |
1179
+ | **Scene object** | Static world content | `ctx.scene.object.place / remove / move / rotate / list` |
1180
+ | **Scene entity** | Movers driven per tick | `ctx.scene.entity.spawn / despawn / setPose / effect / …` |
1181
+
1182
+ 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.
1183
+
1184
+ ## Game repo layout
1185
+
1186
+ 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.
1187
+
1188
+ ```
1189
+ src/
1190
+ game.config.ts single entry — export const game = defineGame({...}) from "@jgengine/shell/defineGame"
1191
+ index.tsx barrel — export { game } from "./game.config" (+ any UI-preview scenario re-export)
1192
+ main.tsx standalone host — mounts <GameHost playable={game}/> from "@jgengine/shell/GameHost"
1193
+ loop.ts onInit, onNewPlayer, onTick
1194
+ world.ts WorldFeature + PhysicsConfig (only for games that have one)
1195
+ game/
1196
+ keybinds.ts ActionCodesMap — named actions + hotbarSlotBindings(n)
1197
+ inventories.ts inventory declarations
1198
+ assets.ts Render catalog
1199
+ content.ts itemById / entityById lookups over all catalogs
1200
+ loadouts.ts Loadout ids → items/economy/unlocks per inventory
1201
+ world/ zones.ts, setup.ts (place/spawn from onInit)
1202
+ items/ <domain>/catalog.ts + use-handlers.ts
1203
+ objects/ catalog.ts (+ loot tables beside their domain)
1204
+ entities/ players/ enemies/ npcs/ — catalog.ts per role (never actors/)
1205
+ quests/catalog.ts when using game.quest
1206
+ progression/ curves.ts — game-owned XP curve numbers fed to game/progression
1207
+ ui/GameUI.tsx ALL layout/positioning
1208
+ ui/components/ content-only pieces GameUI places
1209
+ ```
1210
+
1211
+ ## `defineGame` — the single authoring entry
1212
+
1213
+ `@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).
1214
+
1215
+ **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.
1216
+
1217
+ ```ts
1218
+ // game.config.ts — imports only, nothing inline
1219
+ import { defineGame } from "@jgengine/shell/defineGame";
1220
+ import { assets } from "./game/assets";
1221
+ import { content } from "./game/content";
1222
+ import { GameUI } from "./game/ui/GameUI";
1223
+ import { inventories } from "./game/inventories";
1224
+ import { keybinds } from "./game/keybinds";
1225
+ import { loop } from "./loop";
1226
+ import { physics, world } from "./world";
1227
+
1228
+ export const game = defineGame({
1229
+ name: "My Game",
1230
+ assets,
1231
+ world,
1232
+ physics,
1233
+ inventories,
1234
+ input: keybinds,
1235
+ server: "persistent", // or { mode: "ffa", scoreLimit: 30 } — rules live in game code
1236
+ save: { auto: "5m", scope: "player+chunks" }, // or "none"
1237
+ multiplayer: offline(), // or ws({ topology, url? }) / fly({ app }) / convex({ topology }) / socketIo({ topology, url? }) / p2p({ room? }) / lan({ port?, path? }) / servers({ …, adapter }) — defaults to offline()
1238
+ content,
1239
+ loop, // Partial<GameLoop<GameContext>> — missing hooks default to no-ops
1240
+ GameUI,
1241
+ camera: { perspective: "third" }, // optional — this is the default
1242
+ });
1243
+ ```
1244
+
1245
+ ```ts
1246
+ // game/keybinds.ts — named actions + generated hotbar slots; one key, one action
1247
+ import { hotbarSlotBindings, type ActionCodesMap } from "@jgengine/core/input/actionBindings";
1248
+
1249
+ export const keybinds: ActionCodesMap = {
1250
+ moveForward: ["KeyW"], moveBack: ["KeyS"], moveLeft: ["KeyA"], moveRight: ["KeyD"],
1251
+ jump: ["Space"], sprint: ["ShiftLeft"],
1252
+ interact: ["KeyE"],
1253
+ crouch: { hold: ["KeyC"], toggle: ["KeyZ"] },
1254
+ aim: { hold: ["mouse2"], toggle: ["KeyV"] },
1255
+ tabTarget: ["Tab"], clearTarget: ["Escape"],
1256
+ ...hotbarSlotBindings(9), // hotbarSlot1..9 → Digit1..9 (a 10th slot gets Digit0)
1257
+ };
1258
+ ```
1259
+
1260
+ ```ts
1261
+ // game/inventories.ts
1262
+ import type { InventoryDeclaration } from "@jgengine/core/game/defineGame";
1263
+ export const inventories: Record<string, InventoryDeclaration> = {
1264
+ hotbar: { slots: 9, hud: "hotbar" },
1265
+ backpack: { slots: 28, traits: itemTraits },
1266
+ equipment: { slots: 4, accepts: ["weapon", "armor"], applyModifiers: true },
1267
+ };
1268
+
1269
+ // world.ts — top of src/, not under game/
1270
+ import type { PhysicsConfig } from "@jgengine/core/game/defineGame";
1271
+ import { biomes, type WorldFeature } from "@jgengine/core/world/features";
1272
+ export const world: WorldFeature = biomes({ map: "world/biomes", zones: "world/zones" });
1273
+ export const physics: PhysicsConfig = { gravity: -32 };
1274
+ ```
1275
+
1276
+ - `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.
1277
+ - 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.
1278
+ - **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).
1279
+ - **`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.
1280
+ - 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.
1281
+ - `server.mode` is a string your loop/commands interpret — the engine ships no gamemode presets.
1282
+ - 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).
1283
+ - `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.
1284
+ - `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.
1285
+
1286
+ ### `@jgengine/core/game/defineGame` — the underlying primitive
1287
+
1288
+ 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.
1289
+
1290
+ ```ts
1291
+ import { defineGame as defineEngineGame } from "@jgengine/core/game/defineGame";
1292
+ import { offline } from "@jgengine/core/runtime/adapter";
1293
+
1294
+ const game = defineEngineGame({
1295
+ name: "My Game",
1296
+ assets, world, physics, inventories,
1297
+ input: keybinds,
1298
+ server: "persistent",
1299
+ save: { auto: "5m", scope: "player+chunks" },
1300
+ multiplayer: offline(),
1301
+ loop, // GameLoop<GameContext>
1302
+ });
1303
+ ```
1304
+
1305
+ 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.
1306
+
1307
+ ## `PlayableGame` — how a game plugs into a runner
1308
+
1309
+ 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`:
1310
+
1311
+ ```ts
1312
+ export type PlayableGame<TUi = unknown> = {
1313
+ game: GameDefinition;
1314
+ content: GameContextContent; // { itemById?, entityById?, objectById? }
1315
+ loop: Required<GameLoop<GameContext>>; // onInit, onNewPlayer, onTick
1316
+ GameUI: TUi; // React component in web runners
1317
+ prompts?: (ctx: GameContext) => readonly PositionedPrompt[]; // interact-key + HUD source
1318
+ };
1319
+ ```
1320
+
1321
+ `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.
1322
+
1323
+ 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`.
1324
+
1325
+ `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.
1326
+
1327
+ **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.
1328
+
1329
+ **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.
1330
+
1331
+ **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.
1332
+
1333
+ **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.
1334
+
1335
+ 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.
1336
+
1337
+ ### Object spatial queries, entity patching, and surface sampling
1338
+
1339
+ ```ts
1340
+ ctx.scene.object.at(x, y, z) // cell lookup, cell size 1, most-recent wins
1341
+ ctx.scene.object.inBox(min, max) // inclusive AABB query
1342
+ ctx.scene.object.raycast({ origin, direction, maxDistance, halfExtents?, filter? }) // → nearest hit or null
1343
+ ctx.scene.object.raycastAll({ origin, direction, maxDistance, halfExtents?, filter? }) // → hits, nearest-first
1344
+ ctx.scene.entity.update(id, patch) // name/position/rotations/role/movement/behaviors/meta; false for unknown id
1345
+ ```
1346
+
1347
+ 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.
1348
+
1349
+ `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).
1350
+
1351
+ `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.
1352
+
1353
+ **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.
1354
+
1355
+ ### Audio — positional emitters, listener falloff, buses
1356
+ 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.
1357
+ ### Camera rigs (`camera` field of `defineGame({...})`)
1358
+ 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:
1359
+ | `rig` | For | Key config (`camera.<rig>`) |
1360
+ |-------|-----|------------------------------|
1361
+ | `orbit` (default) | Third-person chase | `initialDistance`, `targetHeight`, `min/maxPolarAngle`, drag/zoom |
1362
+ | `first` | FPS mouse-look | `firstPerson: { eyeHeight, sensitivity, maxPitch, reticle, viewmodel }` |
1363
+ | `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`) |
1364
+ | `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 |
1365
+ | `shoulder` | Over-the-shoulder (Helldivers 2, Remnant II) | `shoulder: { shoulderOffset, distance, ads, side }` — ADS + shoulder-swap (V) |
1366
+ | `lockOn` | Souls-like strafe (Elden Ring) | `lockOn: { targetEntityId?, distance, framingBias, yawSmoothing }` — yaw binds to player→target; WASD becomes strafe |
1367
+ | `chase` | Vehicle chase (Forza, Rocket League) | `chase: { distance, springDamping, fov: { base, max, speedForMax }, shakePerSpeed, view: "chase"|"cockpit"|"hood"|"rear" }` |
1368
+ | `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) |
1369
+ | `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 |
1370
+ | `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 |
1371
+ | `none` | No camera rig mounted | HUD-only presentations or a game that manages its own camera; see `presentation: "hud"` below |
1372
+ **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.
1373
+
1374
+ ## `GameContext` — the ctx surface
1375
+
1376
+ `createGameContext` (in `@jgengine/core/runtime/gameContext`) wires every system:
1377
+
1378
+ ```
1379
+ ctx.scene.object place, remove, move, rotate, get, list, subscribe,
1380
+ at, inBox, raycast, raycastAll, catalog
1381
+ ctx.scene.entity spawn, despawn, setPose, update, get, list,
1382
+ stats.{get,set,delta}, setTarget, getTarget, cycleTarget,
1383
+ canReceive, preview, effect, paint,
1384
+ willHitProjectile, fireProjectile, settleProjectile,
1385
+ distance, inRadius, hasLineOfSight, queryArc, moveToward,
1386
+ spawnPoseOf, resetToSpawn, resetAllToSpawn,
1387
+ form.{register,get,active,abilities,shapeshift,revert}
1388
+ ctx.game commands, events, feed, loot, trade, quest, social, chat,
1389
+ unlocks, economy, leaderboard, roster, store, cards, turn
1390
+ ctx.game.social friends, party, presence, emotes.play, worldInvites
1391
+ ctx.game.store set, delete, get, has, subscribe, mapSnapshot, arraySnapshot — game-defined
1392
+ keyed reactive store slot (any value type); mutations bump ctx.version()
1393
+ ctx.game.cards pile(id, config?) — lazily creates (config required on first call) or returns
1394
+ the existing notify-wrapped CardPile for id
1395
+ ctx.game.turn loop(id, config?) — lazily creates (config required on first call) or returns
1396
+ the existing notify-wrapped TurnLoop for id
1397
+ ctx.player userId, isNew, inventory, stats (modifiers), loadout,
1398
+ applyLoadout, movement (pose/aim), motion (impulse/setVerticalVelocity/setY/takePending),
1399
+ possession, cosmetics
1400
+ ctx.player.motion impulse(vy), setVerticalVelocity(vy), setY(y), takePending() — game-code
1401
+ seam into the shell's vertical-motion integrator; drained once per frame
1402
+ before gravity, so a jump pad or grapple release calls this from
1403
+ onTick/commands instead of touching y directly
1404
+ ctx.item use, weapon
1405
+ ctx.input publish(held), isDown(action), held() — per-frame held-action snapshot, polled from onTick
1406
+ ctx.world ground (TerrainField), groundHeightAt(x, z) — the canonical
1407
+ sampler for the game's declared world; environment worlds
1408
+ resolve their terrain field, every other world kind is 0.
1409
+ Use it for every spawn/placement/waypoint y — never
1410
+ hand-roll a noise sampler or hardcode y = 0 on relief
1411
+ ctx.camera follow(entityId | null), followedEntityId(), setCinematic(config), cinematic(),
1412
+ subscribe — runtime camera-follow/cinematic override; the shell reads
1413
+ followedEntityId() each frame, falling back to the static
1414
+ playable.camera.followEntityId when it returns undefined
1415
+ ctx.time advance, now, calendar, snapshot; pause, play, toggle,
1416
+ setSpeed, cycleSpeed; after, every, at (game-time timers)
1417
+ ctx.subscribe / ctx.version change signal — UI layers bind via useSyncExternalStore
1418
+ ```
1419
+
1420
+ `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.
1421
+
1422
+ ### Two tiers: `ctx` runtime vs pure factories
1423
+
1424
+ 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.
1425
+
1426
+ `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.
1427
+
1428
+ ## `loop` — lifecycle
1429
+
1430
+ ```ts
1431
+ export function onInit(ctx: GameContext) {
1432
+ ctx.item.use.register(itemUseHandlers);
1433
+ ctx.player.loadout.register(loadouts);
1434
+ for (const table of lootTables) ctx.game.loot.register(table);
1435
+ ctx.game.quest.register(quests);
1436
+ ctx.game.quest.bind("entity.died");
1437
+ ctx.game.feed.bind("entity.died");
1438
+ ctx.game.events.on("entity.died", (evt) => onEntityDied(ctx, evt));
1439
+ setupWorld(ctx);
1440
+ }
1441
+
1442
+ export function onNewPlayer(ctx: GameContext) {
1443
+ ctx.scene.entity.spawn("player_default", { id: ctx.player.userId, position: spawnPoint });
1444
+ if (ctx.player.isNew) ctx.player.applyLoadout(ctx.player.userId, "starterKit");
1445
+ }
1446
+
1447
+ export function onTick(ctx: GameContext, dt: number) {
1448
+ // AI, regen, respawn timers — dt is GAME time (see ctx.time). Never death detection (see entity.died)
1449
+ }
1450
+ ```
1451
+
1452
+ `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.
1453
+
1454
+ ## `ctx.time` — the simulation clock
1455
+
1456
+ `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).
1457
+
1458
+ - **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.
1459
+ - **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.
1460
+ - **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.
1461
+
1462
+ ### Beat clock — BPM signal + input quantization
1463
+
1464
+ `@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.
1465
+
1466
+ ## Content catalogs
1467
+
1468
+ ### Object catalog fields
1469
+
1470
+ | Field | Purpose |
1471
+ |-------|---------|
1472
+ | `id`, `model` | Canonical id, asset key |
1473
+ | `footprint` | `{ w, h, d }` placement bounds |
1474
+ | `snap` | `"grid"` \| `"free"` \| `"wall"` |
1475
+ | `solid` | Blocks movement |
1476
+ | `breakable` | `false` or `{ baseBreakTime, harvest, drops, dropsWhenUnmet }` |
1477
+ | `proximityPrompt` | Float UI + optional command invoke |
1478
+ | `slotInventory` | Attached container `{ slots, accepts }` created at place time (`object:<instanceId>`) |
1479
+
1480
+ Break resolution: `duration = baseBreakTime / (tool?.breakSpeed ?? 1)`; drops per `when` (`always` / `harvestMet` / `silkTouch` / `playerKill`); then `inventory.put` + `object.remove`.
1481
+
1482
+ ### Item catalog fields
1483
+
1484
+ | Field | Purpose |
1485
+ |-------|---------|
1486
+ | `id`, `kind`, `stack`, `model` | Basics; `stack` feeds `itemTraits.stackLimit` |
1487
+ | `use` | Game handler name dispatched by `item.use` (`"fireGun"`, `"castBolt"`, `"drinkPotion"`) |
1488
+ | `weapon` | Stats the handler reads via `item.weapon.getStat` — `damage`, `heal`, `reach`, `manaCost`, `projectile.{mass,gravity,fuseTime,settleOn}`, `explosion.{radius}` … |
1489
+ | `trade` | `{ buy?: {coins: 80}, sell?, shops?: ["shop_town"] }` |
1490
+ | `requires` | Unlock ids gating purchase/use |
1491
+ | `placesObject` | Object id placed from hotbar |
1492
+ | `rarity`, `baseType` | Read by the `worldItem` rarity render binding + loot filter when this item drops to the ground (#32/#33); `baseType` defaults to the item id when absent |
1493
+
1494
+ ### Entity catalog fields
1495
+
1496
+ | Field | Purpose |
1497
+ |-------|---------|
1498
+ | `movement` | `walkSpeed` (reaches spawn automatically), `poses?: ["standing","crouch","prone","running"]`, `aim?: ["hip","ads"]`, `frozen?: boolean` — a scene-instance-level movement lock (cutscenes, stuns, mount transitions); `movedWhileFrozen(entity)` (`scene/entityStore`) flags an entity whose velocity moved anyway despite `frozen: true`, catching a system that bypassed the lock |
1499
+ | `role` | `CatalogEntityRole` = `"player"` \| `"enemy"` \| `"hostile"` \| `"npc"` \| `"vehicle"` — catalog hostility class for targeting (`"enemy"`/`"hostile"` classify hostile in `cycleTarget`). Distinct from the scene *instance* `EntityRole` (`"player"` \| `"npc"` \| `"prop"`, in `scene/entityStore`) which drives input/camera binding — **possession** (`ctx.player.possession`) flips this instance role between `"player"`/`"npc"` on every control swap, so exactly one owned entity is ever the input/camera target |
1500
+ | `stats` | Stat declarations — bounded values: `{ health: { max: 120, min: 0 }, level: { max: 60, min: 1, current: 1 }, … }` — `current` optional, defaults to `max` |
1501
+ | `receive` | Per-effect absorption: `{ damage: { order: ["shield","health"], modifiers? }, heal: { order: ["health"] } }` — keyed by **game-defined effect ids**; presence = can receive |
1502
+ | `onDeath` | `{ drops: "table_id" }` or reason-aware `{ drops: [{ table, when: { reason: "player_kill" } }], command?: { name, when? } }` |
1503
+ | `wander`, `talkable` | AI descriptor; dialogue id sugar for a talk prompt |
1504
+
1505
+ ### Dialogue catalog
1506
+
1507
+ `entities/npcs/dialogues.ts` — `{ id, lines: [{ speaker, text } | { choices: [{ label, invoke: { command, args } | null }] }] }`. Choices invoke `quest.accept`, `trade.open`, etc. Types ship from `@jgengine/react/components` (`DialogueDef`, `DialogueChoice`, `DialogueLine`) so a game imports them rather than redeclaring — the `DialogueBox` component renders the same shape it types.
1508
+
1509
+ A choice may gate its branch behind a roll: `{ check: { modifier, dc, advantage? }, onSuccess?, onFailure? }` (`onSuccess`/`onFailure` default to `invoke` when omitted). `DialogueBox` rolls via `@jgengine/core/stats/rollCheck`'s `rollCheck({ modifier, dc, advantage }, rng?)` (d20 by default; `advantage`/`disadvantage` roll twice and take the high/low; a natural 1 or max-die result reports `critical`) when the player clicks a checked choice, then calls `onChoice(choice, result)`; game code resolves which command to run with `resolveDialogueInvoke(choice, result)` (also exported from `@jgengine/react/components`).
1510
+
1511
+ ## `scene.entity.stats` — bounded stats
1512
+
1513
+ ```ts
1514
+ stats.get(instanceId, statId) // → { current, max, min } | null
1515
+ stats.set(instanceId, statId, { current?, max?, min? })
1516
+ stats.delta(instanceId, statId, n) // → null | { reason } — clamps into [min, max]
1517
+ ```
1518
+
1519
+ Health, mana, xp, level, energy — any stat id declared on the catalog. Spawn seeds from the catalog (`current ?? max`). Combat writes through effects; non-combat (regen ticks, XP grants) calls `delta` directly.
1520
+
1521
+ **XP/level use the engine progression primitive.** `@jgengine/core/game/progression` ships `curve()`/`evalCurve()` (evaluate a game-owned XP-per-level curve *definition*) and `leveling()` (a level track over the bounded `xp`/`level` stats that reports overflow). You own the curve *numbers* in a catalog; the engine owns the overflow math — on level-up bump `level.current`, reset `xp.max` from the curve, push a `stat.levelUp` feed entry. Hand-rolling `xpForLevel`/`levelFromXp`/`xpToNextLevel` is the anti-pattern — those already exist. `LevelingConfig.thresholdMode` picks how the curve is read: `"perLevel"` (default) treats `xpForLevel(N)` as the incremental N-1→N cost, summed internally; `"cumulative"` treats `xpForLevel(N)` as the total lifetime XP to reach level N (0 at/below `startLevel`) and compares `xp.current` straight against those totals — pick this for a design that quotes "total XP to level N" tables.
1522
+
1523
+ `leveling({ …, thresholdMode: "cumulative" })` switches to lifetime-total semantics: `xp` is a running total instead of a per-level bank, and `resolve(level, xp)` walks upward from `level` to the highest level whose cumulative threshold `xp` clears — it never demotes, and clamps the returned `xp` to the max-level threshold once capped. Default is `"perLevel"` (unchanged, fully back-compat) — pick `"cumulative"` for a total-XP display (MMO-style "12,450 XP") instead of a resettable per-level bank.
1524
+
1525
+ `ctx.player.stats` is a different thing: **modifiers** (buffs, ADS zoom, walk-speed bonuses) via `base/add/remove/get` with expiries — never bounded current/max values.
1526
+
1527
+ ## Targeting (MMO tab-target)
1528
+
1529
+ Persistent per-entity session state — never a per-use input field.
1530
+
1531
+ ```ts
1532
+ ctx.scene.entity.setTarget(fromId, toId | null)
1533
+ ctx.scene.entity.getTarget(fromId) // → instanceId | null
1534
+ ctx.scene.entity.cycleTarget(fromId, { filter: "hostile" | "friendly" | "any", direction? })
1535
+ ```
1536
+
1537
+ Hostility comes from catalog `role` (`"enemy"`/`"hostile"` classify hostile). Input `tabTarget`/`clearTarget` actions route here. Handlers always read `getTarget(input.from)` — `ItemUseInput` deliberately has **no `to` field** (single source of truth, no client-supplied target to validate, three targeting models stay clean: aim for shooters, `queryArc` for melee, `getTarget` for MMO).
1538
+
1539
+ ## `item.use` — one verb for all usable items
1540
+
1541
+ ```ts
1542
+ ctx.item.use.register(handlers) // once in onInit; duplicate names throw
1543
+ ctx.item.use.can(ctx, input) // → { reason } | null
1544
+ ctx.item.use.use(ctx, input) // dispatches catalog `use` → your handler
1545
+
1546
+ type ItemUseInput = { from: string; itemId: string; inventoryId?: string; aim?: Aim };
1547
+ type ItemUseHandler<GameContext> = {
1548
+ can?(ctx, input): { reason: string } | null;
1549
+ apply(ctx, input): { state: GameContext; error?: string };
1550
+ };
1551
+ ```
1552
+
1553
+ **Handlers receive the full `GameContext` as state** and mutate through it. Handlers own ammo, cooldowns, range checks, and effect ids; the engine owns projectile geometry, stat clamp math, and `canReceive`.
1554
+
1555
+ | Handler | Engine calls |
1556
+ |---------|--------------|
1557
+ | gun | spend ammo → `fireProjectile` → `settleProjectile` |
1558
+ | grenade | `fireProjectile` (ballistic) → settle → `effect({ at, radius })` |
1559
+ | melee | `queryArc` + reach from `getStat` → `effect` per hit |
1560
+ | MMO cast | `getTarget(from)` → `stats.delta(mana)` → `effect({ to })` |
1561
+ | consumable | `effect({ to: from, effect: "heal", via: { amount: -n } })` |
1562
+
1563
+ Banned in the engine: `weapon.fire`, `consumable.use`, `game.combat.*`, per-weapon commands.
1564
+
1565
+ ### Skill-checks and QTE (timed/rolled minigames)
1566
+
1567
+ `@jgengine/core/interaction/skillCheck` models a moving-target-zone minigame (casting/reeling, active-reload): `evaluateSkillCheck({ trackWidth, zone, markerPeriod, window, zoneDriftPerSecond? }, elapsedSeconds)` bounces a marker back and forth over `markerPeriod` seconds and returns `{ success, timedOut, markerPosition, zone }` — `zone` itself can drift when `zoneDriftPerSecond` is set. It is pure: an `item.use` handler starts a session by recording `ctx.time.now()` (game-time, so pause/fast-forward apply for free) the first time it's pressed, and evaluates `evaluateSkillCheck` against the elapsed time on the next press to lock in success/fail — the session bookkeeping (a `Map<instanceId, startedAt>`) is game-owned, same pattern as an ability-cooldown map.
1568
+
1569
+ `@jgengine/core/interaction/qte` sequences discrete timed prompts: `evaluateQteSequence(steps: QteStep[], inputs: QteInputEvent[])` walks `{ id, action, windowStart, windowEnd }` steps against `{ action, at }` presses and returns `{ status: "success" }` or `{ status: "fail", atStep, reason }`; `pendingQteStep`/`qteProgress` read the currently-active step and fraction complete for UI.
1570
+
1571
+ `@jgengine/react` ships matching headless UI: `SkillCheckBar({ config, startedAt })` and `QteTrack({ steps, startedAt })` self-tick via `requestAnimationFrame` and read `ctx.time.now()` each frame — pass `className`/`trackClassName`/`zoneClassName`/`markerClassName` (or `stepClassName`/`activeClassName`/`doneClassName` for `QteTrack`) for the moving-zone/timing visuals the UI quality bar requires.
1572
+
1573
+ ### Capture and owned roster
1574
+
1575
+ `@jgengine/core/scene/captureCheck` — `captureChance({ hpFraction, catchPower, difficulty? })` returns a 0..1 probability (lower `hpFraction` and higher `catchPower` raise it, higher `difficulty` lowers it); `rollCapture(input, rng?)` rolls it. `@jgengine/core/scene/roster` — `createRoster()` is a persisted, per-owner store (`capture`, `release`, `list`, `get`, `setEquipped`, `equippedList`, `snapshot`/`hydrate`) wired onto the runtime as `ctx.game.roster`, distinct from `game.social.party` (session-ephemeral) — roster entries persist and are optionally equipped (deployed) independent of party membership.
1576
+
1577
+ A capture item's `item.use` handler composes the primitives instead of forking them: read the wild target's hp via `ctx.scene.entity.stats.get(target, "health")`, roll `rollCapture({ hpFraction, catchPower })`, and on success call `ctx.scene.entity.despawn(target)` + `ctx.game.roster.capture(ownerId, catalogId)` — the wild scene entity is removed and re-parented into the owner's persisted roster; the react `CaptureOdds({ chance })` component shows the live odds meter the UI quality bar requires.
1578
+
1579
+ ## Combat — effects, projectiles, death, feel, abilities
1580
+
1581
+ Combat primitives — effects & projectiles, death handling, melee/defense/telegraph feel, and abilities/resources/auto-target/resistance/run drafts. Full surface: **[reference/combat.md](reference/combat.md)**.
1582
+
1583
+ ## Loot
1584
+
1585
+ ```ts
1586
+ lootTable({ id, rolls?, entries: [{ item? | currency?, count: n | [min,max], weight }] })
1587
+ ctx.game.loot.register(table) // in onInit
1588
+ ctx.game.loot.has(id) / roll(id, rng?) / grantToPlayer(userId, drops, source?)
1589
+ ```
1590
+
1591
+ Tables colocate with their domain (`entities/enemies/loot-tables.ts`, `objects/loot-tables.ts`). Entities reference them via `onDeath.drops`; chests via a `loot.open` command arg. `grantToPlayer` fills declared inventories, grants currencies, and emits `loot.granted`.
1592
+
1593
+ ## Card, board & shaped-inventory primitives
1594
+ Pure, renderer-free structures for card, board, and deckbuilder games — they sit **beside** the slot inventory, not in place of it. All are immutable-reducer + thin-controller pairs, mirroring the two-tier ctx/factory model: use the `create*` controller in game code, reach for the exported pure functions (`draw`, `moveCards`, `tickTimeline`, `laneAggregate`, `runPipeline`, `placeShaped`) for unit tests and headless servers.
1595
+ ```ts
1596
+ // cards/cardPile — named ordered zones (deck/hand/discard/exhaust); seeded shuffle, hand limit, reshuffle-on-empty
1597
+ const pile = ctx.game.cards.pile("deck", { zones: ["deck","hand","discard","exhaust"], drawFrom:"deck", handZone:"hand", discardTo:"discard", handLimit:7, reshuffleFrom:"discard" });
1598
+ pile.reset(createCardPileState(pileConfig, { deck: ids })); // seed zone contents once, from onInit
1599
+ pile.shuffle("deck", seed); // seeded Fisher–Yates via pileRng — deterministic under the same seed
1600
+ pile.draw(5); // deck → hand, clamped to handLimit, reshuffles discard when deck runs dry
1601
+ pile.discard(ids); pile.exhaust(ids, "exhaust"); // Slay the Spire / Balatro lifecycle
1602
+ // cards/modifierPipeline — ordered { source, apply(value) → value } with an inspectable per-step trace
1603
+ const score = runPipeline({ chips: 10, mult: 1 }, jokers); // score.value + score.trace[i].{before,after,changed} for Balatro-style scoring readouts
1604
+ // board/laneBoard — N lanes, per-side power aggregate + optional per-lane LaneRule modifier (Marvel Snap / Inscryption)
1605
+ board.aggregate(lane, "player").total; board.outcome(lane).winner; board.lanesWon();
1606
+ // board/timelineBoard — N slots each on an independent cooldown, resolving in expiry order (The Bazaar auto-battlers)
1607
+ board.tick(dtMs); // → fires[] sorted by expiry time then slot index; multiple fires per slot per tick
1608
+ // inventory/shapedGrid — polyomino footprints, rotate, overlap-check, adjacency (Backpack Hero / Tetris inventory)
1609
+ placeShaped(grid, { id, value, footprint }, [col,row], rotation); // rotateFootprint / canPlace guard overlap + bounds
1610
+ gridAdjacencyQuery(grid).neighborsOf(id); // feeds synergy effects
1611
+ ```
1612
+ Reuse the engine's seeded RNG (`pileRng`) for anything random — never `Math.random()` in game logic. The React drag/rotate/drop/snap gesture layer over these lives in `@jgengine/react` (see UI section).
1613
+
1614
+ `ctx.game.cards.pile(id, config?)` is the runtime-wired accessor for `createCardPile`: lazily creates the pile on first call (`config` required then) or returns the existing one for `id` on every later call, and every mutation notifies `ctx.subscribe`/bumps `ctx.version()` — so a `useEngineState`-bound hand/discard view re-renders without a game-owned store. Reach for `createCardPile` directly only for a headless test or server; game code goes through `ctx.game.cards.pile`.
1615
+
1616
+ ## Puzzle primitives — cell grids and falling pieces
1617
+
1618
+ Two pure, renderer-free `@jgengine/core` primitives for cell-based puzzle games (Tetris wells, match-3 boards); tile art and the drop-cadence loop are the shell's/game's job.
1619
+
1620
+ - **`puzzle/cellGrid`** — a generic immutable `CellGrid<T>` for uniform typed-cell boards. Row 0 is the top; `y` grows downward. `createCellGrid`, `cellAt`, `withCell`/`withCells` (immutable single/batch writes), `fullRows`/`clearRows` (line-clear + compaction), `collapseColumns` (match-3 cascade gravity), `findRuns` (run detection with an optional custom matcher).
1621
+ - **`puzzle/fallingPiece`** — the falling-piece layer over a `CellGrid`: `ShapeTable<TShape>` maps rotation states to cell offsets; `pieceCells`/`pieceCollides`/`mergePiece` place, test, and commit a piece; `dropDistance` computes the ghost-piece landing row; `gravityInterval`/`levelForLines`/`lineScore` are the classic Tetris drop-speed/level/score curves (overridable); `createLockDelay`/`stepLockDelay` is the grounded→countdown→lock stepper (`delaySeconds: 0` locks instantly on touchdown).
1622
+ - **`tactics/fallingGrid`** — a generic tile-drop grid over any `TCell` payload (distinct from the `cellGrid`/`fallingPiece` row-clear pair): `createFallingGrid(config)`, `gravityIntervalMs(level, config?)` for the drop-speed curve, and a `FallingGridSnapshot`/`LockState` shape for the grounded→lock stepper.
1623
+
1624
+ ## Dropped items — `worldItem` and the loot filter
1625
+ A `worldItem` is a scene **entity** (position + item ref + rarity), never an inventory item or object — see the three buckets. `onDeath.dropMode: "world"` (above) is the usual producer; games can also hand-place ground loot (chests, quest drops).
1626
+ ctx.scene.worldItem.spawn({ itemId, position, rarity?, baseType?, count?, affixTier?, source? })
1627
+ ctx.scene.worldItem.get(instanceId) / list() / nearestInRadius(from, radius, filter?)
1628
+ ctx.scene.worldItem.pickup(instanceId, userId) // grants to inventory + despawns, emits worldItem.picked_up
1629
+ Click-to-grab is engine-owned: setting `pointer.grabWorldItems: true` in `defineGame({...})` makes `@jgengine/shell`'s `GamePlayerShell` resolve `pointer.worldHit()` on primary click, and — when the hit entity is a `worldItem` within the `worldItem.pickupRadius` (default `DEFAULT_PICKUP_RADIUS`) configured on `defineGame({...})` of the local player — calls `pickup` directly, no game command needed. `@jgengine/react`'s `useWorldItems()` / `useNearestWorldItem(radius)` drive a HUD pickup prompt off the same store.
1630
+ Presentation is a two-layer render binding, both engine-owned (rendered by `@jgengine/shell`'s `WorldItems`) over **game-supplied data**:
1631
+ 1. **Rarity baseline** — the `worldItem.rarityStyle: Record<rarity, { color?, beam?, label? }>` field of `defineGame({...})`, the game's rarity palette (Borderlands/Diablo-style beam + color coding).
1632
+ 2. **Loot filter overlay** (#33) — the `worldItem.filter: LootFilterRule[]` field of `defineGame({...})`, built with `lootFilter([{ id, when: { rarity?, baseType?, minAffixTier?, maxAffixTier? }, hide?, color?, beam?, label? }])` from `game/lootFilter`. **First matching rule wins** (PoE/Last Epoch block semantics); a rule only overrides the fields it sets, everything else falls back to the rarity baseline. `resolveWorldItemPresentation(item, rarityStyle, rules)` composes both layers and is what the shell calls per item.
1633
+ ## Gear systems — durability, affixes, modular items, storage tiers
1634
+ Four pure primitives that hang off item **instances** (not the stackable catalog id) — all catalog-first (specs are game-supplied config) and renderer-free. Item instances that carry durability/affix/modular state key off a game-assigned instance id, the same way targeting keys off entity instance ids.
1635
+ **Durability** (`item/durability`) — per-instance wear + repair. `DurabilitySpec` (`{ max, wearPerUse?, wearPerHit?, disableAtZero?, repair? }`) is catalog data; `createDurability(spec)` seeds a `DurabilityState`, `wear(spec, state, "use" | "hit", times?)` decrements (floors at 0), `isDisabled(spec, state)` gates use at zero, `durabilityFraction` feeds a HUD bar. Repair is quote-then-apply: `repairQuote(spec, state, { station?, to? })` returns the `{ item, count }[]` material cost (scaled by points restored) + the post-repair state (optional `qualityLossPerRepair` shrinks `max` each repair, Tarkov-style) — the game charges the materials through inventory, then commits the quote's `state`. `createDurabilityTracker()` keeps `DurabilityState` per instance id for the runtime.
1636
+ **Affix roller** (`item/affix`) — procgen `base × rarity → { rolled affixes, computed stats, name }`. `createAffixRoller({ pools, rarities })` over rarity-weighted `AffixPool`s. `roll(base, rarityId, rng)` draws `affixCount` distinct affixes without replacement (weighted, via the engine's `pickWeighted`), computes stats (base × `rarity.statScale`, then `op: "add"` affixes, then `op: "mul"`), and composes a name from `rarity.namePart` + prefix/suffix parts. `rollRarity(rng)` picks a weighted tier; `rollRandom(base, rng)` chains both. Pass `seededRng(seed)` for deterministic drops; any `() => number` rng works (same contract as `loot.roll`). `seededRng` lives in `random/rng` (re-exported here) alongside `seededStreams(seed)`, which derives independent named streams from one seed — `streams("worldgen")` vs `streams("history")` — so simulation draws never perturb generation (intervening in a run cannot change the map).
1637
+ **Modular item** (`item/modularItem`) — a whole assembled from parts in typed mount slots (guns, mechs). `ModularItemDef` has `slots: MountSlotDef[]` (`{ id, accepts, required? }`); `install(def, installed, slotId, part)` validates the slot exists, accepts the part's `category`, and is empty; `computeEffectiveStats(def, installed)` rolls part `stats` (additive) then `multipliers` over `baseStats`; `missingRequiredSlots`/`isComplete` gate a buildable whole. `createModularItem(def)` is the stateful wrapper (`install`/`uninstall`/`effectiveStats`/`partInSlot`).
1638
+ **Storage tiers + insurance** (`inventory/storageTier`) — the extraction-economy inventory half. Inventory containers carry a `tier: "carried" | "banked"` (`InventoryDeclaration.tier`; a Tarkov secure container is just a `banked` container on the body). `partitionOnDeath(containers)` splits a death snapshot into `{ kept, lost }` (banked survives, carried is dropped, stacks merged). `createDeliveryQueue()` is the delayed-delivery (insurance) hook: `schedule` a `ScheduledDelivery` with a game-time `deliverAt`, then `due(now)` / `claimDue(now)` drain it on the tick clock. `insureLost(lost, policy, userId, now, rng?)` filters the lost set to insured items and stamps a delayed `deliverAt` → feed straight into the queue. `resolveConsolation(policy, partition)` returns a baseline loadout id (apply via `applyLoadout`) — the death consolation grant, optionally gated on `if-carried-empty`. *(Session/round machines — extraction hold-to-leave, raid banking — consume this tier; see the objective-machine group.)*
1639
+ ## Objective, round & session machines
1640
+ Content-agnostic state machines for competitive/session shapes — plant/defuse, buy/live/end rounds, downed/revive, the battle-royale ring, extraction raids, run-vs-meta persistence. All pure `core`; every timer takes a **game-time** `dt`/`now` (`ctx.time`), so pause and fast-forward apply for free. Drive them from `loop.onTick` and pipe their events into `ctx.game.feed`/`events`; render their snapshots as HUD (per the UI quality bar in [`reference/ui-react.md`](reference/ui-react.md) — the downed banner, ring warning, and extraction timer are required HUD).
1641
+ **Contested channel** (`session/contestedChannel`) — the interrupt-on-damage progress objective behind plant/defuse, cash-out, urn deposit, banishing, and hold-to-extract. `createContestedChannel({ duration, interruptOnDamage?, resetOnInterrupt?, favorability?, ratePerOccupant?, contested?, decayRate? })`: `start(team)` begins the channel, `tick(dt, occupants)` advances it against per-team occupancy (`Record<teamId, count>`) and emits `start`/`tick`/`contested`/`paused`/`complete` events, `damage(reason?)` interrupts (keeps or zeroes progress per `resetOnInterrupt`). `favorability[team]` scales fill rate (Deadlock deposit); `ratePerOccupant` fills faster with more owners present; `contested: "pause" | "decay"` chooses whether an opposing occupant freezes or reverses progress (The Finals contest). The owner leaving pauses it. Extraction hold-to-leave reuses this primitive verbatim.
1642
+ **Round state** (`session/roundState`) — the buy→live→end match machine (Valorant/CS). `createRoundState({ phases, teams, phaseOrder?, winCondition?, maxRounds?, winReward?, lossBonus? })`: `tick(dt)` runs the phase timer and auto-advances (emitting `phase.start`/`phase.end`, rolling the last phase back into the next round's first), `concludeRound(winner)` records the win on any "conclude-eligible" phase (any phase but the first/last in the cycle), settles `round.economy` (winner gets `winReward`, losers get an escalating `lossBonus` via `lossBonusFor(rule, streak)` clamped to `max`), and moves to the next phase. `onPhaseEnd(hook)` fires commerce/spawn gates on each transition; `match.end` fires at `maxRounds`. `server.mode` stays a game string — this is the timer/economy engine under it.
1643
+
1644
+ Two extras beyond the default buy/live/end cycle: `phaseOrder?: string[]` overrides the phase names/cycle entirely (a wider `Record<string, number>` `phases` shape to match) — a draft→ban→play→score cycle is the same machine with different phase names. `teams: (string | { id, role? })[]` accepts a plain id or a `{ id, role }` pair; `roleOf(team)` reads the tag back (`"attacker"`/`"defender"`, Valorant side assignment) without a parallel lookup table. `winCondition?: (snapshot: RoundSnapshot) => string | null` lets `evaluate()` (call it from `onTick` alongside `tick(dt)`) auto-conclude the round the instant a score/objective condition is met, instead of the game hand-calling `concludeRound` — return a team id to end it, `null` to keep playing; `RoundSnapshot` is `{ round, phase, timeLeft, scores, lossStreaks, roles, matchOver }`.
1645
+ **Role assignment** (`session/roles`) — `assignRoles(players, specs: RoleSpec[])` distributes fixed-count or proportional roles (hider/seeker, spy/operative, prop/hunter) across a player list — the allocation half of an asymmetric session mode; `RoundConfig.teams`' per-team `role` is the lighter-weight alternative when a round machine already tracks the roster.
1646
+ **Downed / revive** (`combat/downed`) — the 3-state alive→downed→dead chain (Apex/Helldivers). `createDownedState({ bleedoutSeconds, reviveSeconds?, reviveHealthFraction?, banner? })`: `down(id)` starts the bleedout, `tick(dt)` counts it down (→ `died`, optionally spawning a `banner`), `revive(id, dt)` accumulates an ally's hold time (→ `revived` with the health fraction the game restores), `finish(id)` executes a downed enemy, and `respawnFromBanner(id)` brings a banner-holder back at a beacon. It sits **in front of** the engine death resolution: on lethal damage call `down` instead of dying; on `died`/`bleedout` run the real `resolveDeath`. No banner ⇒ death is terminal.
1647
+ **Shrinking ring** (`session/ring`) — the battle-royale safe zone with out-of-bounds DoT. A catalog `RingConfig` is `{ center, phases: RingPhase[] }` where each phase is `{ startTime, shrinkDuration, fromRadius, toRadius, damagePerSecond, center? }` on the game clock. `ringSampleAt(config, t)` / `createRing(config).at(t)` returns the live `{ center, radius, damagePerSecond, shrinking }` (radius/center interpolate during each shrink window, hold between phases); `isOutside(t, pos)` / `distanceOutside(t, pos)` test a point, and `damageOutside(t, dt, positions)` returns per-entity `{ id, damage }` for everyone beyond the wall — feed those into `scene.entity.stats.delta`/`effect` each tick.
1648
+ **Extraction session** (`session/extraction`) — the raid-scoped "reach an extract and leave to bank what you carried" wrapper (Tarkov/DMZ/Helldivers), composed from the contested channel + `inventory/storageTier`. `createRaidSession({ extracts, insurance?, consolation? })`: `beginExtract(userId, extractId, team?)` opens a hold-to-leave channel, `tickExtract`/`damage` drive it, and on completion `resolveExtraction(userId, containers)` banks everything carried. `resolveDeath(userId, containers, now, rng?)` runs `partitionOnDeath` (banked kept, carried lost), schedules insured items through the built-in delivery queue (`claimDeliveries(now)` drains it on the clock), and yields the consolation loadout id. `playerSnapshot(userId)` feeds the extraction-timer HUD.
1649
+ **Persistence scopes** (`runtime/persistenceScope`) — the run-vs-meta split with explicit reset boundaries (Icarus mission wipe, Once Human season reset). `partitionScopes(state, { run })` splits a flat record into `{ meta, run }` by key; `resetRun` clears the run half while meta (talents/blueprints/account currency) survives; `clearRunFields(playerRow, runFields)` and `applyRunReset(profile, runFields, now)` do the same over `RuntimePlayerRow`/`PlayerProfileRecord`. `planScenarioReset({ gameId, serverId?, wipeChunks?, wipeServerSession?, resetPlayers?, runFields? })` normalizes a scenario/season reset that `HostPersistence.resetScenario?(reset)` applies — `@jgengine/sql` implements it (deletes the server's chunks + session, run-resets each profile in one transaction), keeping account meta intact.
1650
+
1651
+ ## Trade
1652
+
1653
+ Catalog `trade` fields drive everything — no duplicate price lists.
1654
+
1655
+ ```ts
1656
+ ctx.game.trade.canBuy(itemId, shopId, count?) // → reason | null
1657
+ ctx.game.trade.canSell(itemId, count?)
1658
+ ctx.game.trade.buy(itemId, count, { shop, inventoryId }) // charge → put, rolls back on failure
1659
+ ctx.game.trade.sell(itemId, count, { shop, inventoryId })
1660
+ ctx.game.trade.tradableAt(shopId, allItemIds) // derive stock from catalogs
1661
+ ```
1662
+
1663
+ ## Economy and unlocks
1664
+
1665
+ ```ts
1666
+ ctx.game.economy.balance(userId, currencyId) / grant(...) / charge(...) // charge → { reason } | null
1667
+ ctx.game.unlocks.has(userId, id) / grant(userId, id) / list(userId) / tree(categoryId)
1668
+ ```
1669
+
1670
+ Catalog `requires: [unlockId]` gates validate at command time.
1671
+
1672
+ ## Crafting, tech tree & production
1673
+
1674
+ Four **pure** primitives (no ctx, no renderer) for survival-crafting, tech-tree, factory, and farming games. All are catalog-first: recipes, tech nodes, production rates, and crop stages are game **data** you feed the primitive — the engine owns the graph math, the timers ride `ctx.time` (game-seconds), never wall-clock.
1675
+
1676
+ **Recipe graph** — `@jgengine/core/crafting/recipe`. A `RecipeDef` is `{ id, inputs: RecipeItem[], outputs: RecipeItem[], seconds?, station?, stationRange?, requires? }` — inputs + optional required-workstation-in-range + time → outputs. `craft(state, layout, traits, recipe, context)` consumes inputs and produces outputs on an `InventoryState` **atomically** (rejects `missing-inputs` / `no-station` / `locked` / `no-output-space` without mutating on failure); `canCraft(...)` is the dry-run. `context = { origin?, stations?, unlocked? }`: `stationSatisfied` checks a matching placed workstation (`{ catalogId, position }`) within `stationRange` of `origin`, and `requires` gates on `unlocked(id)` (wire it to `ctx.game.unlocks.has` or the tech tree). `createRecipeGraph(defs)` indexes recipes by `producing(itemId)` / `using(itemId)` / `category`. Long crafts schedule completion with `ctx.time.after(craftSeconds(recipe), …)`.
1677
+
1678
+ **Tech tree** — `@jgengine/core/economy/techTree`. **Generalizes flat `unlocks`, does not duplicate it**: a `TechNodeDef extends UnlockDef` adds `requires` (prerequisite node/unlock ids), an optional `recipe` payload, and `grants` (extra flat unlock ids). A node id **is** an unlock id, so flat unlocks are just tech nodes with no `requires`. `createTechTree(defs)` wraps `createUnlocks` internally and gates grants on prerequisites: `unlock(userId, id)` refuses until every `requires` is met, `available(userId)` is the reachable frontier, `recipes(userId)` lists the recipe payloads a player has unlocked (feed them to the recipe graph). `tree(categoryId)` and per-user `has`/`list`/`snapshot`/`hydrate` mirror `unlocks`.
1679
+
1680
+ **Production building** — `@jgengine/core/crafting/production`. `productionBuilding({ id, inputs, outputs, rate, power?, bufferMultiplier? })` — a placed building that consumes buffered inputs and emits outputs on a timer. `rate` is production **cycles per game-second**; `tickProduction(def, state, { dt, powered? })` advances continuously through `dt` (so pause/fast-forward apply for free) and completes as many cycles as the buffer allows. `feedProduction` / `drainOutput` move items in and out of the internal buffers (a puller/conveyor). `advanceTransport(path, items, dt)` slides items along a belt and splits off `delivered`. `resolvePowerGrid(supply, consumers)` powers demands greedily until supply is exhausted — gate a building's tick on `powered`.
1681
+
1682
+ **Farming** — `@jgengine/core/crafting/crop`. `CropTileState` is a soil state machine (`untilled` → `tilled` → planted); `tillTile` / `plantCrop` / `waterTile` are pure tile transitions and `advanceCropDay(def, tile)` runs the **day tick** — a `CropDef { stages, regrowDays?, needsDailyWater?, harvest? }` advances a growth stage per watered day and sets `harvestable`; `harvestCrop` yields and either clears the tile or resets a regrow crop. `applyToolToTiles(tiles, center, pattern, apply)` applies a tool across a tile pattern under the cursor — `singleTile()`, `squarePattern(r)`, `diamondPattern(r)`, `rectPattern(w,d)` (watering-can / hoe AoE). `createCropField(catalog)` is the stateful wrapper over a tile grid (`till`/`plant`/`water`/`harvest`/`advanceDay`); drive `advanceDay()` off the calendar day rolling over — `createDayTicker(startDay)` reports how many days `ctx.time.calendar().day` has crossed.
1683
+
1684
+ ## `applyLoadout`
1685
+
1686
+ ```ts
1687
+ ctx.player.loadout.register(loadouts) // onInit
1688
+ ctx.player.applyLoadout(userId, loadoutId) // → null | { reason }
1689
+ ```
1690
+
1691
+ `LoadoutDef = { inventories?: { hotbar: [{ item, count, slot? }], … }, stats?, economy?, unlocks? }`. Application is **all-or-nothing**: every inventory put dry-runs first; any rejection applies nothing. Starter kits gate on `ctx.player.isNew`; class/respawn kits run from commands. Never scatter raw `put`/`grant` calls for a kit.
1692
+
1693
+ ## Quests
1694
+
1695
+ ```ts
1696
+ ctx.game.quest.register(catalog) // onInit
1697
+ canAccept / accept / abandon / canTurnIn / turnIn / grant / revoke
1698
+ progress(userId, questId, objectiveId, delta)
1699
+ list(userId) / has(questId)
1700
+ bind("entity.died") // kill objectives match objective.target === catalogId
1701
+ bind("inventory.added") // collect objectives match objective.item
1702
+ ```
1703
+
1704
+ Catalog: `{ id, title, giver?, turnIn?, requires?, objectives: [{ id, kind, target?/item?, count, partyShare? }], rewards? }`. `requires` is satisfied by a completed quest of that id or an unlock. `turnIn` applies declarative `QuestRewards` — `{ xp?: { amount }, economy?: Record<string, number>, items?: { item, count, inventory }[], unlocks?: string[], quests?: string[] }` — note `xp` takes an `{ amount }` wrapper (applied via `stats.delta` + your level-up loop) and each reward `item` names the `inventory` it fills; chained `quests` are auto-offered if acceptable. Events: `quest.accepted` / `quest.updated` / `quest.completed`. `partyShare: { radius, credit: "all" | "tagger" }` extends kill credit to nearby party members.
1705
+
1706
+ ## Social
1707
+
1708
+ ```ts
1709
+ ctx.game.social.friends.canRequest / request / accept / decline / remove / block / list / requestsFor // persisted
1710
+ ctx.game.social.party.register({ maxMembers }) // then canInvite / invite / accept / decline / kick / leave / promote / list / membersOf / invitesFor
1711
+ ctx.game.social.presence.get(userId) // { online, serverId?, zoneId?, instanceId? }
1712
+ ctx.game.social.emotes.play(fromUserId, emoteId, radius?) // → { from, emoteId, at, recipients } | { reason }
1713
+ ctx.game.social.worldInvites.invite(fromUserId, toUserId, { serverId, joinCode? }) // then canInvite / accept / decline / listFor
1714
+ ```
1715
+
1716
+ Party is ephemeral session state (invites expire; leader leaving promotes the next member). Events: `social.friend.added`, `social.party.joined`, `social.party.left`.
1717
+
1718
+ **World invites** bridge friends and `multiplayer/matchmaking`: an invite carries the `{ serverId, joinCode? }` of the session you're in (the same fields as a `SessionListing`); `accept(userId, inviteId)` → `{ target }` is the join target you hand to your backend's `joinServer`/`joinByCode` — the invite never joins anything itself. Invites are ephemeral like party invites (TTL via `SocialDeps.worldInviteTtlMs`, default 60s; blocked users can't invite either direction). Events: `social.world.invited`, `social.world.accepted`. React: `useWorldInvites()` lists pending invites for the local player.
1719
+
1720
+ `emotes.play` reuses `scene.entity.inRadius` to find nearby **player**-role entities (default radius 20) and emits `emote.played` — never build a parallel proximity broadcast. Emote ids are game-defined strings (no registration, same convention as effect ids). Bind it into the existing feed primitive for a HUD feed: `ctx.game.feed.bind("emote.played")` + `useFeed({ action: "emote.played" })` — no dedicated emote hook exists or is needed.
1721
+
1722
+ ## Chat
1723
+
1724
+ ```ts
1725
+ ctx.game.chat.send(fromUserId, channelId, body) // → { message, recipients } | { reason }
1726
+ ctx.game.chat.whisper(fromUserId, toUserId, body) // stable per-pair channel "whisper:<a>:<b>"
1727
+ ctx.game.chat.history(channelId, { limit?, viewerUserId? }) // viewer filter drops blocked senders
1728
+ ctx.game.chat.register({ id, kind, radius?, historyLimit?, rateLimit? }) // custom channels
1729
+ ctx.game.chat.channels() / snapshot() / hydrate(data)
1730
+ ```
1731
+
1732
+ Built-in channels: `global` (everyone), `party` (reuses `social.party.membersOf`; rejects "not in a party"), `proximity` (reuses the same spatial/entity seam as emotes, default radius 20, **player**-role entities only). `kind` picks the recipient resolution; custom channels pick one of the three kinds. Sends are trimmed, capped (500 chars), and rate-limited per user per channel (default 10/10s, sliding window — `createChatRateLimiter` is the reusable pure primitive). Mute rides social's blocked set: blocked pairs can't whisper, and blocked senders are dropped from party/proximity recipients and from `history` when `viewerUserId` is passed. Every send emits `chat.message` (recipients omitted = broadcast). History is a bounded ring per channel (default 100) with `snapshot`/`hydrate` like `Friends`.
1733
+
1734
+ **Remote chat seam** (`multiplayer/chatContract`): `ChatTransport` is the hook-shaped contract (`useMessages(channelId | "skip")` / `useActions()`, identity-stable like `PresenceTransport`); `ChatSync` is the callback shape for backends that can't host React hooks. Bindings: ws — `createWsBackend(...).chatSync` / `.chatSyncFor(serverId)` over `chatSend` frames + a `chat` update channel (host relays per-channel rings, validates length + rate limit); Convex — `@jgengine/convex/convexChatTransport` `createConvexChatTransport({ messages, sendMessage })` (one live query + one mutation); local/dev — `createLocalChatTransport()`. React lifts a `ChatSync` via `chatTransportFromSync`.
1735
+
1736
+ ## Cosmetic loadout
1737
+
1738
+ ```ts
1739
+ ctx.player.cosmetics.register(defs) // onInit — Record<loadoutId, { slots: Record<slot, cosmeticId> }>
1740
+ ctx.player.cosmetics.apply(userId, loadoutId) // merges the preset's slots
1741
+ ctx.player.cosmetics.equip(userId, slot, cosmeticId | null) // set/clear one slot directly
1742
+ ctx.player.cosmetics.get(userId) // Record<slot, cosmeticId>
1743
+ ```
1744
+
1745
+ A per-player appearance layer distinct from `applyLoadout` (which grants inventory/stats/economy/unlocks) — cosmetics never touch gameplay state, only equipped slot ids for your renderer to read. Emits `cosmetics.changed`.
1746
+
1747
+ ## Possession
1748
+
1749
+ ```ts
1750
+ ctx.player.possession.own(userId, entityId) / disown / owns / listOwned(userId)
1751
+ ctx.player.possession.active(userId) // → entityId, defaults to userId itself
1752
+ ctx.player.possession.possess(userId, entityId) // → null | { reason } — must be owned + spawned
1753
+ ```
1754
+
1755
+ A player can own N scene entities (party members, vehicles, a possessed creature) and control exactly one at a time — distinct from `game.social.party`, which is a social grouping, not a control model. `possess` flips the previous/next entity's scene `EntityRole` between `"player"`/`"npc"` and emits `possession.swapped`; `@jgengine/shell`'s `GamePlayerShell` reads `active(userId)` every frame to rebind WASD movement, tab-targeting, hotbar `from`, and the camera rig's `followEntityId` to whichever entity is currently controlled — a game never wires this rebind itself.
1756
+
1757
+ ## Form / shapeshift
1758
+
1759
+ ```ts
1760
+ ctx.scene.entity.form.register(defs) // onInit — FormDef[] = { id, movement?, abilities?, model? }
1761
+ ctx.scene.entity.form.shapeshift(instanceId, formId, durationSeconds?) // → null | { reason }
1762
+ ctx.scene.entity.form.active(instanceId) // → formId | null
1763
+ ctx.scene.entity.form.abilities(instanceId) // → readonly string[] | null
1764
+ ctx.scene.entity.form.revert(instanceId) // early revert
1765
+ ```
1766
+
1767
+ A `form` bundles movement params + an ability-id list + a mesh into one swappable unit (shapeshift/transformation — V Rising bear/wolf/bat, Wukong's boss transformation). `model` reuses the entity's catalog `name` (the same key `entityModels`/`entitySprites` resolve against), so the mesh swap rides the existing render lookup — no parallel mesh field. `durationSeconds` is **game time**: it schedules the automatic revert through `ctx.time.after`, so it obeys pause and fast-forward like everything else on the clock. Emits `form.changed`.
1768
+
1769
+ ## Events, feed, leaderboard
1770
+
1771
+ ```ts
1772
+ ctx.game.events.on(name, handler) // register in onInit; typed GameEventMap
1773
+ ctx.game.feed.bind(action) // pipe an engine event into a ring buffer (default 20)
1774
+ ctx.game.feed.push(action, entry) // manual channels (chat, crafting)
1775
+ ctx.game.feed.recent(action, { limit? })
1776
+ ctx.game.leaderboard.track({ stat, scope: "global" | "server" | "profile" }) // onInit
1777
+ ctx.game.leaderboard.increment(userId, stat, { scope, by? }) / getTop / getProfile
1778
+ ```
1779
+
1780
+ `ctx.game.commands.define(name, { validate?(ctx, input), apply(ctx, input) })` registers a verb (`has`/`names`/`run` round it out); `run(name, input)` returns `{ status: "applied", state } | { status: "rejected", reason } | { status: "unknown-command" }`. `apply` may either **return** the next state (the classic reducer shape) or mutate `ctx` in place and return **nothing** — `run` keeps the current `ctx` as `state` when `apply` returns `void`, so a handler that only calls other `ctx` methods (spawn, effect, loot.grantToPlayer, …) doesn't need a pointless `return ctx`. **Event handlers use `ctx` directly** the same way (side effects: leaderboard, economy, scheduling) and never reassign state. One feed primitive for kill feeds, loot logs, quest updates — no per-domain feed hooks.
1781
+
1782
+ ## `ctx.game.store` — reactive game state
1783
+
1784
+ ```ts
1785
+ ctx.game.store.set("health", 100) // any key, any value type
1786
+ ctx.game.store.get("health") // T | undefined
1787
+ ctx.game.store.has("health")
1788
+ ctx.game.store.delete("health")
1789
+ ctx.game.store.subscribe(listener) // change-signal fires on set/delete
1790
+ ctx.game.store.mapSnapshot() / arraySnapshot()
1791
+ ```
1792
+
1793
+ 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`.
1794
+
1795
+ ## `ctx.game.cards` / `ctx.game.turn` — lazily-created piles and turn loops
1796
+
1797
+ `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.
1798
+
1799
+ ## Movement, pose, input
1800
+
1801
+ ```ts
1802
+ ctx.player.movement.getPose(id) / setPose(id, "crouch") // validates catalog movement.poses
1803
+ ctx.player.movement.getAim(id) / setAim(id, "ads") // ADS = aim state + zoom modifier, not a pose
1804
+ ```
1805
+
1806
+ 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.
1807
+
1808
+ ### `ctx.input` — polling the raw controls
1809
+
1810
+ `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.
1811
+
1812
+ **`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).
1813
+
1814
+ **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()`.
1815
+
1816
+ ### Controller kinematics — movement config, physics tuning, respawn
1817
+
1818
+ `PlayableGame.movement` (`PlayerMovementConfig`) tunes the shell's built-in walk controller (never touch `PhysicsWorld` for ordinary player movement):
1819
+
1820
+ ```ts
1821
+ movement: {
1822
+ mode?: "free" | "axis" | "grid"; // "free" (default) camera-relative; "axis" locks travel to one world axis; "grid" snaps each committed position to cell centers
1823
+ axis?: "x" | "z"; // world axis for mode "axis". Default "x"
1824
+ cellSize?: number; // cell size for mode "grid". Default 1
1825
+ collideObjects?: boolean; // collide the walking player against placed scene objects (unit-box AABBs) even without collision.voxel
1826
+ 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
1827
+ }
1828
+ ```
1829
+
1830
+ `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.
1831
+
1832
+ `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.
1833
+
1834
+ **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).
1835
+
1836
+ **`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-newgame` → voxel trapdoor board.
1837
+
1838
+ **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).
1839
+
1840
+ ## Touch & mobile
1841
+
1842
+ 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.
1843
+
1844
+ Refine the derived scheme with the `touch` field of `defineGame({...})` (`TouchControlsConfig`, all optional):
1845
+
1846
+ ```ts
1847
+ touch: {
1848
+ gestures: {
1849
+ tap: "rotateCw",
1850
+ swipeUp: "hold",
1851
+ swipeDown: "hardDrop",
1852
+ drag: { left: "shiftLeft", right: "shiftRight" },
1853
+ },
1854
+ buttons: [
1855
+ { action: "rotateCcw", label: "CCW" },
1856
+ { action: "softDrop", label: "Soft" },
1857
+ ],
1858
+ },
1859
+ ```
1860
+
1861
+ - **`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.
1862
+ - **`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.
1863
+ - **`hidden`** — actions to drop from the derived buttons without gesture-binding them.
1864
+ - **`movement: false`** — suppress the virtual joystick even when movement actions are bound.
1865
+ - **`look` / `lookSensitivity`** — drag-to-look on the play surface; defaults to `true` for `first`-person camera rigs, `0.005` radians/px.
1866
+ - **`touch: false`** — opt out entirely when the game's own DOM UI is already touch-native.
1867
+
1868
+ `useDisplayProfile()` (`@jgengine/react/display`) reports `{ coarsePointer, compact, portrait }` — live media-query state, SSR-safe — for adaptive HUD layout; see the mobile/touch rules in [`reference/ui-react.md`](reference/ui-react.md).
1869
+
1870
+ ## Interaction — `proximityPrompt`
1871
+
1872
+ 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.
1873
+
1874
+ ## Pointer-driven input and navigation
1875
+ 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.
1876
+ - **`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.
1877
+ - **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.
1878
+ - **`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.
1879
+ - **Selection math** (`scene/selection`) is pure and testable: `createSelectionSet()`, `screenRect`/`selectWithinRect`/`isMarquee` over projected screen points.
1880
+ - **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).
1881
+ - **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.
1882
+ - **`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.
1883
+ - **`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.
1884
+ ## AI — director, threat, jobs, crowds (`ai/*`)
1885
+ 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.
1886
+ - **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.
1887
+ - **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. `ranked()` for a threat meter.
1888
+ - **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`.
1889
+ - **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.
1890
+ - **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.
1891
+ ## Map, fog of war & ping
1892
+ 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.
1893
+ - **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.
1894
+ - **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`.
1895
+ - **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.
1896
+ - **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 }`.
1897
+ - **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.
1898
+ ## Sensors, vision & observer tools (`sensor/`)
1899
+ 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`.
1900
+ | Primitive | Answers |
1901
+ |-----------|---------|
1902
+ | `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 |
1903
+ | `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 |
1904
+ | `projectToView(camera, point)` → `FrustumProjection` | Pure camera-frustum projection (no three.js) — `inView`, `screenX/screenY` (-1..1), `distance` |
1905
+ | `framingScore(projection, config?)` → `number` | 0..1 framing quality from screen-center placement + distance-to-ideal (photo-mode "is this subject framed", #117) |
1906
+ | `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) |
1907
+ | `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 |
1908
+ | `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` |
1909
+ | `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 |
1910
+ 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.
1911
+
1912
+ ## World features
1913
+
1914
+ 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/world.md](reference/world.md)**.
1915
+
1916
+ ## Turn-based & tactics (renderer-free)
1917
+
1918
+ Pure-`core` primitives for turn-based, grid-tactics, and card games — every one is a stateful factory with matching pure math, and every stateful piece exposes `capture()`/`restore()` so it plugs straight into the snapshot store. Overlays and tile art are the shell's/game's job; these ship the logic.
1919
+
1920
+ - **`turn/turnLoop` — `createTurnLoop(config)`.** An initiative machine over an ordered participant list with optional `phases` and per-turn action-economy `pools`. `advanceTurn()` walks the order (round++ on wrap) and **resets the entering participant's pools**; `advancePhase()` steps phases then rolls into the next turn. Pools are catalog data (`{ id, max, start? }`) — a single Slay-the-Spire energy pool or BG3's Action/Bonus/Movement/Reaction set, spent independently via `spend/canSpend/gain/refill`. `setOrder`/`addParticipant`/`removeParticipant` re-roll initiative without losing the active pointer. `config.onTurnStart?(participantId)`/`onTurnEnd?(participantId)` fire on every `advanceTurn()` transition (start also fires once for the initial participant at construction) — hang status-effect ticks, "your turn" banners, or AI-turn kickoff here instead of diffing `state()` between ticks yourself. `ctx.game.turn.loop(id, config?)` is the runtime-wired accessor: lazily creates (config required the first call) or returns the existing notify-wrapped loop for `id`, so a HUD bound to `ctx.subscribe` re-renders on every turn/phase/pool change with no separate store to wire.
1921
+ - **`turn/commit` — `createCommitController({ mode })`**, also hosted at `turnLoop.commit`. Three commit modes: `immediate` (submit resolves now), `simultaneous` (sealed hidden submissions → `reveal()` once `allReady()`, deterministic order — Marvel Snap), and `rewind` (visible `pending()` → `rewind()` to discard or `commit()` to finalize).
1922
+ - **`turn/intent` — `createIntentBoard()`.** A minimal per-participant "what will you do" board, lighter than a full commit round: `declare(participantId, { kind, magnitude?, targetId?, note? })` records one intent per participant (overwriting any prior undeclared one), `peek(participantId)` reads without clearing, `all()` lists every declared `[participantId, intent]` pair (for an enemy-intent HUD row, Slay-the-Spire style), `consume(participantId)` reads and clears in one call, `clear(participantId?)` clears one or everyone. Reach for this when you need visible declared-but-not-yet-resolved intents (telegraphed enemy actions) without the full simultaneous-reveal machinery of `turn/commit`.
1923
+ - **`tactics/tacticalGrid` — `createTacticalGrid({ width, height, blocked?, diagonal?, world? })`.** Tile occupancy (one unit per tile), `reachable(from, budget)` flood-fill (respects walls + occupants), `path(from, to)` shortest route, and `push(id, dir, { distance, chain })` discrete knockback-to-tile — chained collisions transfer momentum through struck units (Into the Breach), or stop with a recorded `PushCollision` against `wall`/`edge`/another unit. `world: { origin: [x, z], tileSize }` (mirroring `navGrid`'s bounds+cellSize convention) turns on `worldToTile(x, z)` (world point → `Tile | null`, null outside the grid) and `tileToWorld(tile)` (a tile's world-space center) — the render/pointer-hit round trip between the tactics grid and the 3D scene; omit `world` for a grid used purely as abstract logic (both throw if called without it).
1924
+ - **`tactics/predictiveQuery` — `predictAreaEffect`/`predictArcEffect`/`predictTiles`.** A "would-this-effect-hit" query for pre-commit overlays and enemy-intent telegraphs. It reuses the **exact** AoE/LoS targeting behind `ctx.scene.entity.effect` (`combat/effects` `resolveAreaTargets`) so the predicted target set matches what the effect would actually drain — without committing any state change.
1925
+ - **`tactics/snapshot` — `createSnapshotStore()`.** Cheap, repeatable turn-undo: `register(id, slice)` any `capture()/restore()` slice (the grid, surfaces, and turn loop all qualify), then `capture()/restore()` a deep-cloned snapshot or use the `push()/pop()` undo stack. `deepClone` handles objects/arrays/Map/Set so a held snapshot is immune to later mutation.
1926
+ - **`tactics/surface` — `createSurfaceLayer({ kinds, reactions })`.** A stateful tile surface layer with its own `tick(dt)` (timed surfaces decay + expire) and a **combination matrix** — `reactions` is data (`{ when: [a, b], result }`), so grease+fire→fire and water+lightning→electrified are catalog entries, not hard-coded. Distinct from terrain/water; drive its tick from `onTick`'s game-time `dt`.
1927
+
1928
+ ## External data — `data/dataSource` and the dev proxy
1929
+
1930
+ 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.
1931
+
1932
+ - **`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.
1933
+ - **`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.
1934
+ - **`createJsonDataSource<T>(url, options?)`** (`data/jsonDataSource`) — sugar combining the two above: a `DataSource<T>` whose `load` calls `fetchJson(url, options)`.
1935
+ - **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.
1936
+
1937
+ ## Multiplayer and the backend seam
1938
+
1939
+ The transport/host/persistence seam — `createWsBackend`, protocol codec, browser-safe authoritative host + router, WebRTC P2P, the Node/Convex/SQL adapters, presence, and save cadence. Full surface: **[reference/multiplayer.md](reference/multiplayer.md)**.
1940
+
1941
+ ## UI — `@jgengine/react`
1942
+
1943
+ The React layer — `GameProvider`, the hooks table, headless className-passthrough primitives (incl. map components), the identity/chat/voice/social/drag-layer kits, the shadcn registry install path for visual HUD components (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), the screen-layout rule, and the **UI quality bar** (required, not optional polish). Full surface: **[reference/ui-react.md](reference/ui-react.md)**.
1944
+
1945
+ ## Devtools — F2 overlay and tunables
1946
+
1947
+ `@jgengine/shell`'s `GamePlayerShell` mounts an F2-toggled debug overlay (`shell/src/devtools/DevtoolsOverlay.tsx`) over every game automatically — `defineGame({ devtools: false })` is the only way to turn the toggle off (default `true`). Five tabs:
1948
+
1949
+ | Tab | Shows |
1950
+ |-----|-------|
1951
+ | Perf | fps, frame/sim ms, draw calls, triangles, entity/object counts, state notifies/s, registered probes |
1952
+ | Tune | every discovered tunable — checklist grouped by source file or table export name; check one to control it live |
1953
+ | Logs | captured `console.log`/`info`/`warn`/`error` |
1954
+ | Net | observed backend round-trip latency (fed by `instrumentLatency`) |
1955
+ | Keys | the game's `ActionCodesMap` bindings |
1956
+
1957
+ **Tunables are zero-annotation.** Write plain code under `Games/<id>/src/**` — no import, no wrapper — and it's discoverable:
1958
+
1959
+ ```ts
1960
+ // loop.ts — top-level export const, nothing else
1961
+ export const GRAVITY = -22;
1962
+ export const SKY_COLOR = "#87ceeb";
1963
+ export const GOD_MODE = false;
1964
+
1965
+ // game/content.ts — an exported flat table of numbers/booleans/colors
1966
+ export const TUNING = { reach: 6, spawnRate: 0.4, fogColor: "#334455" };
1967
+ ```
1968
+
1969
+ The dev runner's Vite plugin, `tunableDiscoveryPlugin` (`@jgengine/core/devtools/transformTunables`, wired in `apps/dev/vite.config.ts`), rewrites each top-level `export const <number|boolean|"#hex">` literal to `export let` and binds it into the devtools registry as the module loads (`transformTunableExports` is the pure string transform underneath; `tunableModuleTable(id)` derives the table id from the file path, skipping `main.tsx` and `*.test.*`). Table exports need no transform at all — after each game module loads, the dev app calls `devtools.discover.scanModule(moduleExports)`, which walks every export's own properties for a flat plain-object table of numbers/booleans/`"#rrggbb"` strings.
1970
+
1971
+ F2 → Tune tab lists every discovered entry as a checklist, grouped by source file (top-level constants) or by table export name (object tables). Checking an entry hands it a live slider/toggle/color picker; unchecking resets it to its initial value. Kind is inferred from the value: `number` → slider, `boolean` → toggle, a `"#rgb"`/`"#rrggbb"`/`"#rrggbbaa"` string → color.
1972
+
1973
+ **Liveness.** An edit applies live wherever the code reads the constant/table entry at use time. A value captured once at init — passed into a function call, baked into worldgen — only picks up the new value on reload. Overrides persist in `localStorage` per game (key `jg-devtools:<game name>`) and are re-applied *before* `loop.onInit` runs, so even an init-baked constant respects its override after a refresh — *if* the read happens at or after `onInit`. A read that happens earlier than that (see below) never sees the override, reload or not.
1974
+
1975
+ **Default assumption: almost every gameplay number, boolean, and color is a tunable, not a hardcoded fact.** Walk speed, jump height, gravity, damage, cooldowns, spawn rates, drop chances, radii, durations, thresholds, multipliers, colors — if it's a scalar a designer would plausibly want to nudge while playing, it belongs in a place discovery can see (a top-level `export const`, or a direct scalar field on a catalog def object like `PlayerDef`/`EnemyDef`) — never buried as a bare literal inside a deeper nested object with no named export, and never computed once and thrown away. Treat "should this be tunable" as opt-out, not opt-in.
1976
+
1977
+ **Catalog-derived content must read fields live, not bake them at import time.** A common trap: a `content.ts` (or any module implementing `GameContextContent`) that loops over a catalog array *once at module scope* and copies scalar fields into a separately cached `Map`:
1978
+
1979
+ ```ts
1980
+ // WRONG — copies walkSpeed by value at import time, before devtools can even scan exports
1981
+ const entityEntries = new Map<string, GameContextEntityEntry>();
1982
+ for (const p of players) {
1983
+ entityEntries.set(p.id, { stats: p.stats, movement: { poses: p.poses, walkSpeed: p.walkSpeed } });
1984
+ }
1985
+ export const content: GameContextContent = {
1986
+ entityById: (id) => entityEntries.get(id) ?? null,
1987
+ };
1988
+ ```
1989
+
1990
+ This runs during module import — earlier than `discoverGameTunables`/override-application in `apps/dev/src/main.tsx`, and earlier than any `loop.onInit`. The catalog object (`p`) still gets live-mutated by devtools, but nothing ever re-reads it, so the baked `walkSpeed` is permanently stale: no F2 edit and no persisted override ever reaches gameplay, reload or not.
1991
+
1992
+ ```ts
1993
+ // RIGHT — map ids to the catalog def itself; build the entry fresh on every lookup
1994
+ const playersById = new Map(players.map((p) => [p.id, p]));
1995
+ function entityById(id: string): GameContextEntityEntry | null {
1996
+ const p = playersById.get(id);
1997
+ return p === undefined ? null : { stats: p.stats, movement: { poses: p.poses, walkSpeed: p.walkSpeed } };
1998
+ }
1999
+ export const content: GameContextContent = { entityById };
2000
+ ```
2001
+
2002
+ Same shape, same call site — the only change is *when* `p.walkSpeed` is read: at lookup time (each spawn) instead of once at import. Objects (`stats`, `receive`) are already reference-safe to pass through either way; this only matters for scalars (numbers/booleans/strings) copied out of a catalog def.
2003
+
2004
+ **`tunable()` still exists — an optional low-level primitive, not the recommended path.** Reach for it only when you need explicit bounds, an `options` select, or a change subscription that discovery can't infer:
2005
+
2006
+ ```ts
2007
+ import { tunable } from "@jgengine/core/devtools/devtools";
2008
+
2009
+ const gravity = tunable("physics/gravity", -22, { min: -60, max: 0 });
2010
+ ```
2011
+
2012
+ Read `gravity.value` at use time (or `gravity.subscribe(listener)`) — never destructure once at module load. A `"group/label"` name (e.g. `"physics/gravity"`) groups the control under `group` in the Tune tab; `devtools.controls.register` is the same call underneath. Real example: `Games/voxel-mine/src/loop.ts` — `tunable("mining/reach", REACH, { min: 2, max: 16, step: 1 })`, read via a getter passed to `createEditorHandlers`.
2013
+
2014
+ **Agent loop.** The overlay's "Copy report" button copies a JSON `DevtoolsSnapshot`; from a browser session an agent can instead call `window.__JG_DEVTOOLS.snapshot()` directly (or `snapshotDevtools()` from game code) for the same shape — frame stats, render sample, latency stats, captured logs, probe values, every registered control's current + initial value, and a `discovered` array (`id`, `kind`, `value`, `enabled`) covering every auto-discovered tunable whether or not it's enabled — a single call to check "is this actually working" without a screenshot. `window.__JG_DEVTOOLS.discover` is exposed directly too (`list`/`enable`/`disable`/`bind`/`scanTable`/`scanModule`/`clear`) so an agent can flip a discovered tunable on and read/write it from the console without touching the UI.
2015
+
2016
+ `devtools.probes.register("name", () => value)` (`@jgengine/core/devtools/devtools`) surfaces a game-specific gauge (entity count, queue depth, whatever) in both the Perf tab and the snapshot; call the returned unregister function to remove it.
2017
+
2018
+ ## Assets — real art from day one
2019
+
2020
+ Squares as enemies, colored boxes as buildings, and a flat grid floor read as *broken*, not unfinished. The blueprint's **Asset plan** names the packs before the first edit; a pass does not end while any default-material primitive, unstyled ground plane, or debug grid is visible in the staged screenshot — and that includes 2D HUD art: a first-letter tile, emoji, or one generic shape reused per slot is a placeholder exactly like a graybox enemy. Primitive stand-ins are allowed only *mid-pass* as scaffolding.
2021
+
2022
+ **Sources** (CC0 — public domain, commercial use, no attribution — unless noted):
2023
+
2024
+ | Source | What you get |
2025
+ |--------|--------------|
2026
+ | [Kenney.nl](https://kenney.nl) | 40,000+ CC0 assets: characters, buildings, nature, vehicles, weapons, UI, audio — the broadest single library |
2027
+ | [Quaternius](https://quaternius.com) / [KayKit](https://kaylousberg.itch.io) | CC0 low-poly packs incl. **rigged + animated characters**: medieval, sci-fi, dungeons, animals, adventurers |
2028
+ | [Poly Haven](https://polyhaven.com) / [ambientCG](https://ambientcg.com) | CC0 PBR textures, HDRIs, materials — the floor comes from here, never a flat color |
2029
+ | [Poly Pizza](https://poly.pizza) | Search engine over thousands of CC0 low-poly models for one specific thing |
2030
+ | [Game-Icons.net](https://game-icons.net) (CC BY 3.0 — credit it) / Kenney UI packs (CC0) | 4,000+ item/ability **icon silhouettes** — the registry `game-icon` item covers common HUD glyphs first |
2031
+ | [itch.io CC0 3D tag](https://itch.io/game-assets/assets-cc0/tag-3d) / [OpenGameArt](https://opengameart.org) | Long tail — **check the license per asset**, CC0 filter first |
2032
+ | [Mixamo](https://www.mixamo.com) | Free humanoid animations (Adobe license — fine for shipped games, not CC0) |
2033
+ | Kenney audio / [freesound CC0 filter](https://freesound.org) | Hit sounds, UI clicks, ambience |
2034
+
2035
+ **Rules:**
2036
+
2037
+ 1. **One style family per game.** Kenney + Quaternius + KayKit low-poly mix fine; low-poly models on photoreal PBR ground reads broken. Name the family in the blueprint.
2038
+ 2. **License discipline.** CC0 needs nothing; anything else gets a line in `src/game/assets-credits.md` (source, author, license). Never ship an asset you can't name the license of.
2039
+ 3. **Wire through the engine seams.** GLB models live in the game's `src/game/assets.ts` render catalog keyed by catalog id; billboards via `entitySprites`, real meshes via `entityModels`/`objectModels` in `defineGame({...})`; ground/skies belong to the world layer. Catalog `model` fields reference asset keys — never file paths in game logic. Source models through **`@jgengine/assets`** (`buildCatalog({ basePath })` → resolve ids/aliases → urls); `pull` packs into your app's `public/models/` (extracts Kenney's shared `Textures/` alongside the GLBs so models render textured). Network-restricted: `pull` falls back through `--mirror <baseUrl>` / `JGENGINE_ASSETS_MIRROR`, and `--offline` fails fast — see the package README for the fallback order and add/import flow.
2040
+ 4. **Coverage follows the content budget.** Every entity family, placed object, and held item maps to a real asset *before* the catalog entry ships. If the pack lacks a model, restyle the noun to one it has — rename the fantasy, don't ship a cube.
2041
+ 5. **Scale/pivot sanity.** `@jgengine/assets` measures each model's footprint/center/`minY` at reindex and ships them on the catalog entry (`catalog.resolve(id).dims`); with `objectModels` anchor `"center"` (the default) the shell centers the footprint on the placement point and ground-snaps the lowest vertex — so `object.place(id, cellX, y, cellZ, { rotation })` renders centered + grounded with no pivot math and no `dimensions.ts`. Anchor `"origin"` opts back into the raw GLB origin. Check the first placement of each model against its catalog `footprint`; one wrong pivot repeated 100 times is a rebuild.
2042
+ 6. **Item and ability icons are assets too.** Every hotbar/inventory/ability slot renders a real, distinct icon — the registry `game-icon` catalog (`iconForItemId`/`iconForAction`) or a Game-Icons/Kenney silhouette — per the UI quality bar's real-icons rule.
2043
+
2044
+ ## Genre cheat sheet
2045
+
2046
+ - **Voxel/crafting**: objects for blocks/machines, `voxel()`, `object.break`/`object.placeFromInventory`.
2047
+ - **Tycoon/lab**: objects + `slotInventory`, `plots()`, configure via prompt → command.
2048
+ - **Shooter**: `fireProjectile`/`settleProjectile`; grenades settle → `effect({ at, radius })`; `movement.poses`/`aim` + zoom modifier; `servers({ … })` + game-owned `server.mode`; loadout classes from commands.
2049
+ - **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"`.
2050
+ - **All combat games**: react to `entity.died` (feed/leaderboard/score) — never poll HP.
2051
+
2052
+ ## Anti-patterns
2053
+
2054
+ | Wrong | Right |
2055
+ |-------|-------|
2056
+ | Player tuning in `defineGame` | Entity catalog `movement` + stats |
2057
+ | `behaviors: […]` on place/spawn | Catalog entry |
2058
+ | Engine `weapon.fire` / `consumable.use` / `combat.*` | `item.use` + catalog `use` → game handler |
2059
+ | `ItemUseInput.to` for targets | `getTarget(from)` in handlers |
2060
+ | `effect({ to })` for gunshots | `fireProjectile` + `settleProjectile` |
2061
+ | Polling HP in `onTick` for kills | `entity.died` event |
2062
+ | `combat.lootTable` / `loot.enemy` | `onDeath` on the entity that died |
2063
+ | Hand-rolled `Math.random()` loot in commands | `lootTable()` + `ctx.game.loot.roll` |
2064
+ | Hand-rolled `xpForLevel`/`levelFromXp` | `game/progression` `curve()` + `leveling()` |
2065
+ | Hardcoded shop arrays | `item.trade.shops` + `tradableAt` |
2066
+ | Kit seeding via scattered `put`/`grant` | `applyLoadout` |
2067
+ | Per-user quest state hand-rolled | `game.quest.register` + binds |
2068
+ | `useKillFeed` / per-domain feed hooks | `useFeed({ action })` |
2069
+ | Raw keys in game logic | `defineGame` input actions |
2070
+ | Positioning inside `ui/components/` or on primitives (`CurrencyPill className="absolute …"`) | Screen wrappers in `GameUI.tsx` only |
2071
+ | Game UI classes without `@source` in host CSS | `@source` entries for your game dirs + `node_modules/@jgengine/{shell,react}` |
2072
+ | One file per catalog entry / per brand | Dense `<domain>/catalog.ts` |
2073
+ | Convex mutations called from game code | `commands.run` through the `GameBackend` transport |
2074
+ | 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-newgame`) |
2075
+ | 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 |
2076
+ | Game nouns in this skill | Engine primitives + placeholder ids only |
2077
+
2078
+ ## New-game definition of done
2079
+
2080
+ This is a gate, not a suggestion — every box, in one pass (workflow: **`jgengine-newgame`** 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.
2081
+
2082
+ - [ ] `game.config.ts` (`defineGame` from `@jgengine/shell/defineGame`) + `index.tsx` (barrel) + `main.tsx` (standalone host) + `loop.ts` + `game/content.ts`
2083
+ - [ ] Catalogs: `game/entities/<role>/catalog.ts`, `game/items/<domain>/catalog.ts`, `game/objects/catalog.ts`; loot tables beside their domain
2084
+ - [ ] Entity `stats` + `receive` orders aligned on the same stat ids; `role` set (drives targeting + camera)
2085
+ - [ ] `game/items/use-handlers.ts` registered in `onInit`; handlers read `getTarget`/`aim`, never a target input
2086
+ - [ ] `game/loadouts.ts` + `applyLoadout` in `onNewPlayer` (gated on `isNew`)
2087
+ - [ ] `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**
2088
+ - [ ] `onInit`: register handlers/loadouts/loot/quests, event listeners, feed binds, leaderboard tracks; `setupWorld`
2089
+ - [ ] Player spawns with `id === ctx.player.userId`
2090
+ - [ ] `game/ui/GameUI.tsx` owns layout; components use `@jgengine/react` hooks
2091
+ - [ ] UI passes the **quality bar** above (contrast, scale, framing, genre fit) — not just hook wiring
2092
+ - [ ] Camera tuned via `camera` in `defineGame({...})` — defaults untouched means the feel was never checked
2093
+ - [ ] 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
2094
+ - [ ] HUD screenshotted over a staged `GameUiPreview` scenario and **judged by looking at the image** against the UI quality bar in [`reference/ui-react.md`](reference/ui-react.md) — the final human glance, not the verification loop
2095
+ - [ ] Co-located bun tests for pure game math (curves, cooldowns, spawn logic)
2096
+ - [ ] Multiplayer via adapter config only; no direct backend calls
2097
+
2098
+ ## Quick reference
2099
+
2100
+ ```
2101
+ defineGame (shell) engine fields (assets, world, physics, inventories, input, server, save, time, feed, multiplayer)
2102
+ + presentation fields (content, loop, GameUI, camera, environment, shadows, movement, devtools, …) in one call — smart defaults fill the rest
2103
+ defineGame (core) the underlying engine-only primitive: assets, world, physics, inventories, input, server, save, time, feed, multiplayer, loop
2104
+ PlayableGame { game, content, loop, GameUI, camera, … } — the runner contract `defineGame` (shell) returns
2105
+ GameContext ctx.scene / ctx.game / ctx.player / ctx.item / ctx.camera / ctx.input + subscribe/version
2106
+ scene.object place, remove, move, rotate, at, setVisual (per-instance ObjectVisual: scale/color/opacity override)
2107
+ scene.entity spawn (anchor/offset), despawn, setPose, update; stats; targeting; effects;
2108
+ projectiles (object-aware raycasts); spatial queries (opt-in grid broadphase)
2109
+ entity.stats get / set / delta — bounded stats (health, mana, xp, level) on instances
2110
+ progression game/progression — curve() / leveling() over bounded xp/level stats
2111
+ item.use catalog `use` → GameContext handler; no input.to
2112
+ effects drain-signed magnitudes; receive.<effect>.order; AoE = effect + at/radius/los
2113
+ projectiles willHit → fire → settle; ballistic via weapon.projectile
2114
+ death onDeath (reason-aware drops/command), entity.died, auto kill attribution + drop grant
2115
+ game.loot register / has / roll / grantToPlayer (lootTable() = pure factory)
2116
+ game.trade canBuy / canSell / buy / sell / tradableAt
2117
+ game.quest register, accept…turnIn, bind(entity.died | inventory.added), declarative rewards
2118
+ game.social friends (persisted, requests listable), party (ephemeral, invites listable), presence, emotes (nearby broadcast), worldInvites (accept → join target)
2119
+ game.chat send / whisper / history / register — global/party/proximity channels, rate-limited, mute via blocked set
2120
+ game.roster capture / release / list / setEquipped — persisted owned-creature roster
2121
+ game.store/cards/turn store: keyed reactive slot; cards.pile(id): lazy CardPile; turn.loop(id): lazy TurnLoop
2122
+ game.events/feed/leaderboard on / bind+push+recent / track+increment+getTop
2123
+ devtools F2 overlay (Perf/Tune/Logs/Net/Keys); zero-annotation — top-level export const number/boolean/color + exported flat tables auto-discover into Tune; tunable() is the optional low-level escape hatch; snapshotDevtools()/window.__JG_DEVTOOLS.snapshot() → DevtoolsSnapshot (+ discovered[]); probes.register(name, read) adds a Perf gauge
2124
+ applyLoadout all-or-nothing kit seeding per userId
2125
+ player.movement pose (hitboxes) + aim (zoom modifier)
2126
+ player.motion impulse / setVerticalVelocity / setY — vertical-motion seam into the shell's frame driver
2127
+ player.possession own/disown/owns/listOwned + active + possess — control-swap, rebinds shell camera
2128
+ player.cosmetics register + apply/equip + get — per-player appearance slots, no gameplay effect
2129
+ scene.entity.form register + shapeshift/revert + active/abilities — movement+ability+mesh bundle, game-time duration
2130
+ proximityPrompt { radius, display: {kind}, invoke } — one float-UI primitive
2131
+ skillCheck/qte evaluateSkillCheck (moving zone + window) / evaluateQteSequence (timed steps)
2132
+ captureCheck captureChance / rollCapture — hp% + catchPower → probability
2133
+ dialogue check DialogueChoice.check (roll vs DC + advantage/disadvantage) → onSuccess/onFailure
2134
+ world features biomes / voxel / plots / tilemap / flat descriptors
2135
+ physics/physicsWorld optional headless rigid-body sim (PhysicsWorld) — not the defineGame physics field (gravity/jumpVelocity, honored by the kinematics controller); bodies are box (halfExtents) or sphere (radius)
2136
+ physics/ballisticSweep createBallisticSweep(world) → arc-vs-body hit test; wire into ProjectileSystemDeps.sweepBallistic
2137
+ anim/easing lerp/clamp01/smoothstep/tween/timedProgress + easeIn/Out/InOut Quad/Cubic + easeOutBack/Elastic — pure 0..1 tweening math
2138
+ data/dataSource createDataSource/createJsonDataSource — idle/loading/ready/error async state + polling; data/fetchJson + data/devProxy for CORS-safe dev fetches
2139
+ audio/audioFalloff computeFalloffGain / resolveEmitterGain — pure distance→gain curve; shell plays it
2140
+ time/beatClock createBeatClock (BPM ticks) + createBeatInputBuffer (buffered action → next beat)
2141
+ ws/voiceChannel createVoiceChannelRouter — positional falloff + simultaneous non-positional channels
2142
+ multiplayer/identity AuthSession + sessionPlayer + resolveGuestSession — Clerk/better-auth via react structural adapters
2143
+ multiplayer/chatContract ChatTransport (hooks) / ChatSync (callbacks) — ws + convex bindings, local for dev
2144
+ multiplayer/voiceContract VoiceTransport (join/leave/publish/subscribers) + createPushToTalk — media plane host-supplied
2145
+ GameBackend { transport, feeds?, presence? } — Convex is one adapter (createConvexBackend)
2146
+ adapter kinds offline / ws / convex / socketIo / p2p / lan (+ fly({app}) ws sugar) — runtime/adapter
2147
+ ws/pipe TransportPipe/TransportPipeFactory — any bidirectional string channel (webSocketPipe default)
2148
+ ws/host, ws/hostRouter browser-safe createGameHost + createHostRouter/loopbackPipe (node re-exports both)
2149
+ ws/peer createPeerHost/createPeerGuest — WebRTC P2P, host tab authoritative, copy-paste signal codes
2150
+ ws/socketIoPipe, node/socketIoServer socketIoPipe/createSocketIoBackend + attachGameSocketIoServer
2151
+ @jgengine/react GameProvider + hooks + headless primitives (incl. identity/chat/voice/social kits); layout only in GameUI.tsx
2152
+ ```
2153
+
2154
+ Engine ships verbs and primitives. Your game ships nouns.
2155
+
2156
+ # jgengine-api — UI — @jgengine/react
2157
+
2158
+ Reference module for the [`jgengine-api`](../SKILL.md) skill. Load this when you need the React UI layer. The **UI quality bar** section at the bottom is how the HUD must look and behave — required, not optional polish.
2159
+
2160
+ ```tsx
2161
+ import { GameProvider, useSceneEntities, HealthBar } from "@jgengine/react";
2162
+
2163
+ <GameProvider context={ctx}>…</GameProvider>
2164
+ ```
2165
+
2166
+ Import provider, hooks, and headless components from the package root `@jgengine/react` (a barrel re-export). The per-file subpaths (`@jgengine/react/provider`, `/hooks`, `/components`) resolve the same symbols if you prefer them.
2167
+
2168
+ All hooks bind through the ctx change signal (`ctx.subscribe`/`ctx.version`):
2169
+
2170
+ | Hook | Returns |
2171
+ |------|---------|
2172
+ | `useGame()` / `usePlayer()` | `{ commands, events }` / `{ userId, isNew }` |
2173
+ | `useSceneEntities()` / `useSceneObjects()` | live snapshots for rendering |
2174
+ | `useWorldItems()` / `useNearestWorldItem(radius)` | ground-loot snapshots / nearest pickup for a HUD prompt |
2175
+ | `useEntityStat(instanceId, statId)` | `StatValue \| null` |
2176
+ | `useTarget(fromId)` | locked instanceId \| null |
2177
+ | `useInventory(id)` / `useCurrency(id)` | slots / balance |
2178
+ | `useFeed({ action, limit? })` | recent entries — kills, loot, any action |
2179
+ | `useQuestJournal()` | active quests + objective progress |
2180
+ | `useFriends()` / `useParty()` / `usePresence(userId)` / `useWorldInvites()` | social panels |
2181
+ | `useFriendRequests()` / `usePartyInvites()` | pending inbound requests/invites for the local player |
2182
+ | `useWorldBrowser({ fetchSessions, filter?, limit?, refreshMs? })` | polls a host-supplied fetcher (e.g. `createWsBackend().browse`) through matchmaking's `browseSessions` |
2183
+ | `useSession()` / `useAuthedPlayer({ guestSeed? })` | auth session from `<GameIdentityProvider>` / the `{ userId, isNew }` player seam for `createGameContext` |
2184
+ | `useChat(channelId, { limit? })` | local-player-filtered recent messages from `ctx.game.chat` |
2185
+ | `useVoice({ transport?, channelId?, mode?, resolveRoutes? })` | mic capture + PTT + voice-channel roster over the `VoiceTransport` seam |
2186
+ | `useRoster(userId?)` | owned/captured roster entries for a user (defaults to the local player) |
2187
+ | `useLeaderboard(stat, { scope, limit? })` | `{ userId, value }[]` |
2188
+ | `useActivePrompt(prompts)` | nearest proximity prompt |
2189
+ | `useGameClock()` | clock snapshot (`now`, `paused`, `speed`, `calendar`) + `controls` (pause/play/setSpeed) |
2190
+ | `useLocalPlayerDead()` / `localPlayerEntity(entities, userId)` | death-screen gating; local player from a snapshot |
2191
+ | `useMarkers(markerSet)` / `useFog(fogField)` | live map-marker list / fog-cell snapshot (bind a core `MarkerSet`/`FogField`) |
2192
+ | `useGameStore()` | raw store handle — escape hatch under the typed hooks |
2193
+ | `useEngineState(store)` / `useEngineStore(store, selector)` / `useEngineEvent(store, event, handler)` | bind/select/subscribe against any `ReadableEngineStore<TState>` / `EventfulEngineStore<TEventMap>` — the escape hatch below `useGameStore()` for state that isn't wired into a typed hook yet |
2194
+ | `useHeldKeys()` | `(code: string) => boolean` — raw window keydown/keyup/blur-backed held-key predicate, no `ctx` needed; the primitive `useAxisChannel` and the shell's own movement sampling are built on |
2195
+ | `useAxisChannel(config: AxisChannelConfig)` | `{ channel: AxisChannel, isDown }` — wires `useHeldKeys` into a fresh `AxisChannel` (`@jgengine/core/input/axisInput`) for a per-frame `channel.sample(dt, isDown)`; a driving/twin-stick HUD or custom control scheme reads analog throttle/steer without touching `window` directly |
2196
+
2197
+ Import hooks from `@jgengine/react/hooks`, components from `@jgengine/react/components`, `GameProvider` from `@jgengine/react/provider` (the package uses deep paths like core). `useEngineState`, `ReadableEngineStore`, `useEngineStore`, `useEngineEvent` also ship from the main `@jgengine/react` entry point (defined in `@jgengine/react/engineStore`, re-exported at the package root) — no deep import required.
2198
+
2199
+ Headless components (className passthrough, no baked-in styling): `SlotGrid`, `HealthBar` (+ `fillClassName`), `CurrencyPill`, `ProximityPrompt`, `Screen`, `KeybindRow`, `DialogueBox` (+ `lineClassName`/`speakerClassName`/`choicesClassName`/`choiceClassName`/`checkClassName`, `rollCheck`-gated choices), `SkillCheckBar` (+ `trackClassName`/`zoneClassName`/`markerClassName`), `QteTrack` (+ `stepClassName`/`activeClassName`/`doneClassName`), `CaptureOdds` (+ `fillClassName`), `ToastStack`, `DeathScreen`, `LevelUpFlash`. Map components (bind a core `MarkerSet`/`FogField`, `kindStyles` palette overridable): `Minimap` (framed circular player-centered map — fog + markers + facing arrow, optional baked terrain `background`+`mapBounds`), `Compass` (facing strip with cardinals + marker pips), `WorldMap` (full-bounds top-down overlay). Not yet implemented: `useServer`, `useDialogue`.
2200
+ **Identity (`@jgengine/react/identity`)** — `<GameIdentityProvider source={…}>` + `useSession()`. Sources: `clerkIdentity({ isLoaded, isSignedIn, user })` maps Clerk's `useUser()` shape, `betterAuthIdentity({ data, isPending })` maps better-auth's `useSession()` shape (both pure structural mappers — no SDK imports, one line at the call site), `guestIdentity(seed?)` for local/dev. Gate UI with `<RequireSession fallback loading>`; `<UserBadge>` / `<SignOutButton>` are headless like everything else. `useAuthedPlayer({ guestSeed? })` returns the `{ userId, isNew }` to hand `createGameContext` — feed the player seam from the session instead of hand-picking a userId.
2201
+ **Chat (`@jgengine/react/chat`)** — headless `<ChatPanel>` (tabs + log + input composition with internal active-channel state), or compose `<ChannelTabs active onSelect>`, `<ChatLog channelId>` (auto-scrolls, `renderMessage` override), `<ChatInput channelId onSent onRejected>` yourself. All drive `ctx.game.chat` through `useChat`. `chatTransportFromSync(sync)` lifts a callback-style `ChatSync` (e.g. `createWsBackend(...).chatSyncFor(serverId)`) into the hook-shaped `ChatTransport` for remote chat.
2202
+ **Voice (`@jgengine/react/voice`)** — `useVoice()` once per channel: `getUserMedia` mic capture (`requestMic()`, tracks gated by transmission), push-to-talk via `createPushToTalk` (hold/toggle/openMic + mute), roster from `VoiceTransport.subscribers`, and per-speaker `gainFor(userId)` when you pass `resolveRoutes: () => router.resolveRoutes(myUserId)` from `@jgengine/ws/voiceChannel`. Hand the returned state to the headless `<PushToTalkButton voice>`, `<MicToggle voice>`, `<SpeakingIndicator voice userId>`, `<VoiceRoster voice>`.
2203
+ **Social (`@jgengine/react/social`)** — the headless social kit over `ctx.game.social`: friends (`<FriendsList>`, `<FriendRow>`, `<PresenceDot>`, `<AddFriendButton toUserId>`, `<FriendRequestsList>` with accept/decline), party (`<PartyFrame>`, `<PartyMemberRow>`, `<PartyInviteToast>`, `<LeavePartyButton>`), worlds (`<WorldBrowser listings onJoin>`, `<JoinByCode onJoin>` — normalizes codes, `<QuickMatchButton listings filter?>`, `<InviteToWorldButton toUserId target>`, `<WorldInviteToast onAccepted>` — hands you the `{ serverId, joinCode? }` join target), and `<EmoteWheel emotes>` over `emotes.play`. All className-passthrough with `data-*` hooks and `renderX` overrides; the `social-hub` demo in `apps/dev` (`?game=social-hub`) composes the whole kit.
2204
+ **Drag/rotate/drop/snap gesture layer** (`@jgengine/react/dragLayer`) — a 2-D UI-space gesture layer over the card/shaped-grid primitives, distinct from 3-D world drag. `useDragLayer<T>({ onDrop })` owns pointer-follow drag state (begin/rotate/setTarget/end); pair it with the headless, className-passthrough `DraggableCard` (right-click rotates), `DropZone` (reports the snapped `cellFromPoint` cell + active state), and `DragGhost` (a pointer-anchored preview). Drop resolution and overlap validation stay the game's job via `canPlace`/`placeShaped` from `inventory/shapedGrid` — Balatro hand→play drags, Backpack Hero grid placement, Slay-the-Spire card-onto-enemy targeting.
2205
+
2206
+ **Layout rule:** all **screen** positioning (`absolute`, `inset-*`, grid zones, flex regions) lives on wrappers inside `ui/GameUI.tsx` only. `ui/components/` files are content + hooks only — internal `relative`/`absolute` for bar overlays or slot badges inside a component is fine; never anchor a component to the viewport from a child file. Pass `className` to primitives for **visual** styling (colors, borders, size), not screen placement.
2207
+
2208
+ **Tailwind sources:** add `@source` entries in your CSS for your game source dirs plus `node_modules/@jgengine/shell` and `node_modules/@jgengine/react`. Without them, classes used in dynamically imported game code are **not generated** — layout wrappers in `GameUI.tsx` silently fail and every HUD cluster stacks in one corner.
2209
+
2210
+ ## Visual HUD via the shadcn registry
2211
+
2212
+ Styled HUD components are **copy-in code**, not npm exports — install them from the JGengine registry with the shadcn CLI:
2213
+
2214
+ ```sh
2215
+ npx shadcn@latest add https://jgengine.com/r/entity-vital-bar.json
2216
+ ```
2217
+
2218
+ The component lands in your game's `components/ui/`, themes through `--jg-*` CSS variables, and reads engine data through `@jgengine/react/hooks`. Health bar hookup, end to end:
2219
+
2220
+ ```tsx
2221
+ import { usePlayer } from "@jgengine/react/hooks";
2222
+ import { EntityVitalBar } from "@/components/ui/entity-vital-bar";
2223
+
2224
+ const { userId } = usePlayer();
2225
+ return <EntityVitalBar instanceId={userId} statId="health" />;
2226
+ ```
2227
+
2228
+ The full HUD catalog ships as registry items — vitals, slots, feedback, meters, panels, screens, reticles, plus the `game-icon` glyph catalog (`iconForItemId`/`iconForAction`), with engine-bound `Entity`/`Ability`/`Journal`/`Feed`/`Wallet` variants wired to the hooks. Where a registry item exists, prefer it — and never hand-roll a gray-box version of a component the registry already ships.
2229
+
2230
+ ### UI quality bar (required — not optional polish)
2231
+
2232
+ Headless primitives mean **you** ship the visual design. Functional wiring alone is not shippable UI. Judge against staged screenshots, never your mental model of the code.
2233
+
2234
+ **See what you ship.** `@jgengine/shell`'s `GameUiPreview` renders your `GameUI` over a staged `GameContext` (ticks run, hostile targeted, first ability fired) with no gameplay or backend — mount it on a dev route, screenshot it, and judge the image before calling any HUD work done; pass a custom `scenario` to stage richer states (open modals, low health, active quest). Type-green says nothing about whether the HUD renders.
2235
+
2236
+ | Requirement | Minimum |
2237
+ |-------------|---------|
2238
+ | **Contrast** | HUD text and borders readable on the game's scene background — never bare `text-stone-400` on near-black without a panel |
2239
+ | **Scale** | Primary HUD (unit frames, hotbar slots, menu buttons) ≥ 48px touch targets; body text ≥ `text-sm` (12px); key labels never below 11px |
2240
+ | **Distinct construction** | Unit frame, hotbar, currency, quests, and toasts must not share one card style — same `rounded border bg-stone-900/80 p-3` on everything reads identical and cheap |
2241
+ | **Real icons** | Every item/ability/hotbar slot shows a real, distinct silhouette or sprite (registry `game-icon`, asset-pack sprite) — never a gray box, first letter, emoji, or one generic shape reused everywhere |
2242
+ | **Hotbar / slots** | Icon per ability; keybind badge on the slot corner; hover/active state; empty slots visually distinct |
2243
+ | **Unit frames** | Name + level + labeled bars with numeric values; health/mana/resource colors genre-appropriate |
2244
+ | **Layout** | No overlapping anchors; reserve space for frames that appear conditionally (target, quest log) |
2245
+ | **Panels** | Modal/slide panels: title, close control, section headers, consistent chrome with the HUD |
2246
+ | **Feedback** | Errors, cooldowns, and empty actions surface to the player (toast, dim, shake) — not `console.warn` only. Error text is ephemeral floating combat text, never a bordered toast card |
2247
+
2248
+ **Genre fit:** MMO/RPG → ornate dark panels, gold accents, portrait + bars, action bar with icons. Shooter → crosshair + ammo + ability cooldowns. Tycoon → resource pills + build menus. Match the game's fantasy; do not ship debug-gray placeholders.
2249
+
2250
+ **Panel vs frameless.** Modal/panel chrome (backdrop + bordered window) is for on-demand windows only: backpack/bags, combat log/chat feed, social window. Everything persistent stays frameless — typography, bars, icons, shadows, no enclosing card: player/target/party frames, action bar, quest tracker, currency, voice cluster, floating combat text, world VFX. Backpack is the only bag UI (no hotbar inside it); character sheet holds equipment + stats; the abilities page lists catalog abilities with costs/cooldowns/keybinds, not a hotbar duplicate.
2251
+
2252
+ **Keybinds are badges on their control.** Every toggle and hotbar slot shows its binding on itself (slot corner, toggle button) — never a persistent "WASD to move / E to interact" legend pinned to the screen; if a control needs explaining, badge it or use a proximity prompt that fades. Labels derive from the game's `keybinds.ts` via `actionLabel(keybinds, action)` — hardcoded key strings drift. Register actions in `defineGame.input`, wire through `game.commands` (never UI click handlers only), and read the binding table once before shipping: **one key, one action**. Panel toggles follow the `ui.openBackpack`-style command pattern with state in `ctx.game.store` (`useGameStore`), not a hand-rolled module store.
2253
+
2254
+ **Action bar slot states — all four, visually:** ready (full color), cooldown (dim + sweep + numeric timer), no-resource (red tint/desaturate, cost checked before press), just-cast (brief bright ring flash ~200ms). Cooldown data lives in game code; UI reads it.
2255
+
2256
+ **Combat feedback:** bolts/bullets are `fireProjectile` + delayed `settleProjectile` so they visibly travel — never `effect({ to })`; instant heals flash the unit-frame bar; out-of-range/oom is floating text that fades, no bordered toast.
2257
+
2258
+ **Mobile / touch:** HUD fits a 390px portrait viewport with no horizontal overflow (`min()`/viewport units once compact); `useDisplayProfile().compact` (`@jgengine/react/display`) collapses side panels into a slim top bar; keep the bottom ~180px clear for the engine's touch dock on `coarsePointer`; never render key badges/legends on touch; non-interactive wrappers stay `pointer-events-none` (only real controls opt in with `pointer-events-auto`); ≥48px touch targets always.
2259
+
2260
+ **Social HUD:** build from the headless kits (`@jgengine/react/social`, `/chat`, `/voice`, `/identity`), never hand-rolled lists. Chat is a corner-anchored panel (channel tabs, log, input; sender names tinted apart from bodies); invite toasts are ephemeral with accept/decline that expire with the invite; presence is a dot (`data-online`), not a word; push-to-talk badges its keybind and visibly transmits (`data-transmitting`), speaking players glow on their party row; emote wheel appears on hold-key and fades. Every social button drives an engine verb (`social.friends.request`, `party.accept`, `worldInvites.accept` → join) — one that only mutates local UI state is half a system.
2261
+
2262
+ **Camera feel is part of the HUD pass.** The shell's orbit camera (left-drag orbit, scroll zoom, tap = primary ability, camera-relative WASD) tunes per game via the `camera` field of `defineGame({...})` (`minDistance`, `maxDistance`, `targetHeight`, `rotateSpeed`, `zoomSpeed`, `dampingFactor`, smoothing) — defaults untouched means the feel was never checked; never hardcode camera position in `onTick` while a rig is active.
2263
+
2264
+ **Shared chrome:** extract repeated panel/slot styles into `ui/<theme>.ts` or `ui/components/<Frame>.tsx` — do not copy-paste three classes per file.
2265
+
2266
+ **Self-check before calling UI done** (against actual staged screenshots):
2267
+
2268
+ - [ ] Screenshot at 1080p: can you read every label without squinting?
2269
+ - [ ] Could you mistake the unit frame and hotbar for the same component? If yes, redo.
2270
+ - [ ] Every toggle shows its key on its own control — and no persistent controls legend anywhere?
2271
+ - [ ] Every item/ability slot shows a real, distinct icon — no gray boxes, letters, or emoji?
2272
+ - [ ] Do bolts visibly travel before damage lands?
2273
+ - [ ] Only on-demand windows (backpack, log, social) have panel borders?
2274
+ - [ ] Every declared system has its UI end state — quest → tracker, cooldown → sweep + timer, error → floating text? A wired hook with no visual is half a system: finish it or cut it (see `jgengine-newgame`).
2275
+ - [ ] Does the staged shot show the HUD *working* (target locked, cooldown mid-sweep, tracker populated), not resting-empty?
2276
+ - [ ] Would a player think this is intentional art direction? If it looks like a debug build, it ships nothing.
2277
+
2278
+ # jgengine-api — World features
2279
+
2280
+ Reference module for the [`jgengine-api`](../SKILL.md) skill. Load this when you need the renderer-free world surface.
2281
+
2282
+ ## World features
2283
+
2284
+ Descriptors from `@jgengine/core/world/features` — config data the runner/world layer interprets:
2285
+
2286
+ | Feature | Use |
2287
+ |---------|-----|
2288
+ | `biomes({ map, zones, bounds? })` | Region atmosphere/rules layering; zones reference biome ids |
2289
+ | `voxel({ seed, generate?, streaming? })` | Block worlds |
2290
+ | `plots(config)` | Shared city + instanced interiors |
2291
+ | `tilemap({ map })` | 2D/2.5D levels |
2292
+ | `flat()` | Plain arena |
2293
+ | `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 |
2294
+
2295
+ `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.
2296
+
2297
+ `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-newgame`'s first-shot art recipe).
2298
+
2299
+ `parentSpace` positions are local to that space — convert at seams only.
2300
+
2301
+ ### Query primitives (renderer-free, for gameplay)
2302
+
2303
+ Pure `@jgengine/core` functions so gameplay reads the same world the shell renders — no three.js needed:
2304
+
2305
+ | Primitive | Answers |
2306
+ |-----------|---------|
2307
+ | `resolveTerrainField(terrain(...))` / `noiseField(cfg)` → `TerrainField` | `sampleHeight(x,z)`, `sampleNormal(x,z)`, `waterLevel` — ground-snap, collision, camera. `resolveGroundStep` slope-limits movement |
2308
+ | `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 |
2309
+ | `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) |
2310
+ | `windField(cfg)` → `WindField` | `at(t)`, `atPoint(x,z,t)`, `strengthAt` — one wind source for weather sway, grass, sailing, fire spread |
2311
+ | `waterSurface(cfg)` / `waterSurfaceFromDescriptor(ocean(...))` → `WaterSurface` | `height(x,z,t)`, `normal`, `displace` — buoyancy, floating, shoreline (CPU Gerstner matching the ocean shader) |
2312
+ | `scatter(cfg)` → `ScatterPoint[]` | Seeded, overlap-aware point distribution — vegetation, props, lots, spawn points (`minDistance`, `avoid` rects) |
2313
+ | `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 |
2314
+ | `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) |
2315
+ | `buildingIndex(district)` → `BuildingIndex` | `at`/`within`/`nearest`/`isInside`/`blockers` over a generated district — placement avoidance, pathfinding |
2316
+
2317
+ **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`).
2318
+
2319
+ **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`.
2320
+
2321
+ Renderers for these descriptors live in `@jgengine/shell` (`shell/terrain`, `shell/water`, `shell/weather`, `shell/structures`).
2322
+
2323
+ ### Environment fields, weather hooks & realm composition
2324
+ 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`.
2325
+ - **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.
2326
+ - **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.
2327
+ - **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.
2328
+ - **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.
2329
+ ### Survival meters, moodles & multi-region health
2330
+ The `survival/` domain — decay-over-time meters and per-part health, both feeding one stacking **moodle** status display distinct from numeric bars.
2331
+ - **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.
2332
+ - **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)`.
2333
+ - **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).
2334
+ ### Interactive building & terraform (renderer-free tools)
2335
+ 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()`.
2336
+ | Primitive | Answers |
2337
+ |-----------|---------|
2338
+ | `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()`. |
2339
+ | `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. |
2340
+ | `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. |
2341
+ | `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. |
2342
+ | `createPlacedStructureStore()` | Save/load a built layout: `add`/`move`/`rotate`/`remove`/`select`, `snapshot()`↔`load()` round-trip (survives reload), `subscribe` for the renderer. |
2343
+ | `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. |
2344
+ | `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). |
2345
+
2346
+ ### Physics world (optional, headless)
2347
+
2348
+ `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`).
2349
+
2350
+ **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.
2351
+
2352
+ **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.
2353
+
2354
+ **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.
2355
+
2356
+ **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.
2357
+
2358
+ **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.
2359
+
2360
+ **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.
2361
+
2362
+ **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).
2363
+
2364
+ **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.
2365
+ **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).
2366
+ ### Vehicles, mounts, crash damage & racing
2367
+ 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)`.
2368
+ - **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.
2369
+ - **`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.
2370
+ - **`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.
2371
+ - **`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.
2372
+ - **`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.
2373
+ - **`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.
2374
+ - **`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.
2375
+
2376
+ ### Spawn placement
2377
+
2378
+ `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.
2379
+
2380
+ **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.
2381
+
2382
+ **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.