@jgengine/shell 0.9.0 → 0.10.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 (49) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/GamePlayerShell.d.ts +6 -21
  3. package/dist/GamePlayerShell.js +419 -234
  4. package/dist/audio/audioEngine.d.ts +8 -0
  5. package/dist/audio/audioEngine.js +33 -0
  6. package/dist/audio/musicDirector.d.ts +31 -0
  7. package/dist/audio/musicDirector.js +125 -0
  8. package/dist/audio/musicVoices.d.ts +10 -0
  9. package/dist/audio/musicVoices.js +338 -0
  10. package/dist/audio/synthEngine.d.ts +9 -0
  11. package/dist/audio/synthEngine.js +57 -0
  12. package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
  13. package/dist/camera/GameFirstPersonCamera.js +21 -4
  14. package/dist/camera/GameInspectionCamera.js +12 -1
  15. package/dist/camera/GameOrbitCamera.js +35 -1
  16. package/dist/camera/orbitCameraMath.d.ts +18 -0
  17. package/dist/camera/orbitCameraMath.js +6 -1
  18. package/dist/cartridge.js +1 -0
  19. package/dist/commandSink.d.ts +20 -0
  20. package/dist/commandSink.js +27 -0
  21. package/dist/defineGame.js +3 -1
  22. package/dist/devtools/CollisionDebugWorld.js +1 -0
  23. package/dist/devtools/collisionDebugMath.d.ts +1 -0
  24. package/dist/devtools/collisionDebugMath.js +2 -1
  25. package/dist/environment/Daylight.d.ts +7 -1
  26. package/dist/environment/Daylight.js +52 -5
  27. package/dist/environment/EnvironmentScene.js +14 -5
  28. package/dist/environment/RoadRibbons.d.ts +7 -0
  29. package/dist/environment/RoadRibbons.js +57 -0
  30. package/dist/inputSink.d.ts +18 -0
  31. package/dist/inputSink.js +35 -0
  32. package/dist/multiplayer.js +7 -3
  33. package/dist/postfx/PostProcessing.d.ts +10 -0
  34. package/dist/postfx/PostProcessing.js +82 -0
  35. package/dist/postfx/gradeShader.d.ts +4 -0
  36. package/dist/postfx/gradeShader.js +64 -0
  37. package/dist/terrain/CarvedTerrain.d.ts +4 -1
  38. package/dist/terrain/CarvedTerrain.js +4 -3
  39. package/dist/terrain/terrainDetailMaterial.d.ts +14 -0
  40. package/dist/terrain/terrainDetailMaterial.js +90 -0
  41. package/dist/touch/TouchControlsOverlay.js +11 -1
  42. package/dist/world/WorldHud.d.ts +3 -1
  43. package/dist/world/WorldHud.js +7 -7
  44. package/dist/world/worldBarSamples.d.ts +1 -1
  45. package/dist/world/worldBarSamples.js +7 -1
  46. package/dist/worldSync.d.ts +10 -0
  47. package/dist/worldSync.js +19 -0
  48. package/llms.txt +140 -22
  49. package/package.json +4 -4
package/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # @jgengine/shell
2
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
3
 
4
- Version: 0.9.0
4
+ Version: 0.10.0
5
5
  License: AGPL-3.0-only
6
6
  Repository: https://github.com/Noisemaker111/jgengine
7
7
  Docs: https://jgengine.com
@@ -22,12 +22,8 @@ Imports use deep paths: `@jgengine/shell/<path>`.
22
22
  ### @jgengine/shell/GamePlayerShell
23
23
 
24
24
  - GamePlayerShell (function): function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }: { playable: PlayableGame; multiplayer?: ShellMultiplayer | null; poster?: boolean; /** Called once per boot after onInit/onNewPlayer with the live GameContext — a staging seam for screenshots,…
25
- - applyMotionImpulses (function): function applyMotionImpulses(currentVelocity: number, batch: MotionIntentBatch | null): number — Applies a pending `MotionIntentBatch` to a vertical velocity: impulses add, then `verticalVelocity` replaces the result outright (#162.4).
26
- - dispatchBoundAction (function): function dispatchBoundAction(ctx: GameContext, action: string, yaw: number, pitch: number, aim: Aim): void — Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`).
27
- - hasEnvironmentTerrain (function): function hasEnvironmentTerrain(world: WorldFeature | undefined): boolean — True when the world is an environment feature with terrain, so the voxel controller should sample its height.
25
+ - dispatchBoundAction (function): function dispatchBoundAction(ctx: GameContext, action: string, yaw: number, pitch: number, aim: Aim, reserved: ReadonlySet<string> = RESERVED_INPUT_ACTIONS, sink: CommandSink = localCommandSink(ctx)): void — Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`).
28
26
  - heldActionsFor (function): function heldActionsFor(tracker: Pick<ActionStateTracker<string>, "isDown">, actions: readonly string[]): string[] — Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions.
29
- - nearbyObstacles (function): function nearbyObstacles(objects: readonly SceneObject[], center: readonly [number, number, number], radius: number = OBSTACLE_GATHER_RADIUS): CollisionObstacle[] — Placed scene objects within `radius` of `center`, as `CollisionObstacle`s for `resolveObstacleStep` (#162.1).
30
- - resolvePhysicsTuning (function): function resolvePhysicsTuning(physics: PhysicsConfig | undefined): MovementTuningOverrides | undefined — Maps the game's declared `physics` onto the movement controllers' tuning overrides. `PhysicsConfig.gravity` is a signed world acceleration (negative points down, matching every game's config and the Y-up convention), but the controllers integrate `velocityY -= gravityAcceleration * dt` and so expect a positive downward magnitude. Negating here is what keeps a down-pointing gravity pulling the player *down*; passing the signed value straight through flipped the sign and launched airborne players upward instead.
31
27
  - resolveWorldSky (function): function resolveWorldSky(world: WorldFeature | undefined): SkyEnvironmentDescriptor | undefined — The world's declared sky, when its world feature is an environment with one (#196.1).
32
28
  - shouldFireBoundAction (function): function shouldFireBoundAction(tracker: Pick<ActionStateTracker<string>, "isDown" | "wasPressed">, action: string, input: PlayableGame["game"]["input"], repeatFiredAt: ReadonlyMap<string, number>, now: number): boolean — Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`).
33
29
 
@@ -51,6 +47,20 @@ Imports use deep paths: `@jgengine/shell/<path>`.
51
47
  - Vec3 (interface): interface Vec3
52
48
  - createAudioEngine (function): function createAudioEngine(config: AudioSceneConfig = {}): AudioEngine
53
49
 
50
+ ### @jgengine/shell/audio/musicDirector
51
+
52
+ - CrossfadeOptions (interface): interface CrossfadeOptions — Options for a music crossfade.
53
+ - MusicDirector (class): class MusicDirector — Runs a set of looping {@link MusicTheme}s as crossfadeable layers on one shared Web Audio graph: master → compressor → destination, with a fixed convolution reverb send. `crossfadeTo` swaps the audible theme; a 110ms lookahead scheduler keeps each active layer's notes queued ahead of the clock so loops are seamless.
54
+
55
+ ### @jgengine/shell/audio/musicVoices
56
+
57
+ - playMusicNote (function): function playMusicNote(ctx: Ctx, event: NoteEvent, when: number, spb: number, out: GainNode, transpose = 0): void — Play one theme note through its instrument voice into `out` (a per-layer gain node the director wires to master + reverb). `spb` is seconds-per-beat and `transpose` shifts the note in semitones for per-zone key changes. This is the engine's reusable instrument library; unknown instruments fall back to a sine.
58
+
59
+ ### @jgengine/shell/audio/synthEngine
60
+
61
+ - createNoiseBuffer (function): function createNoiseBuffer(ctx: BaseAudioContext): AudioBuffer — Build the shared 1-second mono white-noise buffer every noise voice samples from.
62
+ - realizeSynthPatch (function): function realizeSynthPatch(ctx: BaseAudioContext, out: AudioNode, noiseBuf: AudioBuffer, patch: SynthPatch): void — Realise a procedural cue on Web Audio: every voice is scheduled at `ctx.currentTime + delay` into `out`, summed into one one-shot. `noiseBuf` is the shared buffer from {@link createNoiseBuffer}.
63
+
54
64
  ### @jgengine/shell/behaviour
55
65
 
56
66
  - Object3DBehaviour (class): class Object3DBehaviour extends Behaviour
@@ -77,6 +87,7 @@ Imports use deep paths: `@jgengine/shell/<path>`.
77
87
 
78
88
  - GameFirstPersonCamera (function): function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): React.JSX.Element | null
79
89
  - GameFirstPersonCameraProps (interface): interface GameFirstPersonCameraProps
90
+ - readFirstPersonMuzzle (function): function readFirstPersonMuzzle(target: THREE.Vector3): boolean — World position of the first-person weapon muzzle, or false when no viewmodel is mounted.
80
91
 
81
92
  ### @jgengine/shell/camera/GameInspectionCamera
82
93
 
@@ -264,8 +275,10 @@ Imports use deep paths: `@jgengine/shell/<path>`.
264
275
  - GAME_SIM_FRAME_PRIORITY (const): const GAME_SIM_FRAME_PRIORITY: 0 — Run simulation/movement before orbit follow so poses are current.
265
276
  - ORBIT_CAMERA_FRAME_PRIORITY (const): const ORBIT_CAMERA_FRAME_PRIORITY: -1 — Orbit follow reads the latest entity pose after GAME_SIM_FRAME_PRIORITY.
266
277
  - OrbitCameraConfig (interface): interface OrbitCameraConfig
278
+ - OrbitCollisionConfig (interface): interface OrbitCollisionConfig — Spring-arm occlusion config for the orbit rig.
267
279
  - OrbitFollowRuntimeState (interface): interface OrbitFollowRuntimeState
268
280
  - ResolvedOrbitCameraConfig (interface): interface ResolvedOrbitCameraConfig — Fully resolved shell config after merging with DEFAULT_ORBIT_CAMERA.
281
+ - ResolvedOrbitCollision (interface): interface ResolvedOrbitCollision — An {@link OrbitCollisionConfig} with every field resolved to a concrete value.
269
282
  - Vec3 (interface): interface Vec3
270
283
  - cameraFollowStep (function): function cameraFollowStep(input: { camera: Vec3; target: Vec3; previousTarget: Vec3 | null; lockedDistance: number | null; }): CameraFollowState
271
284
  - 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.
@@ -359,6 +372,13 @@ Imports use deep paths: `@jgengine/shell/<path>`.
359
372
  - CartridgeScreens (interface): interface CartridgeScreens
360
373
  - cartridge (function): function cartridge(config: CartridgeConfig): PlayableGame
361
374
 
375
+ ### @jgengine/shell/commandSink
376
+
377
+ - CommandSink (interface): interface CommandSink — Where a gameplay command goes when the shell dispatches it — run locally, or sent to the authoritative host.
378
+ - localCommandSink (function): function localCommandSink(ctx: GameContext): CommandSink — Runs commands on the local `ctx` — the default, client-authoritative path.
379
+ - remoteCommandSink (function): function remoteCommandSink(backend: Pick<LiveGameBackend, "transport">, serverId: string): CommandSink — Sends commands to the authoritative host over the transport; the result replicates back through world sync.
380
+ - resolveCommandSink (function): function resolveCommandSink(ctx: GameContext, opts: { serverAuthoritative: boolean; backend: Pick<LiveGameBackend, "transport"> | null; serverId: string | null; }): CommandSink — The sink a server-authoritative shell should dispatch gameplay commands through: remote when the game opts into `authority: "server"` and a server is joined, local otherwise. Client-only UI commands (targeting, hotbar scroll) keep calling `ctx.game.commands.run` directly — only authoritative gameplay verbs route to the host.
381
+
362
382
  ### @jgengine/shell/defineGame
363
383
 
364
384
  - GameConfig (type): type GameConfig<TAssetRef extends ModelAssetRef = ModelAssetRef> = EngineFields<TAssetRef> & PresentationFields
@@ -428,7 +448,7 @@ Imports use deep paths: `@jgengine/shell/<path>`.
428
448
  - SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
429
449
  - SkyDaylight (function): function SkyDaylight({ sky, lights = true }: SkyDaylightProps): React.JSX.Element — Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
430
450
  - SkyDaylightProps (interface): interface SkyDaylightProps
431
- - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
451
+ - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, materialRef, }: SkyDomeProps = {}): React.JSX.Element
432
452
  - SkyDomeProps (interface): interface SkyDomeProps
433
453
  - SkyLightOwnership (type): type SkyLightOwnership = "authored" | "sky-default" — Policy for composing sky backdrops with `PlayableGame.lighting`: - authored lighting present → sky renders dome + fog only; lights stay game-owned - no authored lighting → sky may emit its default sun/hemisphere with the dome Time-of-day never rewrites configured lights; it only drives sky colors/fog (and sky-owned lights when the game did not author lighting).
434
454
  - TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock, lights = true }: TimeOfDayDaylightProps): React.JSX.Element — Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock` are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so only dome colors and fog track the day fraction.
@@ -444,7 +464,7 @@ Imports use deep paths: `@jgengine/shell/<path>`.
444
464
  - DaylightProps (interface): interface DaylightProps
445
465
  - SkyDaylight (function): function SkyDaylight({ sky, lights = true }: SkyDaylightProps): React.JSX.Element — Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
446
466
  - SkyDaylightProps (interface): interface SkyDaylightProps
447
- - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
467
+ - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, materialRef, }: SkyDomeProps = {}): React.JSX.Element
448
468
  - SkyDomeProps (interface): interface SkyDomeProps
449
469
  - TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock, lights = true }: TimeOfDayDaylightProps): React.JSX.Element — Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock` are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so only dome colors and fog track the day fraction.
450
470
  - TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
@@ -459,6 +479,10 @@ Imports use deep paths: `@jgengine/shell/<path>`.
459
479
  - GroundPad (function): function GroundPad({ pad, field }: GroundPadProps): React.JSX.Element
460
480
  - GroundPadProps (interface): interface GroundPadProps
461
481
 
482
+ ### @jgengine/shell/environment/RoadRibbons
483
+
484
+ - RoadRibbons (function): function RoadRibbons({ road, field }: { road: RoadEnvironmentDescriptor; field: TerrainField }): React.JSX.Element | null — Renders one `road()` descriptor: an asphalt ribbon plus optional dashed centerline, draped on the terrain.
485
+
462
486
  ### @jgengine/shell/environment/daylightCycle
463
487
 
464
488
  - DEFAULT_DAY_AMBIENT_INTENSITY (const): const DEFAULT_DAY_AMBIENT_INTENSITY: 0.6
@@ -490,7 +514,7 @@ Imports use deep paths: `@jgengine/shell/<path>`.
490
514
  - SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
491
515
  - SkyDaylight (function): function SkyDaylight({ sky, lights = true }: SkyDaylightProps): React.JSX.Element — Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
492
516
  - SkyDaylightProps (interface): interface SkyDaylightProps
493
- - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
517
+ - SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, materialRef, }: SkyDomeProps = {}): React.JSX.Element
494
518
  - SkyDomeProps (interface): interface SkyDomeProps
495
519
  - SkyLightOwnership (type): type SkyLightOwnership = "authored" | "sky-default" — Policy for composing sky backdrops with `PlayableGame.lighting`: - authored lighting present → sky renders dome + fog only; lights stay game-owned - no authored lighting → sky may emit its default sun/hemisphere with the dome Time-of-day never rewrites configured lights; it only drives sky colors/fog (and sky-owned lights when the game did not author lighting).
496
520
  - TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock, lights = true }: TimeOfDayDaylightProps): React.JSX.Element — Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock` are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so only dome colors and fog track the day fraction.
@@ -513,6 +537,14 @@ Imports use deep paths: `@jgengine/shell/<path>`.
513
537
  - MouseLookTracker (interface): interface MouseLookTracker — The analog mouse-look service chase/orbit-cam games hand-rolled (#282.8) — pointer-lock lifecycle plus delta accumulation into a yaw/pitch aim, decoupled from the first-person rig. Attach it to the canvas, read `aim()` from `onTick`/`useFrame`, dispose on unmount.
514
538
  - createMouseLookTracker (function): function createMouseLookTracker(element: HTMLElement, options: MouseLookOptions = {}): MouseLookTracker
515
539
 
540
+ ### @jgengine/shell/inputSink
541
+
542
+ - InputSink (interface): interface InputSink — Where the local player's per-frame input goes: discarded in single-player, sent to the authoritative host under `authority: "server"`.
543
+ - inputFramesEqual (function): function inputFramesEqual(a: InputFrame, b: InputFrame): boolean — Whether two input frames carry identical intent — the shell skips resending unchanged frames so a still player floods the host with nothing.
544
+ - noopInputSink (function): function noopInputSink(): InputSink — Discards input — the single-player / client-authoritative default, where the client integrates movement itself.
545
+ - remoteInputSink (function): function remoteInputSink(backend: Pick<LiveGameBackend, "transport">, serverId: string): InputSink — Sends each frame's input to the authoritative host over the transport, reusing the `runCommand` path via {@link INPUT_COMMAND}.
546
+ - resolveInputSink (function): function resolveInputSink(opts: { serverAuthoritative: boolean; backend: Pick<LiveGameBackend, "transport"> | null; serverId: string | null; }): InputSink — The sink a server-authoritative shell sends its per-frame input through: remote when `authority: "server"` and a server is joined, a no-op otherwise.
547
+
516
548
  ### @jgengine/shell/map/MapMarkerBeacons
517
549
 
518
550
  - 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`.
@@ -565,6 +597,14 @@ Imports use deep paths: `@jgengine/shell/<path>`.
565
597
  - PointerService (interface): interface PointerService
566
598
  - createPointerService (function): function createPointerService(): PointerService
567
599
 
600
+ ### @jgengine/shell/postfx/PostProcessing
601
+
602
+ - PostProcessing (function): function PostProcessing({ config }: { config: PostProcessingConfig }): null — Mounts an `EffectComposer` inside the shell Canvas and takes over rendering (priority-1 `useFrame`, which disables R3F auto-render) to run the configured post chain: RenderPass → GTAO → UnrealBloom → OutputPass → Grade. Rendered only when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
603
+
604
+ ### @jgengine/shell/postfx/gradeShader
605
+
606
+ - createGradePass (function): function createGradePass(config: GradeConfig = {}): ShaderPass — Build the display-space colour-grade pass (lift/gain/gamma, saturation, vignette, grain). Advance `uniforms.uTime.value` each frame to animate the grain.
607
+
568
608
  ### @jgengine/shell/registry
569
609
 
570
610
  - GameRegistry (type): type GameRegistry = Record<string, () => Promise<PlayableGame>>
@@ -674,7 +714,7 @@ Imports use deep paths: `@jgengine/shell/<path>`.
674
714
 
675
715
  ### @jgengine/shell/terrain
676
716
 
677
- - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element — Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
717
+ - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element — Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
678
718
  - CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
679
719
  - DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
680
720
  - EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
@@ -718,7 +758,7 @@ Imports use deep paths: `@jgengine/shell/<path>`.
718
758
 
719
759
  ### @jgengine/shell/terrain/CarvedTerrain
720
760
 
721
- - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element — Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
761
+ - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element — Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
722
762
  - CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
723
763
 
724
764
  ### @jgengine/shell/terrain/EditableGround
@@ -771,7 +811,7 @@ Imports use deep paths: `@jgengine/shell/<path>`.
771
811
 
772
812
  ### @jgengine/shell/terrain/index
773
813
 
774
- - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element — Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
814
+ - CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element — Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
775
815
  - CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
776
816
  - DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
777
817
  - EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
@@ -820,6 +860,11 @@ Imports use deep paths: `@jgengine/shell/<path>`.
820
860
  - hashNoise2 (function): function hashNoise2(x: number, z: number, seed: TerrainSeed = 1): number
821
861
  - seedToUint32 (function): function seedToUint32(seed: TerrainSeed = 1): number
822
862
 
863
+ ### @jgengine/shell/terrain/terrainDetailMaterial
864
+
865
+ - TerrainDetailMaterialHandle (interface): interface TerrainDetailMaterialHandle — The built procedural detail terrain material, ready to mount on the ground mesh.
866
+ - createTerrainDetailMaterial (function): function createTerrainDetailMaterial(detail: ResolvedTerrainDetail): TerrainDetailMaterialHandle — A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex colour as the base ground and blends procedural, 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, and fogged like any standard material, so it composes with the post-processing chain.
867
+
823
868
  ### @jgengine/shell/terrain/terrainMath
824
869
 
825
870
  - FieldGroundOptions (interface): interface FieldGroundOptions
@@ -1076,7 +1121,7 @@ Imports use deep paths: `@jgengine/shell/<path>`.
1076
1121
  - ProjectileTracers (function): function ProjectileTracers({ lifeMs = 130 }: { lifeMs?: number }): React.JSX.Element
1077
1122
  - Reticle (function): function Reticle({ className }: { className?: string }): React.JSX.Element
1078
1123
  - WorldBarSample (interface): interface WorldBarSample
1079
- - WorldEntityBars (function): function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, }: { statId: string; height?: number; roles?: readonly CatalogEntityRole[]; resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined; }): React.JSX.Element
1124
+ - WorldEntityBars (function): function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxDistance = 60, }: { statId: string; height?: number; roles?: readonly CatalogEntityRole[]; resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined; /** Hide bars for entities farther than this from the player (world…
1080
1125
  - WorldFloatText (function): function WorldFloatText({ height = 1.9, lifeMs = 950 }: { height?: number; lifeMs?: number }): React.JSX.Element
1081
1126
  - WorldTelegraphs (function): function WorldTelegraphs(): React.JSX.Element
1082
1127
  - collectWorldBarSamples (function): function collectWorldBarSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: { matrixWorldInverse: unknown; projectionMatrix: unknown }, viewport: { width:…
@@ -1111,6 +1156,10 @@ Imports use deep paths: `@jgengine/shell/<path>`.
1111
1156
  - collectWorldBarSamples (function): function collectWorldBarSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: { matrixWorldInverse: unknown; projectionMatrix: unknown }, viewport: { width:…
1112
1157
  - paintWorldBarSamples (function): function paintWorldBarSamples(canvas: { width: number; height: number; getContext(kind: "2d"): CanvasRenderingContext2D | null }, samples: readonly WorldBarSample[], dpr: number, barWidthPx = 112, barHeightPx = 10): void
1113
1158
 
1159
+ ### @jgengine/shell/worldSync
1160
+
1161
+ - attachWorldSync (function): function attachWorldSync(feeds: Pick<GameRuntimeFeeds, "subscribeServer">, serverId: string, ctx: Pick<GameContext, "hydrate">): () => void — Client half of host-authoritative play: subscribe to the server-state channel and mirror each authoritative `WorldSnapshot` (carried in `serverState`) into the local `ctx`, so the game renders the host's world instead of a locally-simulated one. Pure and transport-agnostic — the backend's `feeds.subscribeServer` is the only dependency; returns the unsubscribe. The shell attaches this (and gates its local sim) when the game's adapter opts into `authority: "server"`.
1162
+
1114
1163
  ## Guides
1115
1164
 
1116
1165
  # JGengine
@@ -1211,7 +1260,9 @@ Exact import paths and export names — **do not invent paths**; every row below
1211
1260
  | Loadout | `game/loadout` | `LoadoutDef`, `LoadoutItemEntry`, `Loadouts` |
1212
1261
  | Cosmetic loadout | `game/cosmetics` | `createCosmetics`, `Cosmetics`, `CosmeticLoadoutDef` |
1213
1262
  | Quest | `game/quest` | `QuestDef`, `QuestRewards`, `QuestObjective`, `QuestJournal` |
1214
- | World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building` |
1263
+ | World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building`, `road` |
1264
+ | Street layout | `world/streets` | `laneCenters`, `sidewalkPaths`, `furnitureSpots`, `parkingSpots`, `sidewalkPoint`, `offsetPath`, `sidewalkWidthOf`, `StreetLane`, `FurnitureSpot`, `ParkingSpot` — where things belong on a `road()`: lanes for traffic, sidewalks for peds, curb anchors for furniture |
1265
+ | Road ribbons | `world/roads` | `buildRoadRibbon`, `dashSegments`, `nearestOnPath`, `isOnRoad`, `pathLength`, `RoadRibbon`, `RoadSample`, `RoadPoint` — geometry + queries behind the `road()` environment feature |
1215
1266
  | Voxel field | `world/voxelField` | `createVoxelField`, `VoxelField`, `VoxelCell`, `VoxelHit`, `VoxelBounds`, `VoxelFieldSummary`, `VoxelFace`, `VOXEL_FACES`, `VOXEL_FACE_NORMALS` — a chunked block lattice, distinct from the `voxel()` `WorldFeature` descriptor |
1216
1267
  | Terrain field | `world/terrain` | `TerrainField`, `noiseField`, `resolveTerrainField`, `rollingField`, `fractalNoise`, `valueNoise`, `withNormal`, `arenaField`, `flatField`, `resolveGroundStep`, `snapToGround`, `snapEntityToGround`, `resolveTerrainPalette`, `TERRAIN_MATERIAL_PALETTES` |
1217
1268
  | Seeded RNG | `random/rng` | `seededRng`, `seededStreams` |
@@ -1503,7 +1554,7 @@ A voxel block is an object. A rack is an object with a slot inventory. A GPU is
1503
1554
 
1504
1555
  ## Game repo layout
1505
1556
 
1506
- Every game is one shape, enforced by `check-game-shape` (part of `check-types`): the top of `src/` holds only the skeleton, and every game-specific module, UI component, and test lives under `src/game/`. Dense files — one `catalog.ts` per domain, never one file per entry.
1557
+ 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/`. In this repo the gate also requires a root `package.json` script `"games:<id>": "bun run --cwd Games/<id> dev"` for every `Games/*` directory — add it with the scaffold, or the very first `check-types` fails before the compiler even runs. Dense files — one `catalog.ts` per domain, never one file per entry.
1507
1558
 
1508
1559
  ```
1509
1560
  src/
@@ -1534,6 +1585,8 @@ src/
1534
1585
 
1535
1586
  **Smart defaults** — omit any of these and the call still resolves: `multiplayer` → `offline()`; `assets` → an empty asset catalog; `loop` hooks (`onInit`/`onNewPlayer`/`onTick`) → no-ops; `content` → `{}`; `GameUI` → an empty component; `camera` → third-person orbit; `feed` → 20-entry ring buffers per action; a `world` of kind `environment()` auto-renders as the backdrop with no `environment` component supplied — a non-`environment()` world (`flat()`, `voxel()`, …) still needs the game to hand it one.
1536
1587
 
1588
+ **Opt-in `ctx.game.*` subsystems (`features`)** — core is genre-agnostic: the always-on base is `commands` / `events` / `store` / `feed` (plus `audio`), and genre subsystems are opt-in via `defineGame({ features: { roster, cards, turn, race, leaderboard, social, chat } })`. Omit one and `ctx.game.<name>` is `undefined` — a puzzle game isn't handed a card pile, race state, or party/chat it never asked for. Declare only what the game uses (`chat` implies `social`). (The content cluster — economy/quest/loot/trade — joins this manifest in a later slim-core phase.)
1589
+
1537
1590
  ```ts
1538
1591
  // game.config.ts — imports only, nothing inline
1539
1592
  import { defineGame } from "@jgengine/shell/defineGame";
@@ -1595,7 +1648,7 @@ export const physics: PhysicsConfig = { gravity: -32 };
1595
1648
 
1596
1649
  - `PhysicsConfig.gravity`/`jumpVelocity` tune the shell's built-in walk controller's fall/jump feel (defaults ~`-24`/`7.1`) — the only two levers on gravity and jump height; everything else about movement (speed, poses) stays catalog `movement` fields.
1597
1650
  - Input bindings are string arrays (hold semantics) or `{ hold, toggle, repeatMs? }` for the same verb. `repeatMs` turns a held action into an auto-repeat fire (build-mode place-on-drag, rapid-fire without a separate `wasPressed`/interval combo in game code) — the shell refires the command every `repeatMs` while the binding stays down, on top of its normal press-edge fire.
1598
- - **Keybind → command convention.** The shell fires a command for any bound action that isn't reserved: pressing an action runs a command of the **same name** if one is defined, else a `ui.<action>` fallback (so `openBackpack` → `ui.openBackpack`). Just declare the binding and a matching command — no per-game `keydown` listener. Reserved actions the shell consumes natively and never routes to a command: `moveForward/moveBack/moveLeft/moveRight`, `turnLeft/turnRight`, `sprint`, `jump`, `tabTarget`, `clearTarget`, `useAbility`, `interact`, and any `hotbarSlotN`/`slotN`. `tabTarget`/`clearTarget` run `target.cycle`/`target.clear` (native `cycleTarget`/`setTarget` fallback).
1651
+ - **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). Because `hotbarSlotN` never reaches a command, a game that drives selection from game code binds its own `selectSlot1..N` actions, defines matching commands that write a store key, and reads it back through `hotbarSelection: () => ...` on `defineGame` (see `loot-shooter`).
1599
1652
  - **`interact`** is special: pressing it resolves the active proximity prompt from the `prompts` field of `defineGame({...})` and runs that prompt's `invoke` command. A prompt with `invoke: null` is display-only and does nothing on the key.
1600
1653
  - UI keybind badges derive from `keybinds` via `actionLabel(keybinds, "openBackpack")` — `bindingLabel` maps codes to short labels (`Digit1`→`1`, `KeyB`→`B`, `mouse0`→`LMB`, `Escape`→`Esc`). Never hardcode label strings; they drift the moment a binding changes.
1601
1654
  - `server.mode` is a string your loop/commands interpret — the engine ships no gamemode presets.
@@ -1680,7 +1733,7 @@ Catalog-first, no per-game audio glue. The `audio` field of `defineGame({...})`
1680
1733
  The shell ships a **rig library**; a game picks and tunes one through `camera` config, never by writing camera positions from `onTick`. Select with `camera.rig` (or the `perspective: "third" | "first"` shorthand) — or by config block alone (#207.8): supplying `camera.<rig>` selects that rig with no redundant `rig` field, checked in the table's order; an explicit `rig` wins when several blocks are present:
1681
1734
  | `rig` | For | Key config (`camera.<rig>`) |
1682
1735
  |-------|-----|------------------------------|
1683
- | `orbit` (default) | Third-person chase | `initialDistance`, `targetHeight`, `min/maxPolarAngle`, drag/zoom |
1736
+ | `orbit` (default) | Third-person chase | Top-level fields on `camera` itself — `initialDistance`, `minDistance`/`maxDistance`, `targetHeight`, `min/maxPolarAngle`, drag/zoom. There is **no `camera.orbit` block**; orbit is the one rig tuned at the top level |
1684
1737
  | `first` | FPS mouse-look | `firstPerson: { eyeHeight, sensitivity, maxPitch, reticle, viewmodel }` |
1685
1738
  | `topDown` | ARPG iso / top-down (Diablo IV, Hades II) | `topDown: { height, pitch, yaw, followSmoothing, zoom }` — decoupled follow; `pitch` is camera elevation (PI/2 = straight down, near 0 = grazing and boom-distance blows up past `frustum.far`) |
1686
1739
  | `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 |
@@ -1739,7 +1792,7 @@ ctx.time advance, now, calendar, snapshot; pause, play, toggle,
1739
1792
  ctx.subscribe / ctx.version change signal — UI layers bind via useSyncExternalStore
1740
1793
  ```
1741
1794
 
1742
- `content.itemById(id)` supplies `{ use?, weapon?, trade? }`; `content.entityById(id)` supplies `{ stats?, receive?, onDeath?, movement?, role? }`; `content.objectById(id)` supplies `GameContextObjectEntry` `{ proximityPrompt?, breakable?, slotInventory? }`. Build all three from your catalogs in `content.ts`. A placed object resolves its catalog entry via `ctx.scene.object.catalog(instanceId)`; `ctx.scene.object.at(x, y, z, tolerance?)` finds placed objects near a point (grid interaction, click resolution beyond the pointer service). `ctx.scene.entity.update(id, patch)` writes a shallow patch onto a spawned entity's mutable fields (e.g. `movement.walkSpeed`) without a full respawn — `scene/movementSpeed`'s `applyStatDrivenSpeed(deps, id, { baseSpeed, multiplierStat?, flatBonusStat? })` is the catalog-driven helper that recomputes and writes `movement.walkSpeed` from a stat-modifier pair each time a buff changes.
1795
+ `content.itemById(id)` supplies `{ use?, weapon?, trade? }` — exact shapes: `use` is the handler **name string** (not an object), `weapon` is a flat `Record<string, number | Record<string, number>>` built from your catalog stats (`damage`, `projectile.{...}`, `explosion.{...}`), and every lookup returns **`null`** (not `undefined`) for an unknown id; `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`. Call-shape gotchas that cost typecheck loops: `ctx.item.use.use(input)` takes **one** argument on the ctx surface (the two-arg `use(state, input)` is the raw factory shape); `ctx.scene.entity.moveToward(id, target, { speed, dt, stopDistance? })` — the third argument is an options object, never a bare `dt`; `nav/pathFollow`'s `createPathFollow(config)` returns the initial `PathFollowState` directly and `advancePathFollow(config, state, dt)` returns the **next state** (with `position: [x,y,z]`, `heading`, `done`) — there is no wrapper object, and `Waypoint` is a `[x, y, z]` tuple. A placed object resolves its catalog entry via `ctx.scene.object.catalog(instanceId)`; `ctx.scene.object.at(x, y, z, tolerance?)` finds placed objects near a point (grid interaction, click resolution beyond the pointer service). `ctx.scene.entity.update(id, patch)` writes a shallow patch onto a spawned entity's mutable fields (e.g. `movement.walkSpeed`) without a full respawn — `scene/movementSpeed`'s `applyStatDrivenSpeed(deps, id, { baseSpeed, multiplierStat?, flatBonusStat? })` is the catalog-driven helper that recomputes and writes `movement.walkSpeed` from a stat-modifier pair each time a buff changes.
1743
1796
 
1744
1797
  ### Two tiers: `ctx` runtime vs pure factories
1745
1798
 
@@ -2067,6 +2120,37 @@ Design-resolution fit is on by default for every game: each `HudCanvas` auto-sca
2067
2120
 
2068
2121
  **Overflow is an error, not a style note.** `HudCanvas` measures every `HudPanel` against the viewport at runtime; offenders land in a `data-hud-overflow` attribute (and a console warning), and `bun run shoot <game> --device mobile` (or `both`) exits non-zero naming the escaping panels. A game is not mobile-done while shoot reports HUD OVERFLOW.
2069
2122
 
2123
+ ### Phase-gated HUD visibility (`showDuring`)
2124
+
2125
+ `HudCanvas` and `HudPanel` take an opt-in `showDuring?: GamePhase[]` — the element renders only while `gamePhase` is one of the listed phases (`"menu" | "playing" | "paused" | "ended"`), so a non-cartridge game hides its HUD under menu/end overlays without hand-rolling a phase check. Omit it for the default always-visible behavior. Gate the whole HUD with `<HudCanvas showDuring={["playing"]}>`, or keep a single results panel up with a per-`HudPanel` value. The read degrades to `"playing"` when the component renders outside a `GameProvider` (component showcases, previews), so it never throws there.
2126
+
2127
+ ### Shared viewport allocation (`GameViewportProvider`, region collision)
2128
+
2129
+ The shell allocates the live viewport once — visual viewport, safe-area insets, orientation, and layout mode — and hosts a **layout registry** every UI subsystem publishes its occupied rectangle to. This replaces "every subsystem independently claims an edge" (the mobile-overlap smell). It is wired automatically for every game; author against it, don't rebuild it.
2130
+
2131
+ - **The mode** is one of `desktop-wide | desktop-compact | mobile-landscape | mobile-portrait`, resolved from the live `visualViewport` + coarse-pointer + `platforms`. Read it with `useGameLayoutMode()` (or `useGameViewportLayout()` for the full geometry: `mode`, `orientation`, `safeArea`, `controlZones`, `gameplayRect`). Both degrade to a sane desktop default outside the provider, so previews never throw.
2132
+ - **Live CSS variables** are published on the provider root: `--jg-viewport-*`, `--jg-visual-viewport-*`, `--jg-safe-{top,right,bottom,left}`. Use them (with `100dvh` fallbacks) instead of assuming `100vh` equals the visible area on mobile Safari.
2133
+ - **Touch controls reserve real rectangles.** The engine measures the joystick / action-cluster / utility zones at runtime (ResizeObserver, not guessed percentages) and registers them as `control` regions. `HudCanvas` already lifts bottom-anchored panels above the dock via `--jg-hud-dock-clearance`; the registry additionally makes any residual overlap a hard error.
2134
+ - **Collision is detected, not hoped for.** A `HudPanel` inside a `HudCanvas` registers as a `hud` region automatically. `bun run shoot <game> --device mobile` / `mobile-landscape` reads a `data-jg-layout-collision` attribute and exits non-zero naming both colliding regions (e.g. `throttle ∩ radio (4270px²)`) — the same failure discipline as HUD overflow. In dev the colliding elements also carry `data-jg-collision` for outlining. Opt an intentional overlap out with `allowOverlapWith` / `collisionGroup`, or a soft `mobileBehavior="transient"`.
2135
+
2136
+ ### HUD priority + mobile behavior (`HudPanel`)
2137
+
2138
+ Declare intent, not just CSS. `HudPanel` accepts:
2139
+
2140
+ - `priority?: "critical" | "secondary" | "tertiary"` — the Tier from §2, surfaced to tooling. Critical stays visible; tertiary folds away first.
2141
+ - `mobileBehavior?: "persistent" | "compact" | "icon" | "transient" | "hidden" | "sheet" | "modal"` — `"hidden"` unmounts the panel on phones (move that Tier-3 readout behind a contextual action instead); `"transient"` softens its collision policy for a fleeting line; the rest tag the element (`data-hud-mobile-behavior`) for the game's own responsive CSS. Keep these game-authored and lightly styled — this is a placement contract, not a website design system.
2142
+
2143
+ The game still owns the genre-appropriate composition; the engine owns the geometry and the failure gate.
2144
+
2145
+ ### Mandatory orientation (`orientation`)
2146
+
2147
+ A game declares its phone-orientation contract on `defineGame({ orientation })`:
2148
+
2149
+ - Legacy `"landscape"` / `"portrait"` stays **advisory** — a dismissible rotate hint, never a gate.
2150
+ - The object form `{ mobile: <rule> }` is the strict contract, where `<rule>` is `any` (both) · `portrait` / `landscape` (advisory preference) · `portrait-required` / `landscape-required` (hard gate) · `unsupported` (no phone support).
2151
+
2152
+ For a driving/landscape game: `orientation: { mobile: "landscape-required" }`. When the device is held the wrong way the shell shows an **engine-owned `RotateDeviceScreen`** (polished, safe-area-aware, `visualViewport`-sized, reduced-motion-respecting, themeable via `--jg-*`) above every layer, **suppresses game input, freezes the simulation, and unmounts the HUD and touch controls** — gameplay never runs behind the gate. The gate is derived live from orientation, so rotating back to a valid orientation resumes automatically with no stuck-paused state. Never re-implement a "rotate for best experience" toast; declare the requirement and the engine owns the rest.
2153
+
2070
2154
  ## 5. Change the visual grammar
2071
2155
 
2072
2156
  Avoid making every element the same rounded translucent rectangle.
@@ -2114,6 +2198,8 @@ Prefer small headless or lightly styled primitives over a giant universal design
2114
2198
 
2115
2199
  A primitive should expose game-oriented choices such as shape, material, urgency, placement, hierarchy, entry motion, icon treatment, compactness, and diegetic-versus-overlay presentation.
2116
2200
 
2201
+ **Minimap** is already shipped, not hand-rolled: `Minimap` / `WorldMap` / `Compass` from `@jgengine/react/map` render a framed SVG minimap over a `MarkerSet` (`createMarkerSet`, `@jgengine/core/world/markers`) and optional `FogField`, projecting via `@jgengine/core/world/minimap` (`projectToMinimap`, `headingToBearing`, edge-clamp). A game feeds flat props (`markers`, `center: [x,z]`, `worldRadius`, `size`, `facingYaw`, `rotate`) and repopulates the marker set each HUD tick from live entities — no custom canvas painter. **Swing timer**: `swingTimerState(player, target, prevPeriod, prevTimer)` (`@jgengine/core/ui/swingTimer`) is the pure, parameter-in/next-state-out core for a melee swing bar (recovers period on the reset edge; hidden unless auto-attacking a live non-object target) — thread the returned `nextPeriod`/`nextTimer` back via refs, render a thin `Meter`.
2202
+
2117
2203
  Do not create a primitive whose only value is wrapping a `div` with border radius.
2118
2204
 
2119
2205
  ## 7. Complete interaction states
@@ -2145,6 +2231,19 @@ Do not communicate all states with background-color changes alone. Use appropria
2145
2231
 
2146
2232
  Focus must look authored while remaining accessible. Menus should support keyboard/controller-style focus navigation when practical.
2147
2233
 
2234
+ ## Rendering — post-processing, lighting, shadows
2235
+
2236
+ A cinematic look is opt-in engine config, never hand-wired render passes. Set `defineGame({ postProcessing })` (`PostProcessingConfig` from `@jgengine/core/render/postProcessing`) and the shell mounts an `EffectComposer` and owns the render: RenderPass → AO → Bloom → tone-map output → Grade. Absent means the renderer draws directly (unchanged), so it never imposes a look on games that don't ask. Each stage is a config object, `false` to skip, or omitted for its tuned default:
2237
+
2238
+ - `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `aces`) + `exposure`.
2239
+ - `bloom: { strength, radius, threshold }` — HDR glow around bright pixels (defaults 0.32 / 0.55 / 0.85).
2240
+ - `grade: { lift, gain, gamma, saturation, vignette, grain }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain.
2241
+ - `ao: { radius, intensity, distanceFalloff, blend }` — ground-truth ambient occlusion; heavier than the rest, omit or `false` on low-end targets.
2242
+
2243
+ **Orbit camera occlusion:** the third-person orbit rig takes `camera: { collision: { enabled, padding?, minTargetDistance? } }` — a spring-arm that raycasts target→camera each frame and pulls the boom in past walls/terrain so the camera never clips inside geometry. Off by default (unchanged chase feel); enable it for any world with interiors or dense structures.
2244
+
2245
+ Lighting is `defineGame({ lighting })` (`LightingConfig`): ambient / hemisphere / directional, replacing the shell's default lights when set. A `DirectionalLightingConfig` with `castShadow` takes `shadowMapSize`, `shadowCameraSize`, `shadowBias`, `shadowNormalBias` for crisp contact shadows. Sky-lit worlds (a `sky()` world feature) get a high-res sun whose shadow camera follows the view each frame, so grounded shadows stay sharp under the player anywhere in a large world.
2246
+
2148
2247
  ## Settings menu
2149
2248
 
2150
2249
  **Settings menu (themed, four layouts, no forced chrome).** The engine builds the whole menu for free — Sound (master + per-bus volume), Graphics (quality/dpr + shadows), Gameplay (FOV slider, default 40–120), Controls (per-action key rebinding, inline click-to-rebind, persisted) — from the game's `audio.buses` and `input` map. What it does **not** do is bolt a fixed gear onto every game: **there is no auto trigger.** You place the entry yourself so it lives *inline with your game's own UI*, never a stray corner overlay. Drop `<SettingsTrigger className=…>` (from `@jgengine/react`) anywhere in your HUD or menu — headless button, `className` for skin/placement, optional `children` to replace the default gear glyph, renders nothing when there's nothing to show. Or call `useSettings().open()` from your own control. Tune the menu via `defineGame({ settings })` (`GameSettingsConfig` from `@jgengine/core/settings/settingsModel`):
@@ -2210,6 +2309,11 @@ Requirements:
2210
2309
 
2211
2310
  Do not use one generic translucent controller across all games.
2212
2311
 
2312
+ **`presentation: "hud"` games get 3D parity.** A pure-HUD game (no camera rig) now reaches the same input/audio seams as a 3D game:
2313
+ - **Touch gestures** — the shell mounts a headless `TouchPlaySurface` in the hud branch too, so `touch.gestures` (swipe/tap) reach actions without the game hand-wiring pointer events on its own canvas. The visible dock stays game-authored per the rule above.
2314
+ - **No phantom reservations** — camera action names (`turnLeft`/`turnRight`/`interact`/…) are reserved *only* when a camera rig is active, so a hud game may bind them directly instead of renaming to `steer*`.
2315
+ - **Audio actually plays** — audio resumes on the first pointer gesture in hud games (not just 3D), and `playOneShot` self-resumes the suspended context. Trigger sound from anywhere holding `ctx` via `ctx.game.audio.play(soundId, at?)` / `ctx.game.audio.resume()` — the reachable seam over the shell's audio engine.
2316
+
2213
2317
  ## 10. Progressive instruction
2214
2318
 
2215
2319
  Do not leave large control grids visible during gameplay.
@@ -2463,6 +2567,18 @@ Shell wiring: `@jgengine/shell/vision/RevealVision` (`RevealHighlights` — dept
2463
2567
 
2464
2568
  Renderer-free world surface — query primitives, environment fields + weather + realm composition, survival meters/moodles, interactive building & terraform, the optional headless physics world, vehicles/mounts/racing, and spawn placement. Full surface: **[reference.md](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-world/reference.md)**.
2465
2569
 
2570
+ ## Level design — places, not noise
2571
+
2572
+ Procedural terrain is a canvas, never the level. A world with only `terrain()` + spawn clusters reads as an empty sandbox no matter how big its bounds. Every open-world game composes **places** on top of the canvas, and the checklist below is the floor, not the ceiling:
2573
+
2574
+ - **Every site is a set-piece.** A camp is a wall ring with a deliberate gate opening facing the approach, cover inside, a tower or banner as its silhouette — never a bare radius of spawns. Compose from placed objects (`ctx.scene.object.place` with stable instance ids) driven by pure data functions (ring/scatter/route generators) so bun tests can assert the composition without rendering.
2575
+ - **Roads make the space legible.** Physically connect sites: trace waypoint routes between them and feed the points into the terrain descriptor's `flatten` masks so the route is genuinely walkable, then dress it — markers, wrecks, signposts — every ~60 units so the player can read the path at a glance. A test should walk the route and assert the step height stays small.
2576
+ - **One landmark per region.** A water tower, crashed vehicle, gate, or monument visible from the approach gives orientation without a map. Distinct silhouette + distinct color.
2577
+ - **Anchor → approach → arena → reward.** Each combat site needs a visible anchor drawing the player in, a readable approach (the road), an arena with cover the AI and player both use, and a visible reward (chest, vendor, quest turn-in) at the back so clearing it feels earned.
2578
+ - **People, not just spawns.** Hubs hold named NPCs (talkable entities with prompts), vendors, and light props (lamps, crates, stalls). A hub with only vending-machine objects is a menu, not a town.
2579
+ - **Density budget.** Inside a site's flatten radius aim for roughly one placed object per 6–10 units of radius; along roads one prop per ~60 units; open wilderness stays sparse so sites contrast. Frustum/distance culling is automatic — err toward more dressing, not less.
2580
+ - **Guided openness, never invisible walls.** Total freedom reads as emptiness: herd the player physically. Raise terrain amplitude so off-road relief is genuinely unclimbable, gate slope in `movement.beforeCommit` (block a step whose ground rises past a climb limit, with per-axis slide so it feels like a wall of rock, not a script), and carve the intended route as the one low path. Then buy the openness back deliberately: side pockets (a den, a cache, a wreck field) hang off the main road on short spur routes, each with its own reward, so exploration branches from the path instead of dissolving it. Test the herding the same way you test the road: assert off-road samples rise above the climb limit while every route and spur stays under it.
2581
+
2466
2582
  ## Turn-based & tactics (renderer-free)
2467
2583
 
2468
2584
  # jgengine domain API — World features
@@ -2480,11 +2596,13 @@ Descriptors from `@jgengine/core/world/features` — config data the runner
2480
2596
  | `plots(config)` | Shared city + instanced interiors |
2481
2597
  | `tilemap({ map })` | 2D/2.5D levels |
2482
2598
  | `flat()` | Plain arena |
2483
- | `environment({ terrain, sky, weather, vegetation, water, structures, pads })` | Composable outdoor scene — terrain + sky/time-of-day + rain/snow + grass + ocean + buildings + ground pads. Each field takes the matching descriptor: `terrain()`, `sky()`, `rain()`/`snow()`, `grass()`, `ocean()`, `building()`, `pad()`. `building()` and `ocean()` take `position: [x, z]` to site a cluster/water body away from the origin (several settlements, an offset lake); building clusters ground-snap to the terrain field per building; each `pad()` (a flat platform/paved patch — `{ center, size: [w,d] | { radius }, height?, color?, rotationY? }`) implicitly flattens the terrain beneath it via a `TerrainFlattenMask`, so a building pad or spawn circle never fights the noise field underneath |
2599
+ | `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]` to site a cluster/water body/weather band/vegetation patch away from the origin (several settlements, an offset lake, per-biome rain, a distant meadow) — the weather/grass `position` is sugar for `area.position`, so a biome-banded world places each band by zone; building clusters ground-snap to the terrain field per building; each `pad()` (a flat platform/paved patch — `{ center, size: [w,d] | { radius }, height?, color?, rotationY? }`) implicitly flattens the terrain beneath it via a `TerrainFlattenMask`, so a building pad or spawn circle never fights the noise field underneath |
2484
2600
 
2485
2601
  `biomes`/`voxel`/`plots`/`tilemap` share a `WorldGridConfig` (`cells?: WorldGridCell[]`, `cellSize?`, `baseHeight?`, `defaultColor?`) — a `WorldGridCell` is `{ x, z, height?, color? }`, one extruded box per cell. `resolveGridInstances(config)` (`@jgengine/core/world/gridInstances`) is the pure cell→instance math (position, scale, color per cell); the shell renders the result as a single `THREE.InstancedMesh` **automatically whenever `PlayableGame.environment` is unset and `game.world` is one of these four grid kinds** — no manual render wiring for a cell-based world, same auto-render convention as `environment()` worlds.
2486
2602
 
2487
- `terrain()`'s `material` (a named palette — `"grass" | "sand" | "snow" | "rock" | "ash"`, resolved via `resolveTerrainPalette`/`TERRAIN_MATERIAL_PALETTES` in `world/terrain`) sets the default low/high/waterline colors; `colors: { low?, high?, waterline? }` overrides any of them field-by-field, and `segments` tunes the render mesh's subdivision. `flatten: TerrainFlattenMask[]` (`{ center, radius, height?, falloff? }`) carves explicit flat circles into the noise field independent of pads — building foundations, spawn circles, roads — blending back to the noise height over `falloff` (default `radius * 0.5`). `sky({ preset?, timeOfDay?, horizonColor?, zenithColor?, sunIntensity?, ambientIntensity?, fog? })` — `preset: "day" | "dusk" | "night"` (default `"day"`) is the static look; `timeOfDay: true` instead drives sun position, sky colors, and fog from the world clock's `calendar().dayFraction` every frame (`@jgengine/shell`'s `TimeOfDayDaylight` mounts this automatically for an `environment()` world with `sky` set — no per-game render wiring). **Gotcha:** `sunIntensity` / `ambientIntensity` are honored only on the **day** keyframe (`daylightCycle` builds dusk/night/dawn from fixed constants). Raising intensities under `preset: "dusk"` or `"night"` does nothing — use `preset: "day"` plus warm `horizonColor`/`zenithColor` when the first screenshot must be bright and readable (see `jgengine`'s first-shot art recipe).
2603
+ `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. `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). 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. **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).
2604
+
2605
+ **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/borderlands2/src/game/world/level.ts`.
2488
2606
 
2489
2607
  `parentSpace` positions are local to that space — convert at seams only.
2490
2608
 
@@ -2557,7 +2675,7 @@ Turn data-only placement into the build tooling of Valheim/Enshrouded/The Sims/F
2557
2675
  **Structural destruction (`physics/structure`).** `StructureGraph` models a building as nodes (pieces) + load-bearing edges with some nodes `anchor`ed (foundations). `damage(id, n)`/`damageEdge(a,b,n)`/`severEdge(a,b)` wear pieces and connections; when one breaks, the graph recomputes reachability to an anchor and returns a single `CollapseEvent { fell }` — every piece the loss disconnected. `toDebris(world, event)` sinks the fallen pieces into a `PhysicsWorld` as rigid bodies (The Finals, Rainbow Six). It is coarse by design: **replicate the collapse event (the `fell` id list), not each fragment's physics** — game clients re-derive the debris locally. Piece integrity and edge strength default from a `StructureMaterial` table (DATA).
2558
2676
  ### Vehicles, mounts, crash damage & racing
2559
2677
  Five primitives layer a driving/racing game over the physics sim and `world/water`. All are **data-first** (spec the chassis/wheels/grip curve, damage thresholds, and checkpoint layout as catalog data) and pure `@jgengine/core`; renderers live in the game/shell. Each `update(dt, …)` runs **before** the shared `world.step(dt)`.
2560
- - **Analog input — `input/axisInput`.** `AxisInput { throttle, brake, steer, handbrake }` is a continuous channel, **distinct from the digital action bindings**. `new AxisChannel({ bindings, smoothing })` ramps held keys into pedal-like analog values (`sample(dt, isDown)`), or `setAnalog(axis, value)` drives it straight from a gamepad axis. `DRIVE_AXIS_BINDINGS` is a ready WASD/arrow map.
2678
+ - **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).
2561
2679
  - **`physics/vehicleBody`.** `createVehicleBody(world, config)` is an arcade car: a chassis box body with per-wheel suspension held by G3's `springJoint` against the sampled `groundHeight`, drive/brake along the heading, and a `GripCurve` (`sampleGripCurve`) that bleeds lateral velocity for cornering — and, under `handbrake`, drift. `update(dt, axisInput)` then `world.step`. Because the chassis is a real body it still collides, which feeds crash damage. Rocket League, Trackmania, Wreckfest.
2562
2680
  - **`physics/buoyancy`.** `createBuoyantBody(world, { body, water, … })` floats a body on a CPU `WaterSurface` (Archimedes per hull point + water drag) so it settles at the waterline and rides the Gerstner waves; pass an `AxisInput` to `update(dt, time, input?)` and it drives as a boat (thrust + yaw + keel). Sea of Thieves, BOTW rafts.
2563
2681
  - **`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.
@@ -2640,4 +2758,4 @@ import { bootCartridge, cartridgeSmokeTest, tickCartridge } from "@jgengine/core
2640
2758
  cartridgeSmokeTest(config); // validate + world summary + headless run/spawn/kill/gem smoke
2641
2759
  ```
2642
2760
 
2643
- `bootCartridge`/`tickCartridge` build a headless `GameContext` and drive the loop (auto-choosing drafts) for custom assertions — see `Games/swarm-survivor/src/game/cartridge.test.ts`. The testkit imports `bun:test`; import it from test files only.
2761
+ `bootCartridge`/`tickCartridge` build a headless `GameContext` and drive the loop (auto-choosing drafts) for custom assertions — see `@jgengine/core/cartridge/testkit`. The testkit imports `bun:test`; import it from test files only.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/shell",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -35,9 +35,9 @@
35
35
  "test": "bun test src"
36
36
  },
37
37
  "dependencies": {
38
- "@jgengine/core": "^0.9.0",
39
- "@jgengine/react": "^0.9.0",
40
- "@jgengine/ws": "^0.9.0"
38
+ "@jgengine/core": "^0.10.0",
39
+ "@jgengine/react": "^0.10.0",
40
+ "@jgengine/ws": "^0.10.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@react-three/drei": "^10.0.0",