@jgengine/shell 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/CombatPresentation.d.ts +5 -0
  5. package/dist/CombatPresentation.js +8 -0
  6. package/dist/GameHost.d.ts +25 -1
  7. package/dist/GameHost.js +60 -4
  8. package/dist/GamePhaseStamp.d.ts +1 -0
  9. package/dist/GamePhaseStamp.js +9 -0
  10. package/dist/GamePlayer.d.ts +5 -0
  11. package/dist/GamePlayer.js +5 -0
  12. package/dist/GamePlayerShell.d.ts +5 -12
  13. package/dist/GamePlayerShell.js +67 -1332
  14. package/dist/GameUiPreview.js +3 -2
  15. package/dist/Shell3dPresentation.d.ts +63 -0
  16. package/dist/Shell3dPresentation.js +302 -0
  17. package/dist/ShellChrome.d.ts +51 -0
  18. package/dist/ShellChrome.js +51 -0
  19. package/dist/ShellHudPresentation.d.ts +41 -0
  20. package/dist/ShellHudPresentation.js +22 -0
  21. package/dist/audio/audioEngine.d.ts +4 -0
  22. package/dist/audio/audioEngine.js +128 -21
  23. package/dist/audio/audioWire.d.ts +40 -0
  24. package/dist/audio/audioWire.js +62 -0
  25. package/dist/audio/loopParams.d.ts +20 -0
  26. package/dist/audio/loopParams.js +28 -0
  27. package/dist/behaviour.d.ts +2 -1
  28. package/dist/behaviour.js +2 -1
  29. package/dist/behaviourAttach.d.ts +1 -0
  30. package/dist/behaviourAttach.js +1 -0
  31. package/dist/behaviourDriver.d.ts +1 -0
  32. package/dist/behaviourDriver.js +1 -0
  33. package/dist/boundActionDispatch.d.ts +13 -0
  34. package/dist/boundActionDispatch.js +37 -0
  35. package/dist/camera/GameCameraRig.d.ts +5 -2
  36. package/dist/camera/GameCameraRig.js +8 -3
  37. package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
  38. package/dist/camera/GameFirstPersonCamera.js +8 -3
  39. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  40. package/dist/camera/GameInspectionCamera.js +2 -2
  41. package/dist/camera/GameOrbitCamera.js +24 -9
  42. package/dist/camera/cameraBlendMath.d.ts +3 -0
  43. package/dist/camera/cameraBlendMath.js +3 -0
  44. package/dist/camera/cameraRigs.d.ts +3 -1
  45. package/dist/camera/cameraRigs.js +21 -3
  46. package/dist/camera/fovPreference.d.ts +6 -1
  47. package/dist/camera/fovPreference.js +6 -1
  48. package/dist/camera/index.d.ts +4 -7
  49. package/dist/camera/index.js +4 -7
  50. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  51. package/dist/camera/inspectionCameraMath.js +7 -2
  52. package/dist/camera/orbitCameraMath.d.ts +36 -6
  53. package/dist/camera/orbitCameraMath.js +56 -6
  54. package/dist/camera/rigMath.d.ts +114 -24
  55. package/dist/camera/rigMath.js +129 -24
  56. package/dist/camera/rigResolve.d.ts +4 -2
  57. package/dist/camera/rigResolve.js +4 -2
  58. package/dist/camera/shakeChannel.d.ts +6 -2
  59. package/dist/camera/shakeChannel.js +6 -2
  60. package/dist/camera/shakeChannelMath.d.ts +1 -0
  61. package/dist/camera/shakeChannelMath.js +1 -0
  62. package/dist/commandSink.d.ts +8 -3
  63. package/dist/commandSink.js +8 -3
  64. package/dist/defineGame.d.ts +15 -1
  65. package/dist/defineGame.js +50 -9
  66. package/dist/devtools/ColPanel.d.ts +1 -0
  67. package/dist/devtools/ColPanel.js +14 -0
  68. package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
  69. package/dist/devtools/DevtoolsOverlay.js +47 -462
  70. package/dist/devtools/KeysPanel.d.ts +4 -0
  71. package/dist/devtools/KeysPanel.js +31 -0
  72. package/dist/devtools/LogsPanel.d.ts +1 -0
  73. package/dist/devtools/LogsPanel.js +12 -0
  74. package/dist/devtools/NetPanel.d.ts +4 -0
  75. package/dist/devtools/NetPanel.js +10 -0
  76. package/dist/devtools/PerfPanel.d.ts +4 -0
  77. package/dist/devtools/PerfPanel.js +53 -0
  78. package/dist/devtools/TunePanel.d.ts +3 -0
  79. package/dist/devtools/TunePanel.js +309 -0
  80. package/dist/devtools/agentBridge.d.ts +41 -0
  81. package/dist/devtools/agentBridge.js +266 -0
  82. package/dist/devtools/collisionDebug.d.ts +6 -0
  83. package/dist/devtools/collisionDebug.js +6 -0
  84. package/dist/devtools/collisionDebugMath.d.ts +8 -2
  85. package/dist/devtools/collisionDebugMath.js +24 -5
  86. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  87. package/dist/devtools/devtoolsOverrides.js +48 -0
  88. package/dist/devtools/panelAtoms.d.ts +9 -0
  89. package/dist/devtools/panelAtoms.js +10 -0
  90. package/dist/devtools/perfDiagnose.d.ts +2 -0
  91. package/dist/devtools/perfDiagnose.js +43 -0
  92. package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
  93. package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
  94. package/dist/drivers/FrameDriver.d.ts +37 -0
  95. package/dist/drivers/FrameDriver.js +232 -0
  96. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  97. package/dist/drivers/HudOnlyDriver.js +60 -0
  98. package/dist/environment/Daylight.d.ts +23 -7
  99. package/dist/environment/Daylight.js +106 -13
  100. package/dist/environment/EnvironmentScene.js +44 -9
  101. package/dist/environment/VolumetricClouds.d.ts +17 -0
  102. package/dist/environment/VolumetricClouds.js +202 -0
  103. package/dist/environment/daylightCycle.d.ts +3 -1
  104. package/dist/environment/daylightCycle.js +3 -1
  105. package/dist/environment/groundPadMath.d.ts +3 -0
  106. package/dist/environment/groundPadMath.js +3 -0
  107. package/dist/environment/index.d.ts +1 -0
  108. package/dist/environment/index.js +1 -0
  109. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  110. package/dist/environment/skyLightingPolicy.js +2 -0
  111. package/dist/environment/terrainGroundColor.d.ts +9 -0
  112. package/dist/environment/terrainGroundColor.js +22 -0
  113. package/dist/gameKit.d.ts +27 -0
  114. package/dist/gameKit.js +29 -0
  115. package/dist/hotbarActions.d.ts +26 -0
  116. package/dist/hotbarActions.js +27 -0
  117. package/dist/inputSink.d.ts +16 -4
  118. package/dist/inputSink.js +57 -7
  119. package/dist/map/MapMarkerBeacons.d.ts +3 -2
  120. package/dist/materialOverride.d.ts +10 -0
  121. package/dist/materialOverride.js +15 -3
  122. package/dist/multiplayer.js +8 -2
  123. package/dist/postfx/PostProcessing.d.ts +9 -1
  124. package/dist/postfx/PostProcessing.js +56 -11
  125. package/dist/postfx/gradeShader.js +9 -1
  126. package/dist/presentationResolve.d.ts +35 -0
  127. package/dist/presentationResolve.js +51 -0
  128. package/dist/registry.d.ts +4 -2
  129. package/dist/render/EnvironmentLighting.d.ts +10 -0
  130. package/dist/render/EnvironmentLighting.js +34 -0
  131. package/dist/render/SceneLighting.d.ts +7 -0
  132. package/dist/render/SceneLighting.js +18 -0
  133. package/dist/render/SceneModels.d.ts +14 -0
  134. package/dist/render/SceneModels.js +337 -0
  135. package/dist/render/assetBase.d.ts +17 -0
  136. package/dist/render/assetBase.js +35 -0
  137. package/dist/render/defaultSurface.d.ts +29 -0
  138. package/dist/render/defaultSurface.js +101 -0
  139. package/dist/render/modelLoad.d.ts +24 -0
  140. package/dist/render/modelLoad.js +76 -0
  141. package/dist/render/modelRender.d.ts +11 -0
  142. package/dist/render/modelRender.js +15 -0
  143. package/dist/render/resolveModel.d.ts +54 -3
  144. package/dist/render/resolveModel.js +116 -4
  145. package/dist/render/useEntityRenderCues.d.ts +13 -0
  146. package/dist/render/useEntityRenderCues.js +62 -0
  147. package/dist/scatter/InstancedScatter.d.ts +24 -0
  148. package/dist/scatter/InstancedScatter.js +135 -0
  149. package/dist/scatter/scatterModels.d.ts +25 -0
  150. package/dist/scatter/scatterModels.js +61 -0
  151. package/dist/scatter/scatterProxies.d.ts +6 -0
  152. package/dist/scatter/scatterProxies.js +118 -0
  153. package/dist/scatter.d.ts +1 -0
  154. package/dist/scatter.js +1 -0
  155. package/dist/scene/AuthoredScene.d.ts +84 -0
  156. package/dist/scene/AuthoredScene.js +178 -0
  157. package/dist/scene/CityRenderer.d.ts +2 -0
  158. package/dist/scene/CityRenderer.js +1031 -0
  159. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  160. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  161. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  162. package/dist/scene/GrassFieldRenderer.js +73 -0
  163. package/dist/scene/PoleLineRenderer.d.ts +2 -0
  164. package/dist/scene/PoleLineRenderer.js +287 -0
  165. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  166. package/dist/scene/SoilPatchRenderer.js +84 -0
  167. package/dist/scene/StudioStage.d.ts +37 -0
  168. package/dist/scene/StudioStage.js +42 -0
  169. package/dist/scene/WaterRenderer.d.ts +2 -0
  170. package/dist/scene/WaterRenderer.js +61 -0
  171. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  172. package/dist/scene/builtinSceneKindRenderers.js +26 -0
  173. package/dist/scene/cityWindowMaterial.d.ts +16 -0
  174. package/dist/scene/cityWindowMaterial.js +81 -0
  175. package/dist/scene/sceneKindRenderers.d.ts +30 -0
  176. package/dist/scene/sceneKindRenderers.js +17 -0
  177. package/dist/scene.d.ts +1 -0
  178. package/dist/scene.js +1 -0
  179. package/dist/settings/appliedSettings.d.ts +7 -1
  180. package/dist/settings/appliedSettings.js +13 -2
  181. package/dist/settings/settingsController.d.ts +2 -0
  182. package/dist/settings/settingsController.js +22 -1
  183. package/dist/shellConstants.d.ts +4 -0
  184. package/dist/shellConstants.js +4 -0
  185. package/dist/shellMovement.d.ts +8 -0
  186. package/dist/shellMovement.js +11 -0
  187. package/dist/shellPointer.d.ts +20 -0
  188. package/dist/shellPointer.js +28 -0
  189. package/dist/structures/PlacementGhost.d.ts +4 -0
  190. package/dist/structures/PlacementGhost.js +4 -0
  191. package/dist/structures/TransformGizmo.d.ts +53 -0
  192. package/dist/structures/TransformGizmo.js +59 -0
  193. package/dist/structures/index.d.ts +1 -0
  194. package/dist/structures/index.js +1 -0
  195. package/dist/terrain/GrassField.d.ts +11 -3
  196. package/dist/terrain/GrassField.js +16 -6
  197. package/dist/terrain/TerraformBrushCursor.js +3 -0
  198. package/dist/terrain/grassBudget.d.ts +11 -2
  199. package/dist/terrain/grassBudget.js +20 -4
  200. package/dist/terrain/grassGeometry.d.ts +35 -0
  201. package/dist/terrain/grassGeometry.js +85 -20
  202. package/dist/terrain/grassMaterial.d.ts +20 -0
  203. package/dist/terrain/grassMaterial.js +68 -13
  204. package/dist/terrain/heightfieldGeometry.d.ts +34 -0
  205. package/dist/terrain/heightfieldGeometry.js +109 -0
  206. package/dist/terrain/index.d.ts +1 -0
  207. package/dist/terrain/index.js +1 -0
  208. package/dist/terrain/random.d.ts +3 -0
  209. package/dist/terrain/random.js +3 -0
  210. package/dist/terrain/soilPatchMaterial.d.ts +23 -0
  211. package/dist/terrain/soilPatchMaterial.js +147 -0
  212. package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
  213. package/dist/terrain/terrainDetailMaterial.js +87 -7
  214. package/dist/terrain/terrainMath.d.ts +8 -1
  215. package/dist/terrain/terrainMath.js +8 -1
  216. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  217. package/dist/touch/TouchControlsOverlay.js +201 -22
  218. package/dist/useShellMultiplayerSync.d.ts +9 -0
  219. package/dist/useShellMultiplayerSync.js +98 -0
  220. package/dist/visibility/CullingProvider.js +1 -1
  221. package/dist/water/Ocean.d.ts +7 -1
  222. package/dist/water/Ocean.js +16 -3
  223. package/dist/water/OceanConfig.d.ts +11 -0
  224. package/dist/water/OceanConfig.js +8 -0
  225. package/dist/water/OceanMaterial.d.ts +12 -0
  226. package/dist/water/OceanMaterial.js +9 -0
  227. package/dist/water/OceanShader.d.ts +2 -2
  228. package/dist/water/OceanShader.js +78 -10
  229. package/dist/weather/fireSpreadPose.d.ts +1 -0
  230. package/dist/weather/fireSpreadPose.js +1 -0
  231. package/dist/weather/weatherGeometry.d.ts +1 -0
  232. package/dist/weather/weatherGeometry.js +1 -0
  233. package/dist/weather/weatherMath.d.ts +3 -0
  234. package/dist/weather/weatherMath.js +3 -0
  235. package/dist/weather/weatherUniforms.d.ts +3 -0
  236. package/dist/weather/weatherUniforms.js +3 -0
  237. package/dist/world/PlaceScene.d.ts +11 -0
  238. package/dist/world/PlaceScene.js +27 -0
  239. package/dist/world/RetainedVfx.d.ts +10 -0
  240. package/dist/world/RetainedVfx.js +136 -0
  241. package/dist/world/WorldHud.d.ts +49 -2
  242. package/dist/world/WorldHud.js +87 -2
  243. package/dist/world/WorldScene.d.ts +21 -0
  244. package/dist/world/WorldScene.js +186 -0
  245. package/dist/world/WorldVfx.d.ts +9 -0
  246. package/dist/world/WorldVfx.js +184 -0
  247. package/dist/world/entityPose.d.ts +2 -0
  248. package/dist/world/entityPose.js +2 -0
  249. package/dist/world/floatTextStyle.d.ts +1 -0
  250. package/dist/world/floatTextStyle.js +1 -0
  251. package/dist/world/telegraphPulse.d.ts +1 -0
  252. package/dist/world/telegraphPulse.js +1 -0
  253. package/dist/world/worldBarSamples.d.ts +25 -0
  254. package/dist/world/worldBarSamples.js +38 -0
  255. package/dist/worldSky.d.ts +11 -0
  256. package/dist/worldSky.js +21 -0
  257. package/dist/worldSync.d.ts +2 -1
  258. package/dist/worldSync.js +2 -1
  259. package/package.json +6 -6
  260. package/skills/game-design/SKILL.md +28 -0
  261. package/skills/game-design/agents/openai.yaml +4 -0
  262. package/skills/game-design/references/application-playbook.md +170 -0
  263. package/skills/game-design/references/field-guide.md +288 -0
  264. package/skills/jgengine/SKILL.md +54 -0
  265. package/skills/jgengine/api.md +527 -0
  266. package/skills/jgengine/capabilities.md +131 -0
  267. package/skills/jgengine/recipes/minimal-game.md +88 -0
  268. package/skills/jgengine-ui/SKILL.md +41 -0
  269. package/skills/jgengine-ui/api.md +2007 -0
  270. package/skills/jgengine-ui/capabilities.md +211 -0
  271. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  272. package/skills/jgengine-ui/reference.md +513 -0
  273. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  274. package/skills/jgengine-world/SKILL.md +51 -0
  275. package/skills/jgengine-world/api.md +3056 -0
  276. package/skills/jgengine-world/capabilities.md +476 -0
  277. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  278. package/skills/jgengine-world/reference.md +261 -0
  279. package/skills/level-design/SKILL.md +54 -0
  280. package/skills/level-design/agents/openai.yaml +4 -0
  281. package/skills/level-design/references/application-playbook.md +192 -0
  282. package/skills/level-design/references/field-guide.md +337 -0
  283. package/skills/level-design/references/live-editor-loop.md +179 -0
  284. package/dist/cartridge.d.ts +0 -145
  285. package/dist/cartridge.js +0 -246
  286. package/llms.txt +0 -2761
@@ -0,0 +1,261 @@
1
+ # jgengine domain API — World features
2
+
3
+ Reference module for the [`jgengine-world` API](SKILL.md) skill. Load this when you need the renderer-free world surface.
4
+
5
+ ## World = place (the definition of a world)
6
+
7
+ A world is the place a game happens in — substrate + laws — declared with `world()` from
8
+ `@jgengine/core/world/place`. It is never a coded diorama: sky look, foliage scatter, props, and
9
+ sculpt are scene content authored in the editor (`jgengine-editor`), and the engine renders its
10
+ default sky when the document has none.
11
+
12
+ ```ts
13
+ import { world } from "@jgengine/core/world/place";
14
+
15
+ export const overworld = world({
16
+ id: "overworld", // place identity: saves, seeds, switching
17
+ ground: { mode: "flat", size: { x: Infinity, z: Infinity } },
18
+ physics: { gravity: -24 }, // laws of THIS place
19
+ });
20
+ ```
21
+
22
+ - `ground.mode` discriminates `size`: `flat` takes `{ x, z }` (or `{ x, y, z }`; `Infinity` = unbounded axis), `round` takes `{ radius }` only, `voxel` takes the generator domain `{ x, y, z }` (axes may be `Infinity`), `board` takes `{ x, y }` in cells/layout units (2D surface you look at — grid games, solitaire, tabletop; `stage` is a `board` alias). TS rejects a `radius` on `flat` and `x`/`z` on `round`.
23
+ - `ground.surface` is optional matter/feel law (`{ matter?, friction?, restitution?, traits? }`) systems and audio read — metal vs slime vs felt changes the feel of the *same* rule systems, never forks them. Not a theme kit.
24
+ - `ground.generator` (procedural modes) is algorithm params only — never a seed, never a genre/franchise preset.
25
+ - **No `seed` in a world definition, ever.** The engine derives seeds from world `id` + save/run via `seedForPlace(id, runSeed)` and injects them into generators and scatter.
26
+ - Multiple worlds per game are first-class: one `world()` per place, each with its own ground + physics (`defineGameDefinition` resolves the active place's physics over the game default). Pure UI/rules games omit `world` entirely.
27
+ - Thin defaults: `flat` + infinite axes + default physics for 3D, `board` for 2D, or no world at all. Never scaffold a "meadow with sky preset and seeded grass" — that is editor content.
28
+
29
+ ## Legacy world features
30
+
31
+ Descriptors from `@jgengine/core/world/features` — legacy code-declared world shapes kept for
32
+ existing games. Do not start new worlds here; `environment()` is now the consumption target for
33
+ editor/preset-written scene data (`environmentContentFromDocument`), not a world definition:
34
+
35
+ | Feature | Use |
36
+ |---------|-----|
37
+ | `biomes({ map, zones, bounds? })` | Region atmosphere/rules layering; zones reference biome ids |
38
+ | `voxel({ seed, generate?, streaming? })` | Block worlds |
39
+ | `plots(config)` | Shared city + instanced interiors |
40
+ | `tilemap({ map })` | 2D/2.5D levels |
41
+ | `flat()` | Plain arena |
42
+ | `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()`, `ocean()`, `rain()`/`snow()`, and `grass()` take `position: [x, z]` — sugar for `area.position` — to site a cluster/water body/vegetation patch away from the origin (several settlements, an offset lake, a distant meadow), and a biome-banded world places grass by zone this way; building clusters ground-snap to the terrain field per building. **`rain()`/`snow()` are the exception:** their `area.position`/`position` are accepted for API symmetry but have **no visual effect** — `RainField`/`SnowField` always follow the camera and never apply the volume's transform, so `area.w/d/h` instead sizes a camera-anchored wrap-volume that must stay near camera scale (roughly 50-120 per axis) or the storm goes sub-pixel-invisible at district scale; dial `density`/`width` (rain)/`flakeSize` (snow)/`opacity` for a heavier storm instead of a bigger area; 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. `grass({ density })` is **blades/m²** (default 4, matching `vegetation`'s density): blade count scales with patch **area**, capped at a per-field perf budget (`budget`/buffer `count`) — a devtools warning fires when a large patch is clamped, so raising density on a big field adds blades instead of silently no-opping against a fixed budget |
43
+
44
+ `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.
45
+
46
+ `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. `biomeBands: BiomeBand[]` (`{ z, fade?, material?, colors? }`) cross-fades the **ground palette along the world's z axis** — ordered zones (vale → marsh → peaks) that blend across a `fade`-wide window (default 64) centered on each boundary; it's the linear counterpart to the radial `materialRegions` (which paint on top; each region is a `TerrainCircleRegion` (default), `TerrainPolylineRegion` (roads/rivers), or `TerrainRectRegion` (rotatable districts), sharing a `TerrainRegionStyle`). Pure/testable via `createTerrainPaletteSampler`/`createBiomeBandSampler` in `world/terrain`; the shell renders it through the same per-vertex `paletteAt` seam as regions, no extra wiring. (Per-zone **fog/sky** cross-fade isn't wired yet — three.js fog is scene-global — so bands drive ground color today.) `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`). `detail: TerrainDetailConfig` (`{ rockColor?, sandColor?, snowColor?, rockSlopeStart?, snowHeight?, waterLevel?, detailScale?, macroScale?, roughness?, strength? }`) swaps the flat vertex-colour ground for a procedural noise shader (`@jgengine/shell`'s `createTerrainDetailMaterial`, defaults via `resolveTerrainDetail`): it keeps the biome-tinted base and blends noise-broken **rock by slope**, **sand by waterline**, and **snow by height** over it — textured-reading terrain with no image assets, full PBR (lit/shadowed/fogged), composing with the post-processing chain. Omit for the flat look. `detail.material: { maps, repeat?, strength? }` (`TerrainDetailMaterialConfig`) is the real-texture seam: `maps` is `buildMaterialCatalog({ basePath }).resolve("material/grass")!.maps` from `@jgengine/assets` (or any URLs with the same `{ color, normal, roughness, ao, displacement }` shape) — `resolveTerrainDetail` fills `repeat` (world units per tile, default 4) and `strength` (blend 0..1, default 1), and the shell's `createTerrainDetailMaterial(detail, textures)` tiles the loaded color/roughness/ao/normal by world position and blends them over the same procedural rock/sand/snow result, never replacing it. The shell resolves the map URLs to `THREE.Texture`s itself (`useTexture`/`useLoader`, e.g. in the game's `environment` component) — `core` only ever sees the URL strings. Detail noise is coordinate-stable at any world position — the fragment hash is integer/position-based, so `detailScale` keeps full fine detail far from origin (a playfield at `|x|≈550` reads the same as one near origin; the old sin-hash flattened past `worldPos/scale ≳ 100`). `sky({ preset?, timeOfDay?, horizonColor?, zenithColor?, sunIntensity?, ambientIntensity?, radius?, hazeStrength?, sunGlowStrength?, 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). The sky dome renders a bright HDR sun disc + warm glow (aligned to the sun light, so it blooms through a `postProcessing` chain), a mid-sky procedural cloud band, and a horizon haze — no HDRI assets; richer than a flat gradient with zero config. The dome **follows the camera every frame** (XZ), so it never leaves the viewer — but its `radius` (default 260) still has to clear your `fog.far` and any far scenery; a playfield sited far from the origin (or with distant landmarks) sets `radius` up to a few thousand so nothing pokes through the shell. `hazeStrength` (default 0.5) sizes the dusty horizon band and `sunGlowStrength` (default 1) the bloom around the sun disc — leave both unset for the stock look, raise/lower to dial a dusk/dust-storm horizon (the-robots uses `radius: 2600, hazeStrength: 0.62, sunGlowStrength: 0.6`). No game needs its own sky-dome mesh; configure `sky()` instead. **Gotcha:** `sunIntensity` / `ambientIntensity` are honored only on the **day** keyframe (`daylightCycle` builds dusk/night/dawn from fixed constants). Raising intensities under `preset: "dusk"` or `"night"` does nothing — use `preset: "day"` plus warm `horizonColor`/`zenithColor` when the first screenshot must be bright and readable (see `jgengine`'s first-shot art recipe).
47
+
48
+ **Default look is cinematic (#773).** A 3D game gets a real sky + shadow-casting sun/hemisphere rig + tuned post stack out of the box (`look` unset ⇒ `"cinematic"`; `defineGame({ look: "flat" })` opts back to the bare rig). The preset **defers to a world that declares its own `sky`** — an `environment({ sky })` world keeps exactly the dome/time-of-day/lighting it configured; the preset only supplies a sky (and the post stack) to worlds that have none. So `sky()` tuning below still owns the look for scenes that set it. Full rig + resolver: `jgengine-ui` → "Rendering".
49
+
50
+ **Flatten-mask composition semantics** (`withFlattenMasks` — easy to fight if guessed): masks apply **sequentially and the last matching mask wins outright**; there is no mask-to-mask blending. Inside `radius` the height becomes that mask's `target` exactly; in the `radius..radius+falloff` annulus it lerps from `target` back to the **raw noise height**, never to another mask's output. `target` defaults to the raw noise height at the mask's own center (`height` overrides). Consequences to know before composing roads or multi-site layouts: (1) a chain of default-height circles stair-steps, because each snaps to its own center's noise — give road masks explicit `height` values ramped between the endpoint site targets; (2) a later mask's falloff annulus punches raw-noise bumps through an earlier flat area — order masks so the surface that must win comes last, keep road `falloff` small (≤ radius), and skip road circles inside an already-flat site radius; (3) raw noise rarely herds — to guarantee canyon walls, emit masks with `height` *above* the surroundings flanking the corridor. A working road-ramp recipe (overlapping circles every ~12 units, height ramped between site targets, ramp clamped flat inside each site radius, wall masks alongside) lives in `Games/the-robots/src/game/world/level.ts`.
51
+
52
+ `parentSpace` positions are local to that space — convert at seams only.
53
+
54
+ Display-only map consumers use the portable `MarkerView` / `MarkerSource`
55
+ contracts in `@jgengine/core/world/markers`; the React integration and complete
56
+ external-store walkthrough are owned by the
57
+ [portable minimap recipe](../jgengine-ui/recipes/portable-minimap.md).
58
+
59
+ ### Query primitives (renderer-free, for gameplay)
60
+
61
+ Pure `@jgengine/core` functions so gameplay reads the same world the shell renders — no three.js needed:
62
+
63
+ | Primitive | Answers |
64
+ |-----------|---------|
65
+ | `resolveTerrainField(terrain(...))` / `noiseField(cfg)` → `TerrainField` | `sampleHeight(x,z)`, `sampleNormal(x,z)`, `waterLevel` — ground-snap, collision, camera. `resolveGroundStep` slope-limits movement |
66
+ | `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 |
67
+ | `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) |
68
+ | `windField(cfg)` → `WindField` | `at(t)`, `atPoint(x,z,t)`, `strengthAt` — one wind source for weather sway, grass, sailing, fire spread |
69
+ | `waterSurface(cfg)` / `waterSurfaceFromDescriptor(ocean(...))` → `WaterSurface` | `height(x,z,t)`, `normal`, `displace` — buoyancy, floating, shoreline (CPU Gerstner matching the ocean shader) |
70
+ | `scatter(cfg)` → `ScatterPoint[]` | Seeded, overlap-aware point distribution — vegetation, props, lots, spawn points (`minDistance`, `avoid` rects) |
71
+ | `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 |
72
+ | `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) |
73
+ | `resolveScatterRegion(region, terrain?)` / `resolveScatter(doc, terrain?)` | Deterministic instanced placements for a **freehand polygon** scatter region (the editor's foliage lasso): `density`/`minSpacing`/`seed`, `minScale`/`maxScale`, weighted `palette`, `maxSlope`/`minHeight`/`maxHeight` masks, `edgeFalloff` feathering, `alignToNormal`. Regions ride editor paths of `kind` `SCATTER_PATH_KIND` — `scatterRegionFromPath`/`readScatterRules`/`readScatterPalette` build one from a path's meta, `scatterRegionEstimate` gives the live count readout, `resolveScatter` walks every scatter path in a document. Polygon helpers: `pointInPolygon`, `distanceToPolygonEdge`, `polygonArea`, `polygonBounds`, `isScatterPath`. Same seed → same field; GPU-instanced, never one node per instance. |
74
+ | `resolveAuthoredObjects(doc)` / `placeAuthoredObjects(store, objects, sampleHeight, opts?)` / `placeAuthoredObjectsFromDocument` / `markerCatalogId(marker)` | Catalog mesh props from editor markers: every marker with first-class `catalogId` or `meta.catalogId` → `{ catalogId, x, z, rotationY, instanceId, verticalOffset }[]`. `placeAuthoredObjects` grounds on `sampleHeight` and calls `store.place` (WorldScene renders via `objectModels`). Shell: `<AuthoredObjects document field />` or `<AuthoredScene placeObjects />`. Never hand-loop `document.markers` for prop placement. |
75
+ | `buildingIndex(district)` → `BuildingIndex` | `at`/`within`/`nearest`/`isInside`/`blockers` over a generated district — placement avoidance, pathfinding |
76
+
77
+ **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`).
78
+
79
+ **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`.
80
+
81
+ Renderers for these descriptors live in `@jgengine/shell` (`shell/terrain`, `shell/water`, `shell/weather`, `shell/structures`).
82
+
83
+ ### Environment fields, weather hooks & realm composition
84
+ 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`.
85
+ - **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.
86
+ - **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.
87
+ - **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.
88
+ - **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.
89
+ - **Gameplay visibility / fog-of-war** (`world/visibilityField`, distinct from render culling in `core/visibility`): `createVisibilityField({ bounds, cellSize, memory })` → `VisibilityField` tracks per **viewer group** whether each grid cell is `hidden`, `remembered`, or `observed`. Each update the caller feeds a group's observed cells — `observe(group, cells)` or `observeCircle(group, x, z, radius)`, unioning multiple sources via `cellsInRadius(...out)` — and gets back a bounded `VisibilityDelta` (never a full-grid scan). Terrain memory is injected, not baked: `{ kind: "permanent" }` keeps last-known terrain (RTS explored map, generalizing single-group `world/fog`), `"none"` is pure line-of-sight, `{ kind: "decay", updates }` ages memory out. Day/night, height, stealth, and sensor rules stay the caller's job — they decide which cells count as observed. `visibleTo(group, entities, positionOf)` is the authoritative disclosure filter: it returns only entities in cells the group currently **observes**, so a unit that left view (or a rival group's units) never leaks into a client's replica — enemy re-hides while remembered terrain stays. `toState()`/`restore` round-trips sparsely; `cells(group)` gives dense codes for minimap draw.
90
+ ### Survival meters, moodles & multi-region health
91
+ The `survival/` domain — decay-over-time meters and per-part health, both feeding one stacking **moodle** status display distinct from numeric bars.
92
+ - **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.
93
+ - **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)`.
94
+ - **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).
95
+ ### Interactive building & terraform (renderer-free tools)
96
+ Turn data-only placement into the build tooling of Valheim/Enshrouded/The Sims/Fortnite/Dinkum. Pure `@jgengine/core/world` math + `@jgengine/shell/structures` render (`PlacementGhost`, `TransformGizmo`); terraform brush via `shell/terrain/*`. Demo: `apps/dev` `builder-sandbox`.
97
+ | Primitive | Answers |
98
+ |-----------|---------|
99
+ | `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()`. |
100
+ | `placeAssetFromCommit(commit, assetId, opts?)` / `resolvePlaceAsset` | **One place-asset verb** for editor `place_asset` and in-game builds → `PlaceAssetResult`. `toStructureInput(result)` → structure store; `toEditorMarker(result)` → scene-document marker. |
101
+ | `PlacementGhost` / `TransformGizmo` (`@jgengine/shell/structures`) | Ghost preview (valid/invalid tint). Runtime select/move/rotate/scale gizmo — editor `SelectionGizmo` mounts the same `TransformGizmo`. |
102
+ | `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. |
103
+ | `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. |
104
+ | `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. |
105
+ | `createPlacedStructureStore()` | Save/load a built layout: `add`/`move`/`rotate`/`remove`/`select`, `snapshot()`↔`load()` round-trip (survives reload), `subscribe` for the renderer. |
106
+ | `createEditableTerrain({ bounds, base, cellSize })` → `EditableTerrain` | A `TerrainField` you can **write back to**: `apply(edit: TerraformEdit)` sculpts `raise`/`lower`/`smooth`/`flatten`/`noise`/`ramp` (plus surface `paint`) under a cursor and re-samples `sampleHeight`; brush `shape`/`heightLimit`/`falloff` shape the footprint; `surfaceAt`, `snapshot`/`restore`, `reset`. `createTerraformBrush(field)` is the simple cursor tool (radius/strength). This write-back grid is the shared terrain-edit pattern. |
107
+ | Compact sculpt strokes | `beginTerraformStroke(terrain)` batches a whole drag of stamps into one `TerraformDelta` (`.stamp(edit)` → `.delta()`) so undo replays a stroke as a single step without copying the heightfield. `editDelta`/`applyDelta`/`revertDelta` do the same for one edit. Serialize with `createTerrainSnapshot({ bounds, cellSize })` and rebuild via `editableTerrainFromSnapshot(snapshot, base)`; `applyDeltaToSnapshot`/`revertDeltaFromSnapshot` redo/undo a delta onto a stored snapshot copy-on-write. Backs the editor's Terrain tool (`jgengine-editor`). |
108
+ | Material paint strokes | The same delta model for surface layers: `terrain.paintDelta(edit)` / `beginSurfaceStroke(terrain)` record a `SurfaceDelta`; `fillSurfaceDelta(surface)` fills or clears (`null`) every cell; `autoPaintDelta({ surface, minSlope, maxSlope, minHeight, maxHeight })` paints by a slope/height rule (rock on steep, snow up high). `applySurfaceDelta`/`revertSurfaceDelta` on the live field, `applySurfaceDeltaToSnapshot`/`revertSurfaceDeltaFromSnapshot` copy-on-write on a stored snapshot. `surfaceAt(x,z)` reads the painted id back for coloring. |
109
+ | `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). |
110
+ | `createFootprintGrid({ cellSize? })` (`world/footprintGrid`) | Multi-cell footprint occupancy/reservation on a shared build grid — `cellsFor(origin, footprint, quarterTurns?)` derives the cells, `reserve(id, kind, cells)`/`release(id)`/`isFree`/`occupantAt`/`kindAt` hold the persistent claim `placementController`'s ghost preview doesn't. `footprintObstacles(grid)` bridges live reservations into `world/placement`'s `PlacementRules.obstacles` unchanged — no separate obstacle bookkeeping. `boundaryNeighbors(grid, cells)`/`hasValidAdjacency(grid, cells, accepts, requireConnection?)` validate connective-piece adjacency (roads/pipes/belts): every touching occupied neighbor must satisfy `accepts(kind)`, and `requireConnection` demands at least one (a road segment with nothing to connect to is invalid). |
111
+
112
+ ### Physics world (optional, headless)
113
+
114
+ `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`).
115
+
116
+ **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.
117
+
118
+ **Collision shapes are box/sphere/voxel — plus opt-in triangle meshes for raycasts and mesh-accurate walk obstruction.** 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. The one exception is opted-in catalog assets: an asset listed in `@jgengine/assets` `COLLISION_MESH_ASSET_IDS` ships a compact quantized triangle mesh in the generated index (`scene/collisionMesh` `CollisionMeshData`, extracted from the GLB at reindex — never from the rendered scene, so a headless host resolves the same shape) **plus a deterministic compound-box decomposition** (`voxelizeToBoxes` → `CollisionMeshData.boxes`, a voxel-rasterize + column-fill + greedy-merge of the model's solid volume). Collider auto-fit then (a) raycasts the actual triangles through a precomputed BVH — shots pass through a torus hole or an archway instead of hitting the fitted box; and (b) exposes the compound boxes on the fitted `mesh` `ColliderShape` (`ColliderShape.mesh.boxes`, scaled/translated onto the placement like the mesh). The `collideObjects` walk controller consumes these in `stepPlayerMovement` (host and client alike, so parity is automatic): it gathers obstacles from resolved blocking physical colliders via a bounded `object.inBox` broadphase (reach ≥ radius + max half-extent, so a wide wall's full span blocks, not a phantom 1 m box), carries each collider's real `halfExtents`/`offset` — or, for a mesh collider, the compound sub-boxes — yaw-rotated onto the obstacle, and slides the capsule against **each** sub-box (`resolveObstacleStep`), so a capsule walks THROUGH an archway opening and is stopped by its pillars/lintel/sill. `PhysicsWorld` and overlap tests still see the conservative fitted AABB; objects with no resolved collider keep today's default box, objects whose colliders don't block stop obstructing, and non-opted assets keep the fitted box everywhere. The seams for custom *walk* collision remain `movement.beforeCommit` and object raycasts.
119
+
120
+ **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.
121
+
122
+ **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.
123
+
124
+ **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.
125
+
126
+ **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.
127
+
128
+ **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).
129
+
130
+ **Swarm-scale presentation.** `@jgengine/shell/world/SpriteBatch` renders up to `capacity` sprite-sheet quads as one `InstancedMesh` (per-instance atlas `frame`, `plane: "xy" | "xz"` or `billboard`, `pixelated` nearest-filtering) — the batched path for bullet-heaven swarms, item streams, and tile-grid presentation instead of one `<sprite>` per entity. Pair it with `world/lod`'s `createLodScheduler` (see the concept table) to band render detail by distance and throttle far entities' update work.
131
+
132
+ **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.
133
+ **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).
134
+ **Group movement (`movement/formation` + `movement/avoidance`).** Three separable seams keep "where the group stands" apart from "how each member gets there" so a game can pair them with any route source. `placeFormation(destination, facing, count, generator)` maps a `FormationSlotGenerator`'s local `[right, forward]` offsets into world XZ slots around a target — formation shape is **caller data**, not an engine enum; `lineFormation`/`boxFormation`/`wedgeFormation`/`circleFormation` are sample generators and a custom one needs no engine edit. `assignFormationSlots(members, slots, { previous?, stickiness? })` is deterministic greedy nearest matching (`assignment[member] = slot`, `-1` when short on slots); `stickiness` biases a member toward its previous slot to cut per-frame reshuffle. `facingYaw(from, to)` orients a formation along its travel heading. Then `resolveLocalAvoidance(agents, { iterations?, strength?, padding?, weights? })` pushes overlapping circular agents apart in place — a bounded, deterministic Jacobi relaxation over an internal 3×3 hash grid (never an O(n²) whole-world scan), honoring per-agent radii and inverse-mass `weights` (`0` = pinned anchor/obstacle). Route planning (nav path, flow field, direct seek) stays the caller's; this owns slot geometry, matching, and separation only. Squads, parties, convoys, and non-formation crowds all compose the same three calls.
135
+ ### Vehicles, mounts, crash damage & racing
136
+ 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)`.
137
+ - **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. `sampleAxisBindings` is the instantaneous, unsmoothed read the channel ramps toward each frame — exposed headlessly as `ctx.input.axis(bindings, ranges?)`, bound to **action names** rather than raw key codes (#533.7).
138
+ - **`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.
139
+ - **`physics/kinematicVehicle`.** `createKinematicVehicle(tuning, options?)` is the no-`PhysicsWorld` sibling of `vehicleBody` — same steer-yaw/accel/grip-curve arcade feel, driven purely by `tick(dt, axis, modifiers?)` returning `{ position, heading, forwardSpeed, lateralSpeed, slip, surface }`. `surfaceFriction`/`dragAt` hooks (grip-by-position, drag zones) and `clampMove` (walls/arena bounds) keep game flavor out of the sim; `scaleVelocity`/`resetTo` cover boost pads and respawns. Racers and open-world cars that don't need real rigid-body collision.
140
+ - **`physics/drivableVehicle`** (#533.1). `tickDrivableVehicle(vehicle, dt, axis, { groundHeight?, modifiers? })` is the missing wire from an `AxisInput` sample through a `KinematicVehicle` to a `setPose`-ready entity patch — the throttle/steer/handbrake → sim → `entities.setPose(id, drive.pose)` loop every drivable-vehicle game hand-rolled, one call instead of a hand-copied per-game `stepCar`. `groundHeight` snaps the resulting `y` to terrain each tick; omit it to keep the sim's flat `y`. Returns `{ pose, step }` — `step` is the raw `KinematicVehicleStep` for HUD speed/slip reads.
141
+ - **`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.
142
+ - **`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.
143
+ - **`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.
144
+ - **`scene/vehicleSeat`** (#533.2). `createVehicleSeats(controller?)` composes `scene/mount`'s control-transfer with the enter/exit-vehicle transition every game hand-rolled: `register({ id, kit, seats? })` then `enter(riderId, vehicleId, options?)` resolves a free seat and returns `{ seat, cameraTarget, driveTarget, riderMovementPatch }` in one call — feed `cameraTarget` into `ctx.camera.follow(...)`, `riderMovementPatch` (`{ frozen: true }`) into `entities.update(riderId, { movement: { ...current, frozen } })`. `exit(riderId, vehiclePose, offset?)` computes a side-door placement (`distance`/`side: "left" | "right"`) next to the vehicle and returns the same triad in reverse (`riderMovementPatch: { frozen: false }`, `cameraTarget` back to the rider). Pure — no entity/camera side effects, the caller applies the patches via its own `ctx`. `isSeated`/`driverOf`; `.mounts` exposes the underlying `MountController` for direct dismount (e.g. a vehicle explosion that skips the placement math). GTA-style boarding, mountable creatures, boat docking.
145
+ - **`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.
146
+ - **`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.
147
+ - **Session lifecycle** (`RaceSessionState`, phases `idle → countdown → racing → finished`) — the pure clock every racer wraps around `RaceState`, so no game hand-rolls its own `RacePhase`/`COUNTDOWN_SECONDS`. `idleRaceSession()` is the grid; `startRaceCountdown({ seconds })` drops the lights (default 3, `seconds ≤ 0` = standing start); `tickRaceSession(session, dt)` bleeds the countdown then accumulates `elapsed` once green; `finishRaceSession(session)` freezes the clock at the flag. It carries no track/lap data — compose it with `RaceState` (positions/laps) and `createLapTimer` (per-lap wall-clock). A racing game layers medals/DNF on top of it.
148
+ ```ts
149
+ let session = startRaceCountdown(); // { phase: "countdown", countdown: 3, elapsed: 0 }
150
+ session = tickRaceSession(session, 3); // { phase: "racing", countdown: 0, elapsed: 0 }
151
+ session = tickRaceSession(session, 12.4); // elapsed 12.4
152
+ session = finishRaceSession(session); // { phase: "finished", elapsed: 12.4 }
153
+ ```
154
+ - **Placement → outcome** turns a `race.finished` `ranking` (or any finish order, index 0 = winner) into results: `racePlacements(order, { winningPlaces })` → `{ racerId, place, outcome }[]` (`place` 1-based); `placementOf(order, id)` → one racer or `null`; `raceOutcomeOf(order, id)` is the `ranking[0] === player ? "win" : "lose"` check generalized to a `winningPlaces` podium (an absent racer loses). A caravan-race game derives its win/strand this way.
155
+ ```ts
156
+ raceOutcomeOf(event.ranking, ctx.player.userId); // "win" | "lose"
157
+ racePlacements(event.ranking, { winningPlaces: 3 }); // podium = wins
158
+ ```
159
+
160
+ ### Sim-snapshot → scene-entity pose bind
161
+
162
+ `ctx.scene.entity.bind(key)` (`scene/bodyBind`, `createBodyBind`) replaces a hand-written per-tick `setPose` loop plus its `despawn`/`spawn` respawn dance: call `bind(key).sync(bodies, dt?)` once a tick with every sim body's current snapshot (`{ id, kind, position, rotationY?, rotationX?, rotationZ?, role?, meta? }`, `kind` = catalog entity name, read only the first time an `id` appears). An `id` seen for the first time spawns from its `kind`; one already bound gets posed (`dt` derives velocity the same as a direct `setPose`); a previously-bound `id` absent from this tick's `bodies` is despawned — a dying/leaving sim body just stops appearing in the iterable, no explicit `despawn` call. `bind` is lazily keyed (one bind per swarm/roster the game tracks — a player+rivals racing pack, a wave of enemies, a vehicle pool), so unrelated scene entities are untouched. `wreckway` and similar racing packs, caravans, and creature-swarms drive their racer/caravan/creature-swarm poses this way.
163
+ ```ts
164
+ ctx.scene.entity.bind("racers").sync(
165
+ [{ id: ctx.player.userId, kind: KART_PLAYER_ENTITY, position, rotationY, role: "player" }],
166
+ dt,
167
+ );
168
+ ```
169
+
170
+ ### Authored behavior triggers (volumes / markers)
171
+
172
+ Schema'd `on`/`action` meta on editor volumes and markers — "when player enters this volume, spawn wave 2" is scene data, not game code. `@jgengine/core/scene/authoredTriggers`:
173
+
174
+ ```ts
175
+ registerTriggerAction({
176
+ id: "spawn_wave",
177
+ label: "Spawn wave",
178
+ schema: { fields: [{ type: "number", key: "wave", default: 1, min: 1 }] },
179
+ });
180
+ // volume.meta: { on: "enter"|"exit"|"interact", action: "spawn_wave", wave: 2 }
181
+ const runtime = createAuthoredTriggerRuntime({
182
+ document: editorLayers,
183
+ handlers: { spawn_wave: (e: TriggerDispatchEvent) => startWave(e.params.wave as number) },
184
+ });
185
+ // each tick:
186
+ runtime.step({
187
+ actors: [{ id: playerId, position: entity.position }],
188
+ interact: ctx.input.justPressed("interact") ? [playerId] : [],
189
+ });
190
+ // discovery: listTriggerActions() / getTriggerAction(id) / collectAuthoredTriggers(doc) / pointInVolume(volume, point)
191
+ // types: TriggerEvent · TriggerActionDefinition · AuthoredTrigger · TriggerHandlers · TriggerSourceKind · AuthoredTriggerRuntime
192
+ ```
193
+
194
+ **Built-in actions** — every game gets `announce` / `win` / `advance` for free, so "enter this zone → win" is authored entirely in the editor with no bespoke action code. `registerBuiltinTriggerActions()` exposes them in the inspector; `createTriggerOutcome()` returns a headless, serializable `TriggerOutcome` (`{ won, message, tone, objective, revision }`) plus the `handlers` to pass the runtime — the HUD reads it via `useSyncExternalStore`. That pair is the whole zero-code "reach the goal to win" wiring the create scaffold ships (a `goal` marker with `{ on: enter, action: win }`).
195
+
196
+ ```ts
197
+ registerBuiltinTriggerActions(); // announce/win/advance in the editor
198
+ const outcome = createTriggerOutcome(); // { get, subscribe, handlers, reset }
199
+ const runtime = createAuthoredTriggerRuntime({ document: editorLayers, handlers: outcome.handlers });
200
+ // HUD: const status = useSyncExternalStore(outcome.subscribe, outcome.get, outcome.get); // status.won / status.message
201
+ ```
202
+
203
+ Multi-triggers: `meta.triggers: [{ on, action, ...params }]`. Editor inspector renders action params from the declared schema when the game has registered actions.
204
+
205
+ ### Continuous area effects (source-following membership)
206
+
207
+ The dynamic counterpart to authored triggers: moving sources emit an area that **follows them** each tick, receivers get `enter`/`refresh`/`leave` edges, and membership is cleaned up on source removal/disable/clear. Genre-agnostic and pure — it never applies damage/heals/stats itself; you route each edge through your own pipeline (auras, hazard/heal fields, capture zones, silence/music areas). `@jgengine/core/area/areaEffectField`:
208
+
209
+ ```ts
210
+ const field = createAreaEffectField<{ dps: number }>();
211
+ // each tick, per live source (so the shape follows its emitter):
212
+ field.setSource({ id: bossId, shape: { kind: "sphere", center: bossPos, radius: 3 }, payload: { dps: 6 }, refreshMs: 700 });
213
+ field.removeSource(deadBossId); // -> leave edges (reason "source-removed"); auto cleanup
214
+ const events = field.step({
215
+ dtMs: dt * 1000,
216
+ candidates: (shape) => index.querySphere(shape.center[0], shape.center[1], shape.center[2], shape.radius, out), // bounded broad phase
217
+ positionOf: (id) => positions.get(id), // undefined => receiver gone -> leave
218
+ eligible: (sourceId, receiverId) => isEnemy(receiverId), // optional team/immunity gate
219
+ });
220
+ for (const e of events) if (e.kind === "refresh") applyDamage(e.receiverId, e.payload.dps * (e.ticks ?? 1));
221
+ ```
222
+
223
+ - Bounded by design (caller supplies the candidate query) and serializable (`serialize()` keeps membership + refresh phase across save/load; re-`setSource` live shapes before the next `step`).
224
+ - `custom` shape leaves a seam for authored volumes / non-sphere shapes while still exposing a bounding sphere for the broad phase.
225
+ - Overlap aggregation is a policy over `field.membershipsOf(receiverId)` — `@jgengine/core/area/stackPolicy`: `independentStacks` · `uniqueByStackKey` · `cappedStacks` · `extremumStack` (strongest/weakest) · `sumMagnitude` (additive).
226
+ - First adopter: `Games/the-robots` boss overload auras (`game/entities/enemies/hazardAura.ts`).
227
+
228
+ ### Spawn placement
229
+
230
+ `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.
231
+
232
+ **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.
233
+
234
+ **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.
235
+
236
+ ### Ambient behaviors + instance lifecycle (`scene/behaviors`, `scene/behaviorRuntime`)
237
+
238
+ Attach `patrol({ waypoints, speed, loop?, startProgress?, groundClamp? })` or `wander({ radius })` in a spawn's `behaviors`; the host's `advanceBehaviors(ctx, dt)` (already wired in the runners) keeps the per-entity nav state and poses it — register-once ambient traffic/idle routes, never a per-game `advancePathFollow` + `setPose` loop. `startProgress` seeds a follower at a distributed phase (see `PathProgress` below); `groundClamp: true` samples world ground height for the pose Y so an XZ route rides terrain.
239
+
240
+ `behaviorControl(ctx)` is the per-entity lifecycle surface (keyed by entity id, no full-world scan) games use instead of bypassing the runtime for possession/freezing/streaming/staggering:
241
+
242
+ ```ts
243
+ const bc = behaviorControl(ctx);
244
+ bc.pause(id, "driven"); // retain state, stop advance+pose (player commandeered the car)
245
+ bc.resume(id); // catch-up policy: "freeze" (default, deterministic) | "advance" (silently fast-forward paused span)
246
+ bc.disable(id, "panic"); // hold off until enable() — frozen in place
247
+ bc.enable(id);
248
+ bc.seek(id, { kind: "normalized", value: 0.5 }); // patrol only; jump to semantic progress
249
+ bc.serialize(id) / bc.restore(id, snap); // BehaviorSnapshot round-trips exactly
250
+ bc.status(id) / bc.reason(id) / bc.inspect(id) / bc.list(); // editor/debug inspection
251
+ ```
252
+
253
+ Path progress adapter (`nav/pathFollow`): `PathProgress` is `{ kind: "normalized", value } | { kind: "distance", value } | { kind: "segment", index, fraction }` — travel direction falls out of the resulting `heading`. `pathFollowSeek(config, progress)` places a follower without simulating from the start (loops wrap, non-loops clamp + report `done`); `pathFollowProgress(config, state)` reads it back as `{ distance, normalized, segment, fraction }`; `pathLength(config)` sums the polyline. Vice-isle's traffic + pedestrians run entirely on this seam (phase-distributed `startProgress`, `pause` on drive-away, `disable` on wanted-panic) — no hand-rolled route loop.
254
+ ### Entity orders (command queue)
255
+
256
+ `orders/orderQueue` + `orders/orderKinds` — a serializable per-entity command queue for player- or AI-issued intent, instead of a hard-coded RTS verb kit. Build a shared registry once with `createOrderRegistry<Ctx>()` and register verbs as data; engine code never branches on a verb. Give each commandable unit its own `createOrderQueue(registry, { genId?, onEvent? })`.
257
+
258
+ - **Lifecycle**: `issue(request)` validates and queues; `tick(ctx, dt)` activates the next order, advances the active one, and completes/cancels it — returning `{ active, activated, completed, canceled }`. `cancelActive`/`cancelAll`/`clear` force cleanup. `serialize()`/`load()` (or the `initial` option) round-trip mid-flight state for save and host-authoritative replication.
259
+ - **Preemption policy** on each issue: `replace` (preempt active + clear pending), `append` (shift-queue), `front` (jump the queue without dropping the active order), `reject` (refuse while busy). Mark an order `uninterruptible` and a replacing order waits until it finishes; explicit `cancelActive` still forces it.
260
+ - **Order kinds** are `OrderKind<Ctx, Payload>` with `start`/`update`/`finish`, composed over two narrow adapters — `OrderMover` (`position`/`moveToward`/`halt`) and `OrderTargeting` (`acquire`/`positionOf`). Built-ins ship as ordinary compositions with a configurable `kind` string so a game re-skins a verb ("harvest") without engine edits: `defineMoveOrder`, `defineStopOrder`, `defineHoldOrder`, `defineAttackMoveOrder`, `defineTargetedOrder`, `definePatrolOrder`. Engagement verbs write `{ engaging, inRange }` into `order.state`; the game reads it to run the actual attack/effect, keeping combat resolution game-side.
261
+
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: level-design
3
+ description: Design, audit, author, implement, and validate readable levels, worlds, encounters, navigation, and pacing.
4
+ ---
5
+
6
+ # Level design
7
+
8
+ ## Ownership
9
+
10
+ Own the spatial realization of game design: player metrics, topology, routes, landmarks, gates, encounter geometry, challenge sequencing, exploration, pacing, recovery, and spatial storytelling. Use `game-design` for the player promise and loops, `jgengine-editor` to author space, and `jgengine-world` to consume it.
11
+
12
+ All authorable world content belongs in `editor.scene.json`, changed through the editor GUI or RPC/CLI. Never hand-edit the document or keep geometry, paths, spawns, zones, objectives, terrain, foliage, encounters, or coordinate arrays in code. File an editor `[FEATURE]` issue before any fallback.
13
+
14
+ Read [references/application-playbook.md](references/application-playbook.md) only when the task explicitly calls for spatial design depth — a level/world design brief, an audit or redesign, or "make this fun/better" work; it is not a mandatory step of creating a game. Read [references/live-editor-loop.md](references/live-editor-loop.md) whenever live captures, an editor-agent handoff, or iterative scene improvement is involved. Read [references/field-guide.md](references/field-guide.md) for metric families, topology, pacing, genre lenses, accessibility, and research.
15
+
16
+ ## Choose the mode
17
+
18
+ ### Greenfield
19
+
20
+ Measure the real controller, camera, interaction, combat, AI, and network constraints in an authored metric gym. Produce a topology graph, critical/optional route model, beat sheet, learning sequence, encounter cards, authoring plan, and falsifiable completion scenario before art production.
21
+
22
+ ### Existing game
23
+
24
+ Start with a spatial-truth audit. Inventory every source of coordinates and semantics; report any gameplay placement outside `editor.scene.json` as a blocking ownership defect. Reconstruct the critical path and representative optional path from scene data and live play. Measure travel, decision, encounter, retry, and recovery time. Inspect first-look orientation, reverse navigation, affordances, spawn safety, combat positions, pacing contrast, and visual hierarchy.
25
+
26
+ “Improve with level design” is an authoring and implementation request. Do not stop at a map critique: select the highest-leverage route/zone/encounter slice, author the change through editor operations, connect runtime semantics, and prove it in play.
27
+
28
+ For a live editor handoff, send evidence and an executable authoring contract—not an aesthetic request. Require the editor agent to inspect authority, document revision, ids, layers, assets, runtime state, and tool support before mutation; apply one coherent hypothesis through undoable operations; then return the exact revision/change log for replay and comparison. Stop when the live editor is not connected to the runtime's authoritative scene or the required operation is unsupported.
29
+
30
+ ## Required output contract
31
+
32
+ Make every level-design pass yield:
33
+
34
+ 1. **Spatial truth audit:** scene counts and semantic kinds plus every code-owned coordinate table, generated placement, or duplicated position that must migrate.
35
+ 2. **Metric sheet:** measured movement, camera, interaction, weapon/AI, readability, density, performance, multiplayer, and retry constraints with units.
36
+ 3. **Topology and journey:** nodes/edges, critical and optional routes, gates, returns, landmarks, expected traversal times, and first-minute/mission/campaign beats.
37
+ 4. **Zone/encounter diagnosis:** purpose, entry knowledge/state, choices, geometry, actors, escalation, resources, exits, failure/reset, reward, and downstream change.
38
+ 5. **Bottleneck ranking:** observed symptom, player consequence, root cause, evidence, reach, confidence, effort, dependencies, and ownership.
39
+ 6. **Editor change set:** exact semantic ids/kinds and operations to add/move/remove for paths, markers, zones, spawns, objectives, checkpoints, props, and terrain—without putting coordinates in code.
40
+ 7. **Acceptance evidence:** document assertions, route/encounter tests, fresh-player play scenario, pacing/wayfinding thresholds, inspected screenshots, and before/after comparison.
41
+
42
+ ## Execution rules
43
+
44
+ - Graph before geometry; graybox before art; gameplay and wayfinding before decoration.
45
+ - Present decision-relevant information before commitment. Make alternate routes differ in risk, cost, verbs, information, reward, or experience.
46
+ - Sequence teaching through cue, safe use, feedback, independent test, combination, transformation, and mastery as the audience requires.
47
+ - Align composition, landmark, value/color, motion, audio, affordance, path shape, and optional UI guidance. No marker can repair contradictory space.
48
+ - Design pacing across separate threat, motor, cognitive, navigation, emotional, resource, and social axes. Constant pressure or empty traversal is not pacing.
49
+ - Make every encounter support intended verbs, counterplay, retreat, fair spawns, recovery, and bounded performance.
50
+ - Populate only after the graybox survives play. Preserve silhouettes, safe footing, goals, threats, interactables, teams, and traversal boundaries through art passes.
51
+
52
+ ## Completion
53
+
54
+ An audit or prettier screenshot is not completion for a build/improve request. Finish when the scene document owns spatial truth; a fresh target player can orient, choose, act, recover, and complete the scenario without coaching; the route has intentional decisions and pacing; encounter exploits and alternate states are tested; and `jgengine-verify` records document, gameplay, and inspected visual evidence.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Level Design"
3
+ short_description: "Design readable, paced, playable spaces"
4
+ default_prompt: "Use $level-design to observe this world live, brief and drive the editor agent through an authoritative scene change, then replay the same journey and prove the result."
@@ -0,0 +1,192 @@
1
+ # Applying level design
2
+
3
+ Use this playbook to turn spatial intent or an existing world into editor-authored, playable change. Its purpose is to force concrete topology, authoring operations, and evidence rather than aesthetic commentary.
4
+
5
+ ## Contents
6
+
7
+ 1. Greenfield procedure
8
+ 2. Existing-world audit
9
+ 3. Spatial truth and metrics
10
+ 4. Journey, topology, and beats
11
+ 5. Zone and encounter diagnosis
12
+ 6. Ranking interventions
13
+ 7. Editor change set
14
+ 8. Implementation and proof
15
+ 9. Failure modes
16
+
17
+ ## 1. Greenfield procedure
18
+
19
+ Before production art:
20
+
21
+ 1. Import the game-design contract: promise, verbs, repeated decision, loops, target player, and completion proof.
22
+ 2. Author a metric gym and measure the actual controller, camera, interaction, combat, AI, and network behavior.
23
+ 3. Draw the topology graph with starts, goals, critical/optional routes, gates, returns, checkpoints, encounters, safe spaces, and rewards.
24
+ 4. Write a beat sheet across threat, motor, cognitive, navigation, emotional, resource, and social intensity.
25
+ 5. Map where each mechanic is cued, tried safely, tested, combined, transformed, and mastered.
26
+ 6. Specify encounter cards and the semantic scene ids they require.
27
+ 7. Graybox one route/zone slice in `editor.scene.json` through GUI or RPC/CLI.
28
+ 8. Playtest orientation, decisions, traversal, encounter flow, failure, and retry before population and art.
29
+
30
+ ## 2. Existing-world audit
31
+
32
+ ### Inventory spatial truth
33
+
34
+ Inspect the editor document and source tree. Report separately:
35
+
36
+ - authored paths and point counts
37
+ - authored markers grouped by semantic kind and catalog id
38
+ - authored zones/volumes, terrain, foliage, annotations, prefabs, and collections
39
+ - gameplay spawns, objectives, NPCs, vendors, loot, checkpoints, gates, and encounters
40
+ - coordinate arrays, centers, offsets, radii, procedural placement, and duplicated positions in code
41
+ - runtime queries that consume stable scene ids/kinds
42
+
43
+ If gameplay placement exists in code but not the document, mark the spatial-truth audit failed before discussing polish. The repair is an editor-authored migration with stable semantics, not another adapter around coordinate tables.
44
+
45
+ ### Observe live play
46
+
47
+ Capture and inspect:
48
+
49
+ - spawn/first frame and the first ten seconds
50
+ - first decision and first objective
51
+ - one critical-path transition
52
+ - one representative encounter and retry
53
+ - one optional route/POI
54
+ - reverse travel or return after state change
55
+ - a dense/peak gameplay frame
56
+
57
+ Record what dominates the frame, what the player can infer, where they look, where they move, what they miss, and whether UI/world cues agree. Pixel metrics only reject technical blanks; human inspection judges composition and gameplay readability.
58
+
59
+ ## 3. Spatial truth and metrics
60
+
61
+ ### Metric sheet
62
+
63
+ | Family | Measured values | Source/build | Design consequence | Extreme/accessibility case |
64
+ | --- | --- | --- | --- | --- |
65
+ | movement | speed, acceleration, stop, turn, jump, slope, radius | | | |
66
+ | camera | perspective, FOV, offset, obstruction, sensitivity | | | |
67
+ | interaction | range, use time, target clarity | | | |
68
+ | combat/AI | weapon bands, projectile time, sensing, attack, group size | | | |
69
+ | navigation | path width, sightline, branch/rejoin, landmark range | | | |
70
+ | pacing | time to action, encounter, rest, reward, retry | | | |
71
+ | multiplayer | spawn, regroup, revive, latency, count | | | |
72
+ | performance | actor/object density, nav, draw, simulation | | | |
73
+
74
+ Derive dimensions from the measured game. Never paste corridor, cover, jump, or arena numbers from another title.
75
+
76
+ ### Travel-time audit
77
+
78
+ Measure each route at the actual movement speed, including turns, hazards, encounters, and interaction delay. For every interval, classify the intended value: decision, mastery, anticipation, story, resource planning, combat, exploration, recovery, or deliberate atmosphere. Empty time with no purpose is a pacing defect even when the scenery is attractive.
79
+
80
+ ## 4. Journey, topology, and beats
81
+
82
+ ### Topology table
83
+
84
+ | Node/edge | Purpose | Entry information | Choice/tradeoff | Gate/return | Expected time | Downstream change |
85
+ | --- | --- | --- | --- | --- | --- | --- |
86
+
87
+ Validate reachability for relevant quest/world states and the reverse direction. Branches must differ in risk, cost, verb, information, reward, story, or experience.
88
+
89
+ ### Beat sheet
90
+
91
+ | Time/distance | Beat | Threat | Motor | Cognitive | Navigation | Emotion/resource | Evidence/failure signal |
92
+ | --- | --- | --- | --- | --- | --- | --- | --- |
93
+
94
+ Look for long flats, constant peaks, unseen goals, repeated rhythms, rewards without anticipation, combat without aftermath, and failure without fast useful retry.
95
+
96
+ ### First-minute contract
97
+
98
+ The first minute must deliberately answer:
99
+
100
+ - Where am I and what fantasy am I inhabiting?
101
+ - What can I do now?
102
+ - What should I notice first?
103
+ - What is my immediate goal and why?
104
+ - Where is the first safe experiment?
105
+ - What consequence teaches the rule?
106
+ - What next decision promises depth?
107
+
108
+ Do not answer all seven with text. Use space, action, feedback, and only the necessary UI.
109
+
110
+ ## 5. Zone and encounter diagnosis
111
+
112
+ ### Zone card
113
+
114
+ - role in the game/session loop
115
+ - visual/spatial identity and landmark hierarchy
116
+ - entry/exit states and critical/optional routes
117
+ - mechanics introduced, tested, combined, and mastered
118
+ - decision types and exploration rewards
119
+ - pacing arc, safe spaces, checkpoints, and return value
120
+ - content budget and performance peak
121
+ - spatial story and world-state change
122
+
123
+ Zones that differ only by palette, enemy level, or kill quota do not provide meaningful progression.
124
+
125
+ ### Encounter card
126
+
127
+ - purpose and target player behavior
128
+ - entry sightline, knowledge, position, resources, and condition
129
+ - initial decision and viable approaches
130
+ - geometry for every intended verb and counterplay
131
+ - actors/hazards, escalation, reinforcement, peak, and resolution
132
+ - cover/refuge, retreat, flank, exits, and exploit risks
133
+ - reward, failure/reset, retry time, and downstream state change
134
+ - semantic scene ids and performance budget
135
+
136
+ ## 6. Ranking interventions
137
+
138
+ Rank candidate changes with evidence:
139
+
140
+ | Candidate | Player behavior changed | Reach | Evidence/confidence | Effort/risk | Dependencies | Editor/runtime owner |
141
+ | --- | --- | --- | --- | --- | --- | --- |
142
+
143
+ Use three horizons:
144
+
145
+ - **Minimum correction:** migrate invalid spatial ownership or repair a blocking orientation/flow failure.
146
+ - **Strong target:** make one representative route/zone/encounter deliver the intended promise end to end.
147
+ - **Multiplier:** propagate the proven spatial grammar, semantic kinds, encounter kit, and tests across the world.
148
+
149
+ Prioritize upstream seams and reusable authoring kinds. Do not spend the first pass scattering props across a structurally unchanged map.
150
+
151
+ ## 7. Editor change set
152
+
153
+ Specify operations semantically, not as code coordinates:
154
+
155
+ | Operation | Stable id/kind | Purpose/meta | Runtime consumer | Acceptance |
156
+ | --- | --- | --- | --- | --- |
157
+ | add/move/remove path | | from/to, route role | | |
158
+ | add/move/remove marker | | spawn/objective/NPC/checkpoint/landmark | | |
159
+ | add/move/remove zone | | encounter/settlement/safe/gate/resource | | |
160
+ | terrain/foliage/material pass | | traversal/readability/identity | | |
161
+ | prop/asset pass | | cover/affordance/story/landmark | | |
162
+
163
+ Execute through `jgengine-editor`. Gameplay code queries ids, kinds, layers, and metadata; it does not receive copied positions. File an editor feature issue if a required semantic kind or operation does not exist.
164
+
165
+ ## 8. Implementation and proof
166
+
167
+ When the user asks to build or improve:
168
+
169
+ 1. Establish before evidence and the acceptance scenario.
170
+ 2. Author the chosen graybox and semantics through the editor.
171
+ 3. Connect shared runtime/gameplay primitives to the authored ids.
172
+ 4. Migrate superseded coordinate tables without changing unrelated content.
173
+ 5. Add document assertions for required paths, zones, markers, and ids.
174
+ 6. Add deterministic route, encounter, reset, and state-transition tests.
175
+ 7. Run a fresh-player or representative play scenario.
176
+ 8. Capture and inspect the same views before/after.
177
+ 9. Evaluate timing, choice, comprehension, exploit, accessibility, and performance falsifiers.
178
+
179
+ Evidence should answer the design claim. Scene counts prove ownership, headless tests prove state, a driven playtest proves progress, and inspected screenshots prove visible composition. No one rung proves all four.
180
+
181
+ ## 9. Failure modes
182
+
183
+ - praising asset count while gameplay semantics remain in code
184
+ - drawing a map without measuring the real controller/camera
185
+ - calling multiple corridors meaningful choice
186
+ - filling empty travel with collectibles instead of decisions or pacing
187
+ - changing color/lighting while topology and encounter flow remain unchanged
188
+ - using objective markers to compensate for contradictory geometry
189
+ - designing only the critical path or only the plan view
190
+ - making every zone the same objective with new enemies and palette
191
+ - reporting an audit after the user asked for improvement
192
+ - claiming success from scene counts, tests, telemetry, or screenshots alone