@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.
- package/CHANGELOG.md +21 -1
- package/dist/GamePlayerShell.d.ts +6 -21
- package/dist/GamePlayerShell.js +419 -234
- package/dist/audio/audioEngine.d.ts +8 -0
- package/dist/audio/audioEngine.js +33 -0
- package/dist/audio/musicDirector.d.ts +31 -0
- package/dist/audio/musicDirector.js +125 -0
- package/dist/audio/musicVoices.d.ts +10 -0
- package/dist/audio/musicVoices.js +338 -0
- package/dist/audio/synthEngine.d.ts +9 -0
- package/dist/audio/synthEngine.js +57 -0
- package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
- package/dist/camera/GameFirstPersonCamera.js +21 -4
- package/dist/camera/GameInspectionCamera.js +12 -1
- package/dist/camera/GameOrbitCamera.js +35 -1
- package/dist/camera/orbitCameraMath.d.ts +18 -0
- package/dist/camera/orbitCameraMath.js +6 -1
- package/dist/cartridge.js +1 -0
- package/dist/commandSink.d.ts +20 -0
- package/dist/commandSink.js +27 -0
- package/dist/defineGame.js +3 -1
- package/dist/devtools/CollisionDebugWorld.js +1 -0
- package/dist/devtools/collisionDebugMath.d.ts +1 -0
- package/dist/devtools/collisionDebugMath.js +2 -1
- package/dist/environment/Daylight.d.ts +7 -1
- package/dist/environment/Daylight.js +52 -5
- package/dist/environment/EnvironmentScene.js +14 -5
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/inputSink.d.ts +18 -0
- package/dist/inputSink.js +35 -0
- package/dist/multiplayer.js +7 -3
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +82 -0
- package/dist/postfx/gradeShader.d.ts +4 -0
- package/dist/postfx/gradeShader.js +64 -0
- package/dist/terrain/CarvedTerrain.d.ts +4 -1
- package/dist/terrain/CarvedTerrain.js +4 -3
- package/dist/terrain/terrainDetailMaterial.d.ts +14 -0
- package/dist/terrain/terrainDetailMaterial.js +90 -0
- package/dist/touch/TouchControlsOverlay.js +11 -1
- package/dist/world/WorldHud.d.ts +3 -1
- package/dist/world/WorldHud.js +7 -7
- package/dist/world/worldBarSamples.d.ts +1 -1
- package/dist/world/worldBarSamples.js +7 -1
- package/dist/worldSync.d.ts +10 -0
- package/dist/worldSync.js +19 -0
- package/llms.txt +140 -22
- 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.
|
|
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
|
-
-
|
|
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;
|
|
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? }
|
|
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 `
|
|
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
|
|
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.
|
|
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.
|
|
39
|
-
"@jgengine/react": "^0.
|
|
40
|
-
"@jgengine/ws": "^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",
|