@jgengine/shell 0.8.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 +36 -0
- package/README.md +11 -0
- package/dist/GamePlayerShell.d.ts +10 -22
- package/dist/GamePlayerShell.js +709 -264
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +9 -0
- package/dist/audio/audioEngine.js +37 -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/behaviour.d.ts +5 -24
- package/dist/behaviour.js +14 -50
- package/dist/behaviourAttach.d.ts +9 -0
- package/dist/behaviourAttach.js +32 -0
- package/dist/behaviourDriver.d.ts +7 -0
- package/dist/behaviourDriver.js +21 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
- package/dist/camera/GameFirstPersonCamera.js +35 -4
- package/dist/camera/GameInspectionCamera.js +12 -1
- package/dist/camera/GameOrbitCamera.js +44 -1
- package/dist/camera/PlayerFov.d.ts +18 -0
- package/dist/camera/PlayerFov.js +48 -0
- package/dist/camera/cameraBlendMath.d.ts +13 -0
- package/dist/camera/cameraBlendMath.js +36 -0
- package/dist/camera/cameraRigs.d.ts +3 -0
- package/dist/camera/cameraRigs.js +36 -33
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +3 -0
- package/dist/camera/index.js +3 -0
- package/dist/camera/orbitCameraMath.d.ts +18 -0
- package/dist/camera/orbitCameraMath.js +6 -1
- package/dist/camera/rigMath.d.ts +10 -0
- package/dist/camera/rigMath.js +36 -1
- package/dist/camera/shakeChannel.d.ts +3 -17
- package/dist/camera/shakeChannel.js +2 -21
- package/dist/camera/shakeChannelMath.d.ts +8 -0
- package/dist/camera/shakeChannelMath.js +21 -0
- package/dist/cartridge.d.ts +145 -0
- package/dist/cartridge.js +246 -0
- package/dist/commandSink.d.ts +20 -0
- package/dist/commandSink.js +27 -0
- package/dist/defineGame.js +4 -1
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +181 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
- package/dist/devtools/DevtoolsOverlay.js +383 -41
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +103 -0
- package/dist/devtools/collisionDebugMath.js +129 -0
- package/dist/environment/Daylight.d.ts +20 -8
- package/dist/environment/Daylight.js +63 -15
- package/dist/environment/EnvironmentScene.js +91 -35
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/environment/groundPadMath.d.ts +2 -2
- package/dist/environment/groundPadMath.js +1 -1
- package/dist/environment/index.d.ts +2 -1
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +10 -0
- package/dist/environment/skyLightingPolicy.js +6 -0
- package/dist/input/mouseLook.d.ts +27 -0
- package/dist/input/mouseLook.js +35 -0
- package/dist/inputSink.d.ts +18 -0
- package/dist/inputSink.js +35 -0
- package/dist/materialOverride.d.ts +4 -6
- package/dist/materialOverride.js +12 -16
- package/dist/multiplayer.js +7 -3
- package/dist/pointer/PointerProbe.js +6 -2
- package/dist/pointer/pointerService.d.ts +3 -0
- package/dist/pointer/pointerService.js +6 -0
- 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/render/modelRender.d.ts +10 -1
- package/dist/render/modelRender.js +32 -5
- package/dist/render/resolveModel.d.ts +14 -0
- package/dist/render/resolveModel.js +24 -0
- package/dist/settings/QuickControls.d.ts +4 -0
- package/dist/settings/QuickControls.js +42 -0
- package/dist/settings/SettingsChrome.d.ts +1 -0
- package/dist/settings/SettingsChrome.js +10 -0
- package/dist/settings/SettingsMenu.d.ts +6 -0
- package/dist/settings/SettingsMenu.js +148 -0
- package/dist/settings/SettingsRuntime.d.ts +11 -0
- package/dist/settings/SettingsRuntime.js +19 -0
- package/dist/settings/appliedSettings.d.ts +14 -0
- package/dist/settings/appliedSettings.js +27 -0
- package/dist/settings/settingsController.d.ts +20 -0
- package/dist/settings/settingsController.js +165 -0
- package/dist/structures/GeneratedBuilding.d.ts +10 -0
- package/dist/structures/GeneratedBuilding.js +96 -8
- package/dist/structures/index.d.ts +1 -1
- package/dist/structures/index.js +1 -1
- package/dist/terrain/CarvedTerrain.d.ts +5 -1
- package/dist/terrain/CarvedTerrain.js +6 -5
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +4 -2
- package/dist/terrain/grassBudget.d.ts +3 -0
- package/dist/terrain/grassBudget.js +6 -0
- package/dist/terrain/grassGeometry.js +1 -1
- package/dist/terrain/terrainDetailMaterial.d.ts +14 -0
- package/dist/terrain/terrainDetailMaterial.js +90 -0
- package/dist/terrain/terrainMath.d.ts +8 -0
- package/dist/terrain/terrainMath.js +9 -0
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +17 -1
- package/dist/touch/TouchControlsOverlay.js +125 -9
- package/dist/visibility/CullingProvider.d.ts +21 -0
- package/dist/visibility/CullingProvider.js +134 -0
- package/dist/vision/FrustumSensorHud.d.ts +1 -6
- package/dist/vision/FrustumSensorHud.js +25 -27
- package/dist/vision/frustumSampleEqual.d.ts +2 -0
- package/dist/vision/frustumSampleEqual.js +10 -0
- package/dist/water/Ocean.js +1 -1
- package/dist/water/OceanConfig.d.ts +13 -0
- package/dist/water/OceanConfig.js +25 -17
- package/dist/water/index.d.ts +1 -1
- package/dist/water/index.js +1 -1
- package/dist/weather/FireSpreadLayer.js +7 -2
- package/dist/weather/RainField.d.ts +3 -1
- package/dist/weather/RainField.js +4 -4
- package/dist/weather/SnowField.d.ts +3 -1
- package/dist/weather/SnowField.js +4 -4
- package/dist/weather/fireSpreadPose.d.ts +2 -0
- package/dist/weather/fireSpreadPose.js +4 -0
- package/dist/weather/weatherMath.d.ts +5 -1
- package/dist/weather/weatherMath.js +7 -2
- package/dist/world/DataObjects.d.ts +1 -1
- package/dist/world/DataObjects.js +3 -1
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +6 -1
- package/dist/world/WorldHud.js +95 -48
- package/dist/world/entityPose.d.ts +14 -0
- package/dist/world/entityPose.js +10 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +4 -0
- package/dist/world/worldBarSamples.d.ts +30 -0
- package/dist/world/worldBarSamples.js +57 -0
- package/dist/worldSync.d.ts +10 -0
- package/dist/worldSync.js +19 -0
- package/llms.txt +1522 -1143
- 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
|
|
@@ -9,6 +9,30 @@ Imports use deep paths: `@jgengine/shell/<path>`.
|
|
|
9
9
|
|
|
10
10
|
## Exported surface
|
|
11
11
|
|
|
12
|
+
### @jgengine/shell/GameHost
|
|
13
|
+
|
|
14
|
+
- GameHost (function): function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer }: GameHostProps): React.JSX.Element
|
|
15
|
+
- GameHostProps (interface): interface GameHostProps
|
|
16
|
+
|
|
17
|
+
### @jgengine/shell/GamePlayer
|
|
18
|
+
|
|
19
|
+
- GamePlayer (function): function GamePlayer({ gameId, registry, fallbackGameId, loading = null, multiplayer = null }: GamePlayerProps): React.JSX.Element
|
|
20
|
+
- GamePlayerProps (type): type GamePlayerProps = { gameId: string; registry: GameRegistry; fallbackGameId?: string; loading?: ReactNode; multiplayer?: ShellMultiplayer | null; }
|
|
21
|
+
|
|
22
|
+
### @jgengine/shell/GamePlayerShell
|
|
23
|
+
|
|
24
|
+
- GamePlayerShell (function): function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }: { playable: PlayableGame; multiplayer?: ShellMultiplayer | null; poster?: boolean; /** Called once per boot after onInit/onNewPlayer with the live GameContext — a staging seam for screenshots,…
|
|
25
|
+
- 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`).
|
|
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.
|
|
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).
|
|
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`).
|
|
29
|
+
|
|
30
|
+
### @jgengine/shell/GameUiPreview
|
|
31
|
+
|
|
32
|
+
- GameUiPreview (function): function GameUiPreview({ playable, scenario = defaultUiScenario, }: { playable: PlayableGame; scenario?: UiPreviewScenario; }): React.JSX.Element
|
|
33
|
+
- UiPreviewScenario (type): type UiPreviewScenario = (ctx: GameContext, playable: PlayableGame) => void
|
|
34
|
+
- defaultUiScenario (const): const defaultUiScenario: UiPreviewScenario
|
|
35
|
+
|
|
12
36
|
### @jgengine/shell/audio/AudioComponents
|
|
13
37
|
|
|
14
38
|
- AudioListener (function): function AudioListener({ engine }: { engine: AudioEngine }): null
|
|
@@ -17,295 +41,434 @@ Imports use deep paths: `@jgengine/shell/<path>`.
|
|
|
17
41
|
|
|
18
42
|
### @jgengine/shell/audio/audioEngine
|
|
19
43
|
|
|
20
|
-
- createAudioEngine (function): function createAudioEngine(config: AudioSceneConfig = {}): AudioEngine
|
|
21
|
-
- Vec3 (interface): interface Vec3
|
|
22
|
-
- AudioSceneConfig (interface): interface AudioSceneConfig
|
|
23
44
|
- AudioEmitterHandle (interface): interface AudioEmitterHandle
|
|
24
45
|
- AudioEngine (interface): interface AudioEngine
|
|
46
|
+
- AudioSceneConfig (interface): interface AudioSceneConfig
|
|
47
|
+
- Vec3 (interface): interface Vec3
|
|
48
|
+
- createAudioEngine (function): function createAudioEngine(config: AudioSceneConfig = {}): AudioEngine
|
|
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}.
|
|
25
63
|
|
|
26
64
|
### @jgengine/shell/behaviour
|
|
27
65
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
66
|
+
- Object3DBehaviour (class): class Object3DBehaviour extends Behaviour
|
|
67
|
+
- attachObject3D (function): function attachObject3D<T extends Object3DBehaviour>(world: BehaviourWorld, object: Object3D, behaviour: T, nodeId: string = object.uuid): T
|
|
68
|
+
- createBehaviourWorldDriver (function): function createBehaviourWorldDriver(world: BehaviourWorld): { start(): void; stop(): void; isRunning(): boolean; step(dt: number): void; }
|
|
69
|
+
- useBehaviourWorld (function): function useBehaviourWorld(world: BehaviourWorld, scaleDt?: (rawDt: number) => number): void — Bootstraps `world` on mount and dispatches `world.update` every frame at simulation priority. Stops driving updates on unmount so remount does not double-step. `scaleDt` maps the raw frame delta to the dt behaviours receive — pass the game clock's scaling to keep behaviours on game time, or omit for real-time seconds. Games driving `world.update` themselves from `loop.onTick` should not also render this.
|
|
31
70
|
|
|
32
|
-
### @jgengine/shell/
|
|
71
|
+
### @jgengine/shell/behaviourAttach
|
|
33
72
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
- ObserverRig (function): function ObserverRig(props: RigProps): null — Detached spectator/photo cam (#120): binds to any entity or fixed point and auto-orbits it, reading no player input at all — the van CCTV / photo-mode / kill-cam rig. Distinct from every other rig, which drives from mouse/keys.
|
|
41
|
-
- CinematicRig (function): function CinematicRig(props: RigProps): null
|
|
42
|
-
- CAMERA_RIG_FRAME_PRIORITY (const): const CAMERA_RIG_FRAME_PRIORITY: -1
|
|
43
|
-
- CAMERA_POST_FRAME_PRIORITY (const): const CAMERA_POST_FRAME_PRIORITY: number
|
|
44
|
-
- RigProps (interface): interface RigProps
|
|
73
|
+
- Object3DBehaviour (class): class Object3DBehaviour extends Behaviour
|
|
74
|
+
- attachObject3D (function): function attachObject3D<T extends Object3DBehaviour>(world: BehaviourWorld, object: Object3D, behaviour: T, nodeId: string = object.uuid): T
|
|
75
|
+
|
|
76
|
+
### @jgengine/shell/behaviourDriver
|
|
77
|
+
|
|
78
|
+
- createBehaviourWorldDriver (function): function createBehaviourWorldDriver(world: BehaviourWorld): { start(): void; stop(): void; isRunning(): boolean; step(dt: number): void; }
|
|
45
79
|
|
|
46
80
|
### @jgengine/shell/camera/GameCameraRig
|
|
47
81
|
|
|
48
82
|
- GameCameraRig (function): function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): React.JSX.Element
|
|
49
|
-
- resolveRigKind (function): function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind — Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least specific: an explicit `rig` field always wins; then `perspective: "first"` (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence of a rig's own config block selects that rig, checked in the fixed order below (#207.8) so a config carrying more than one block resolves deterministically instead of depending on object key order. Set `rig` explicitly to break a tie.
|
|
50
83
|
- GameCameraRigProps (interface): interface GameCameraRigProps
|
|
84
|
+
- resolveRigKind (function): function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind — Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least specific: an explicit `rig` field always wins; then `perspective: "first"` (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence of a rig's own config block selects that rig, checked in the fixed order below (#207.8) so a config carrying more than one block resolves deterministically instead of depending on object key order. Set `rig` explicitly to break a tie.
|
|
51
85
|
|
|
52
86
|
### @jgengine/shell/camera/GameFirstPersonCamera
|
|
53
87
|
|
|
54
88
|
- GameFirstPersonCamera (function): function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): React.JSX.Element | null
|
|
55
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.
|
|
56
91
|
|
|
57
92
|
### @jgengine/shell/camera/GameInspectionCamera
|
|
58
93
|
|
|
59
|
-
- GameInspectionCamera (function): function GameInspectionCamera({ config: configPatch }: GameInspectionCameraProps): React.JSX.Element
|
|
94
|
+
- GameInspectionCamera (function): function GameInspectionCamera({ config: configPatch }: GameInspectionCameraProps): React.JSX.Element — Model-viewer style rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state.
|
|
60
95
|
- GameInspectionCameraProps (interface): interface GameInspectionCameraProps
|
|
61
96
|
|
|
62
97
|
### @jgengine/shell/camera/GameOrbitCamera
|
|
63
98
|
|
|
64
|
-
- GameOrbitCamera (function): function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls = false, }: GameOrbitCameraProps): React.JSX.Element
|
|
65
|
-
- seedOrbitCameraTarget (function): function seedOrbitCameraTarget(camera: Camera, target: Vector3, distance: number, height: number): void — Seed orbit target before controls mount (demo spawn at origin).
|
|
66
99
|
- CameraFollowListener (type): type CameraFollowListener = (state: CameraFollowState) => void
|
|
100
|
+
- GameOrbitCamera (function): function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls = false, }: GameOrbitCameraProps): React.JSX.Element
|
|
67
101
|
- GameOrbitCameraProps (interface): interface GameOrbitCameraProps
|
|
102
|
+
- seedOrbitCameraTarget (function): function seedOrbitCameraTarget(camera: Camera, target: Vector3, distance: number, height: number): void — Seed orbit target before controls mount (demo spawn at origin).
|
|
68
103
|
|
|
69
|
-
### @jgengine/shell/camera/
|
|
104
|
+
### @jgengine/shell/camera/PlayerFov
|
|
105
|
+
|
|
106
|
+
- PlayerFovProvider (function): function PlayerFovProvider({ config, orthographic, children, }: { config?: GameCameraConfig; orthographic: boolean; children: ReactNode; }): React.JSX.Element
|
|
107
|
+
- PlayerFovSlider (function): function PlayerFovSlider(): React.JSX.Element | null
|
|
108
|
+
- PlayerFovState (interface): interface PlayerFovState
|
|
109
|
+
- usePlayerFov (function): function usePlayerFov(): PlayerFovState
|
|
110
|
+
|
|
111
|
+
### @jgengine/shell/camera/cameraBlendMath
|
|
112
|
+
|
|
113
|
+
- CameraBlendScratch (interface): interface CameraBlendScratch
|
|
114
|
+
- applyCameraBlendStep (function): function applyCameraBlendStep(scratch: CameraBlendScratch, camera: Camera, targetFov: number, dt: number): boolean
|
|
115
|
+
- captureCameraBlendFrom (function): function captureCameraBlendFrom(scratch: CameraBlendScratch, position: Vector3, quaternion: Quaternion, fov: number, duration: number): void
|
|
116
|
+
- createCameraBlendScratch (function): function createCameraBlendScratch(Vector3Ctor: new () => Vector3, QuaternionCtor: new () => Quaternion): CameraBlendScratch
|
|
117
|
+
|
|
118
|
+
### @jgengine/shell/camera/cameraRigs
|
|
70
119
|
|
|
71
|
-
- GameOrbitCamera (function): function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls = false, }: GameOrbitCameraProps): React.JSX.Element
|
|
72
|
-
- CameraFollowListener (type): type CameraFollowListener = (state: CameraFollowState) => void
|
|
73
|
-
- GameOrbitCameraProps (interface): interface GameOrbitCameraProps
|
|
74
|
-
- GameFirstPersonCamera (function): function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): React.JSX.Element | null
|
|
75
|
-
- GameFirstPersonCameraProps (interface): interface GameFirstPersonCameraProps
|
|
76
|
-
- GameInspectionCamera (function): function GameInspectionCamera({ config: configPatch }: GameInspectionCameraProps): React.JSX.Element — Model-viewer style rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state.
|
|
77
|
-
- GameInspectionCameraProps (interface): interface GameInspectionCameraProps
|
|
78
|
-
- GameCameraRig (function): function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): React.JSX.Element
|
|
79
|
-
- resolveRigKind (function): function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind — Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least specific: an explicit `rig` field always wins; then `perspective: "first"` (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence of a rig's own config block selects that rig, checked in the fixed order below (#207.8) so a config carrying more than one block resolves deterministically instead of depending on object key order. Set `rig` explicitly to break a tie.
|
|
80
|
-
- GameCameraRigProps (interface): interface GameCameraRigProps
|
|
81
120
|
- CAMERA_POST_FRAME_PRIORITY (const): const CAMERA_POST_FRAME_PRIORITY: number
|
|
82
121
|
- CAMERA_RIG_FRAME_PRIORITY (const): const CAMERA_RIG_FRAME_PRIORITY: -1
|
|
122
|
+
- CameraBlendScratch (interface): interface CameraBlendScratch
|
|
83
123
|
- ChaseRig (function): function ChaseRig(props: RigProps): null
|
|
84
124
|
- CinematicRig (function): function CinematicRig(props: RigProps): null
|
|
85
125
|
- LockOnRig (function): function LockOnRig(props: RigProps): null
|
|
86
|
-
- ObserverRig (function): function ObserverRig(props: RigProps): null
|
|
126
|
+
- ObserverRig (function): function ObserverRig(props: RigProps): null — Detached spectator/photo cam (#120): binds to any entity or fixed point and auto-orbits it, reading no player input at all — the van CCTV / photo-mode / kill-cam rig. Distinct from every other rig, which drives from mouse/keys.
|
|
127
|
+
- RigProps (interface): interface RigProps
|
|
87
128
|
- RtsRig (function): function RtsRig(props: RigProps & { panKeysEnabled?: boolean }): null
|
|
88
129
|
- ShoulderRig (function): function ShoulderRig(props: RigProps): null
|
|
89
|
-
- SideScrollRig (function): function SideScrollRig(props: RigProps): null
|
|
130
|
+
- SideScrollRig (function): function SideScrollRig(props: RigProps): null — Fixed side-on 2.5D follow rig: watches the followed entity from the perpendicular axis, never reading WASD/mouse-look.
|
|
90
131
|
- TopDownRig (function): function TopDownRig(props: RigProps): null
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
- resolveSideScroll (function): function resolveSideScroll(config: SideScrollCameraConfig | undefined): ResolvedSideScroll
|
|
116
|
-
- resolveSideScrollPose (function): function resolveSideScrollPose(entityPos: Vec3, resolved: ResolvedSideScroll, fov: number): CameraPose — Fixed lateral 2.5D follow pose: the camera sits perpendicular to the travel axis at `distance`, above the entity by `height`, and looks at the entity raised by `lookHeight`. Axis "x" watches from +z; axis "z" watches from +x.
|
|
117
|
-
- resolveTopDown (function): function resolveTopDown(config: TopDownCameraConfig | undefined): ResolvedTopDown
|
|
118
|
-
- rightVector (function): function rightVector(yaw: number): Vec3
|
|
119
|
-
- rtsPanKeysConflict (function): function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean
|
|
120
|
-
- seatPose (function): function seatPose(follow: Vec3, yaw: number, local: { x?: number; y?: number; z?: number }, fov: number): CameraPose — World-space seat pose (cockpit/hood/rear) rigidly attached to the vehicle.
|
|
121
|
-
- shakeOffset (function): function shakeOffset(state: TraumaState, config: { maxOffset?: number; maxRoll?: number; exponent?: number; frequency?: number; } | undefined): ShakeOffset — Positional + roll shake for the current trauma. Shake magnitude is `trauma^exponent` so small hits stay subtle; the oscillation is deterministic per-seed pseudo-noise (no per-frame RNG, so identical inputs reproduce).
|
|
122
|
-
- shoulderPose (function): function shoulderPose(follow: Vec3, yaw: number, pitch: number, sideSign: number, shoulder: ResolvedShoulder): CameraPose — Over-the-shoulder camera pose. The boom sits behind the follow point along `yaw`, raised by `heightOffset`, and pushed sideways by `shoulderOffset` (sign set by `sideSign`: +1 right, -1 left). `pitch` tilts the look point.
|
|
123
|
-
- sideScrollFollowBlend (function): function sideScrollFollowBlend(followSmoothing: number, dt: number): number — Frame-rate independent follow blend for the side-scroll rig; `followSmoothing <= 0` hard-locks (blend 1) instead of freezing.
|
|
124
|
-
- smoothstep (function): function smoothstep(t: number): number
|
|
125
|
-
- smoothYaw (function): function smoothYaw(current: number, desired: number, speed: number, dt: number): number — Exponential yaw smoothing that respects wrap-around.
|
|
126
|
-
- speedToFov (function): function speedToFov(speed: number, curve: { base?: number; max?: number; speedForMax?: number } | undefined): number — Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`.
|
|
127
|
-
- springArmStep (function): function springArmStep(current: Vec3, desired: Vec3, damping: number, dt: number): Vec3 — Exponential spring-arm approach toward a desired point (frame-rate independent).
|
|
128
|
-
- stepTrauma (function): function stepTrauma(state: TraumaState, decayPerSecond: number, dt: number): void — Decay trauma linearly toward 0 and advance the shake clock.
|
|
129
|
-
- topDownPose (function): function topDownPose(follow: Vec3, resolved: ResolvedTopDown, fov: number): CameraPose — Camera pose for a fixed top-down / isometric rig. `pitch` is the elevation of the camera→target ray above the ground (PI/2 = straight down); `yaw` is the fixed azimuth (PI/4 reads isometric). Height sets zoom; horizontal boom is derived so the look angle stays constant as height changes.
|
|
130
|
-
- yawTo (function): function yawTo(from: Vec3, to: Vec3): number — Yaw that points from `from` toward `to` on the XZ plane (matches shell forward = (sin, cos)).
|
|
132
|
+
- applyCameraBlendStep (function): function applyCameraBlendStep(scratch: CameraBlendScratch, camera: Camera, targetFov: number, dt: number): boolean
|
|
133
|
+
- captureCameraBlendFrom (function): function captureCameraBlendFrom(scratch: CameraBlendScratch, position: Vector3, quaternion: Quaternion, fov: number, duration: number): void
|
|
134
|
+
- createCameraBlendScratch (function): function createCameraBlendScratch(Vector3Ctor: new () => Vector3, QuaternionCtor: new () => Quaternion): CameraBlendScratch
|
|
135
|
+
|
|
136
|
+
### @jgengine/shell/camera/fovPreference
|
|
137
|
+
|
|
138
|
+
- PLAYER_FOV_DEFAULT (const): const PLAYER_FOV_DEFAULT: any
|
|
139
|
+
- PLAYER_FOV_MAX (const): const PLAYER_FOV_MAX: 120
|
|
140
|
+
- PLAYER_FOV_MIN (const): const PLAYER_FOV_MIN: 40
|
|
141
|
+
- PLAYER_FOV_STORAGE_KEY (const): const PLAYER_FOV_STORAGE_KEY: "jgengine:player-fov"
|
|
142
|
+
- PlayerFovBounds (interface): interface PlayerFovBounds
|
|
143
|
+
- clampPlayerFov (function): function clampPlayerFov(value: unknown, min: number = PLAYER_FOV_MIN, max: number = PLAYER_FOV_MAX): number
|
|
144
|
+
- composePlayerFov (function): function composePlayerFov(preference: number, poseFov: number, mode: "relative" | "absolute" = "relative", bounds: PlayerFovBounds = resolvePlayerFovBounds()): number — Composition model for perspective rigs: - preference is the player base FOV - poseFov is the rig's authored FOV (includes chase-speed modulation, ADS zoom, transitions) - relative mode shifts the authored FOV by (preference − default) so mods still stack - absolute mode (cinematic keyframes) keeps the authored FOV as-is
|
|
145
|
+
- loadPlayerFov (function): function loadPlayerFov(bounds: PlayerFovBounds = resolvePlayerFovBounds(), storage: Pick<Storage, "getItem"> | null | undefined = defaultStorage()): number
|
|
146
|
+
- resolvePlayerFovBounds (function): function resolvePlayerFovBounds(options?: { min?: number; max?: number; default?: number; }): PlayerFovBounds
|
|
147
|
+
- savePlayerFov (function): function savePlayerFov(value: number, bounds: PlayerFovBounds = resolvePlayerFovBounds(), storage: Pick<Storage, "setItem"> | null | undefined = defaultStorage()): number
|
|
148
|
+
|
|
149
|
+
### @jgengine/shell/camera/index
|
|
150
|
+
|
|
151
|
+
- CAMERA_POST_FRAME_PRIORITY (const): const CAMERA_POST_FRAME_PRIORITY: number
|
|
152
|
+
- CAMERA_RIG_FRAME_PRIORITY (const): const CAMERA_RIG_FRAME_PRIORITY: -1
|
|
153
|
+
- CameraBlendScratch (interface): interface CameraBlendScratch
|
|
154
|
+
- CameraFollowListener (type): type CameraFollowListener = (state: CameraFollowState) => void
|
|
155
|
+
- CameraFollowState (interface): interface CameraFollowState
|
|
131
156
|
- CameraPose (interface): interface CameraPose
|
|
157
|
+
- CameraShakeChannel (interface): interface CameraShakeChannel
|
|
158
|
+
- CameraShakeContext (const): const CameraShakeContext: React.Context<CameraShakeChannel>
|
|
159
|
+
- ChaseRig (function): function ChaseRig(props: RigProps): null
|
|
160
|
+
- CinematicRig (function): function CinematicRig(props: RigProps): null
|
|
161
|
+
- DEFAULT_ORBIT_CAMERA (const): const DEFAULT_ORBIT_CAMERA: ResolvedOrbitCameraConfig
|
|
132
162
|
- DirectorCameraValues (interface): interface DirectorCameraValues
|
|
163
|
+
- GAME_SIM_FRAME_PRIORITY (const): const GAME_SIM_FRAME_PRIORITY: 0 — Run simulation/movement before orbit follow so poses are current.
|
|
164
|
+
- GameCameraRig (function): function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): React.JSX.Element
|
|
165
|
+
- GameCameraRigProps (interface): interface GameCameraRigProps
|
|
166
|
+
- GameFirstPersonCamera (function): function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): React.JSX.Element | null
|
|
167
|
+
- GameFirstPersonCameraProps (interface): interface GameFirstPersonCameraProps
|
|
168
|
+
- GameInspectionCamera (function): function GameInspectionCamera({ config: configPatch }: GameInspectionCameraProps): React.JSX.Element — Model-viewer style rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state.
|
|
169
|
+
- GameInspectionCameraProps (interface): interface GameInspectionCameraProps
|
|
170
|
+
- GameOrbitCamera (function): function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls = false, }: GameOrbitCameraProps): React.JSX.Element
|
|
171
|
+
- GameOrbitCameraProps (interface): interface GameOrbitCameraProps
|
|
172
|
+
- LockOnRig (function): function LockOnRig(props: RigProps): null
|
|
173
|
+
- ORBIT_CAMERA_FRAME_PRIORITY (const): const ORBIT_CAMERA_FRAME_PRIORITY: -1 — Orbit follow reads the latest entity pose after GAME_SIM_FRAME_PRIORITY.
|
|
174
|
+
- ObserverRig (function): function ObserverRig(props: RigProps): null — Detached spectator/photo cam (#120): binds to any entity or fixed point and auto-orbits it, reading no player input at all — the van CCTV / photo-mode / kill-cam rig. Distinct from every other rig, which drives from mouse/keys.
|
|
175
|
+
- OrbitCameraConfig (interface): interface OrbitCameraConfig
|
|
176
|
+
- OrbitFollowRuntimeState (interface): interface OrbitFollowRuntimeState
|
|
177
|
+
- PLAYER_FOV_DEFAULT (const): const PLAYER_FOV_DEFAULT: any
|
|
178
|
+
- PLAYER_FOV_MAX (const): const PLAYER_FOV_MAX: 120
|
|
179
|
+
- PLAYER_FOV_MIN (const): const PLAYER_FOV_MIN: 40
|
|
180
|
+
- PLAYER_FOV_STORAGE_KEY (const): const PLAYER_FOV_STORAGE_KEY: "jgengine:player-fov"
|
|
181
|
+
- PlayerFovBounds (interface): interface PlayerFovBounds
|
|
182
|
+
- PlayerFovProvider (function): function PlayerFovProvider({ config, orthographic, children, }: { config?: GameCameraConfig; orthographic: boolean; children: ReactNode; }): React.JSX.Element
|
|
183
|
+
- PlayerFovSlider (function): function PlayerFovSlider(): React.JSX.Element | null
|
|
184
|
+
- PlayerFovState (interface): interface PlayerFovState
|
|
133
185
|
- ResolvedChase (interface): interface ResolvedChase
|
|
134
186
|
- ResolvedDirectedCamera (interface): interface ResolvedDirectedCamera
|
|
187
|
+
- ResolvedInspectionCameraConfig (interface): interface ResolvedInspectionCameraConfig
|
|
135
188
|
- ResolvedObserver (interface): interface ResolvedObserver
|
|
189
|
+
- ResolvedOrbitCameraConfig (interface): interface ResolvedOrbitCameraConfig — Fully resolved shell config after merging with DEFAULT_ORBIT_CAMERA.
|
|
136
190
|
- ResolvedShoulder (interface): interface ResolvedShoulder
|
|
137
191
|
- ResolvedSideScroll (interface): interface ResolvedSideScroll
|
|
138
192
|
- ResolvedTopDown (interface): interface ResolvedTopDown
|
|
193
|
+
- RigProps (interface): interface RigProps
|
|
194
|
+
- RtsRig (function): function RtsRig(props: RigProps & { panKeysEnabled?: boolean }): null
|
|
139
195
|
- ShakeOffset (interface): interface ShakeOffset
|
|
196
|
+
- ShoulderRig (function): function ShoulderRig(props: RigProps): null
|
|
197
|
+
- SideScrollRig (function): function SideScrollRig(props: RigProps): null — Fixed side-on 2.5D follow rig: watches the followed entity from the perpendicular axis, never reading WASD/mouse-look.
|
|
140
198
|
- StaticCameraValues (interface): interface StaticCameraValues
|
|
199
|
+
- TopDownRig (function): function TopDownRig(props: RigProps): null
|
|
141
200
|
- TraumaState (interface): interface TraumaState
|
|
201
|
+
- Vec3 (interface): interface Vec3
|
|
202
|
+
- addTrauma (function): function addTrauma(state: TraumaState, amount: number): void — Add trauma (0..1) and clamp; larger hits raise the ceiling toward 1.
|
|
203
|
+
- angleDelta (function): function angleDelta(a: number, b: number): number — Shortest signed angular delta from `a` to `b`, wrapped to (-PI, PI].
|
|
204
|
+
- applyCameraBlendStep (function): function applyCameraBlendStep(scratch: CameraBlendScratch, camera: Camera, targetFov: number, dt: number): boolean
|
|
205
|
+
- blendShoulder (function): function blendShoulder(hip: ResolvedShoulder, ads: ResolvedShoulder, t: number): ResolvedShoulder — Blend two shoulder framings by an ADS factor in [0,1].
|
|
142
206
|
- cameraFollowStep (function): function cameraFollowStep(input: { camera: Vec3; target: Vec3; previousTarget: Vec3 | null; lockedDistance: number | null; }): CameraFollowState
|
|
143
|
-
- cameraLookPitch (function): function cameraLookPitch(camera: Vec3, target: Vec3): number
|
|
144
|
-
-
|
|
207
|
+
- 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.
|
|
208
|
+
- cameraShake (function): function cameraShake(amplitude: number, decayPerSecond?: number): void — Feed the default camera-shake channel from anywhere (see G7 hitstop cross-cut).
|
|
209
|
+
- captureCameraBlendFrom (function): function captureCameraBlendFrom(scratch: CameraBlendScratch, position: Vector3, quaternion: Quaternion, fov: number, duration: number): void
|
|
210
|
+
- chaseDesiredPosition (function): function chaseDesiredPosition(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
|
|
211
|
+
- chaseLookAt (function): function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Look point ahead of / above a chased vehicle.
|
|
212
|
+
- cinematicSample (function): function cinematicSample(keyframes: readonly CameraKeyframe[], elapsed: number, loop: boolean, fallbackFov: number): CinematicSample — Sample a keyframe path at `elapsed` seconds. Each keyframe's `duration` is the travel time from the previous keyframe into it; segment easing is per the destination keyframe. Reports `done` once past the final keyframe (unless looping, which wraps `elapsed` into the total path duration).
|
|
213
|
+
- clamp (function): function clamp(value: number, min: number, max: number): number
|
|
214
|
+
- clampPlayerFov (function): function clampPlayerFov(value: unknown, min: number = PLAYER_FOV_MIN, max: number = PLAYER_FOV_MAX): number
|
|
215
|
+
- composePlayerFov (function): function composePlayerFov(preference: number, poseFov: number, mode: "relative" | "absolute" = "relative", bounds: PlayerFovBounds = resolvePlayerFovBounds()): number — Composition model for perspective rigs: - preference is the player base FOV - poseFov is the rig's authored FOV (includes chase-speed modulation, ADS zoom, transitions) - relative mode shifts the authored FOV by (preference − default) so mods still stack - absolute mode (cinematic keyframes) keeps the authored FOV as-is
|
|
216
|
+
- createCameraBlendScratch (function): function createCameraBlendScratch(Vector3Ctor: new () => Vector3, QuaternionCtor: new () => Quaternion): CameraBlendScratch
|
|
217
|
+
- createCameraShakeChannel (function): function createCameraShakeChannel(defaultDecayPerSecond = 1.6): CameraShakeChannel
|
|
218
|
+
- createTrauma (function): function createTrauma(): TraumaState
|
|
219
|
+
- crossfadePose (function): function crossfadePose(from: CameraPose, to: CameraPose, t: number): CameraPose — Linear cross-fade between two full camera poses (position, lookAt, fov).
|
|
220
|
+
- defaultCameraShakeChannel (const): const defaultCameraShakeChannel: CameraShakeChannel — Process-wide default channel. A shell mounts its own channel via `CameraShakeContext`, but game systems that have no React context (e.g. a `loop.onTick` reacting to `entity.died`) can import `cameraShake` and feed the default channel directly.
|
|
145
221
|
- distanceBetween (function): function distanceBetween(a: Vec3, b: Vec3): number
|
|
146
|
-
-
|
|
222
|
+
- forwardVector (function): function forwardVector(yaw: number): Vec3
|
|
223
|
+
- lerp (function): function lerp(from: number, to: number, blend: number): number
|
|
147
224
|
- lerpVec3 (function): function lerpVec3(from: Vec3, to: Vec3, blend: number): Vec3
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
-
|
|
225
|
+
- loadPlayerFov (function): function loadPlayerFov(bounds: PlayerFovBounds = resolvePlayerFovBounds(), storage: Pick<Storage, "getItem"> | null | undefined = defaultStorage()): number
|
|
226
|
+
- lockOnPose (function): function lockOnPose(player: Vec3, target: Vec3, config: LockOnCameraConfig | undefined, fov: number): { pose: CameraPose; yaw: number } — Lock-on pose: camera sits behind the player along the player→target vector so the target stays framed. The look point is biased between player and target by `framingBias`.
|
|
227
|
+
- observerPose (function): function observerPose(subject: Vec3, angle: number, resolved: ResolvedObserver, fov: number): CameraPose — Detached spectator pose: orbits `subject` at a fixed distance/height, never reading player input.
|
|
228
|
+
- orbitFollowStep (function): function orbitFollowStep(input: { state: OrbitFollowRuntimeState; desiredTarget: Vec3; deltaSeconds: number; config: ResolvedOrbitCameraConfig; dragging: boolean; }): OrbitFollowRuntimeState & { distance: number } — Pure orbit follow step — smooth target tracking, camera carries target delta (OrbitControls alone keeps camera fixed when only target moves), optional distance lock. Call each frame before OrbitControls.update() in the shell.
|
|
229
|
+
- orbitYawFromCamera (function): function orbitYawFromCamera(cameraX: number, cameraZ: number, targetX: number, targetZ: number): number — Horizontal facing derived from an orbit camera orbiting a target on the XZ plane.
|
|
230
|
+
- resolveChase (function): function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase
|
|
231
|
+
- resolveDirectedCamera (function): function resolveDirectedCamera(director: DirectorCameraValues | undefined, staticConfig: StaticCameraValues): ResolvedDirectedCamera — Merges a `CameraDirector` runtime snapshot over the static `GameCameraConfig` (#196.2). `director` omitted, or its fields `undefined`/`null`, is a pure passthrough to `staticConfig` so mounting a director with no active override changes nothing.
|
|
151
232
|
- resolveFollowTargetFromPosition (function): function resolveFollowTargetFromPosition(position: readonly [number, number, number], config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset">): Vec3
|
|
233
|
+
- resolveInspectionCameraConfig (function): function resolveInspectionCameraConfig(config?: InspectionCameraConfig): ResolvedInspectionCameraConfig
|
|
234
|
+
- resolveInspectionZoomToCursor (function): function resolveInspectionZoomToCursor(anchor: InspectionZoomAnchor): boolean — Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag.
|
|
235
|
+
- resolveObserver (function): function resolveObserver(config: ObserverCameraConfig | undefined): ResolvedObserver
|
|
152
236
|
- resolveOrbitCameraConfig (function): function resolveOrbitCameraConfig(patch?: OrbitCameraConfig): ResolvedOrbitCameraConfig
|
|
237
|
+
- resolvePlayerFovBounds (function): function resolvePlayerFovBounds(options?: { min?: number; max?: number; default?: number; }): PlayerFovBounds
|
|
238
|
+
- resolveRigKind (function): function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind — Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least specific: an explicit `rig` field always wins; then `perspective: "first"` (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence of a rig's own config block selects that rig, checked in the fixed order below (#207.8) so a config carrying more than one block resolves deterministically instead of depending on object key order. Set `rig` explicitly to break a tie.
|
|
239
|
+
- resolveShoulder (function): function resolveShoulder(config: ShoulderCameraConfig | undefined, aiming: boolean): ResolvedShoulder
|
|
240
|
+
- resolveSideScroll (function): function resolveSideScroll(config: SideScrollCameraConfig | undefined): ResolvedSideScroll
|
|
241
|
+
- resolveSideScrollPose (function): function resolveSideScrollPose(entityPos: Vec3, resolved: ResolvedSideScroll, fov: number): CameraPose — Fixed lateral 2.5D follow pose: the camera sits perpendicular to the travel axis at `distance`, above the entity by `height`, and looks at the entity raised by `lookHeight`. Axis "x" watches from +z; axis "z" watches from +x.
|
|
153
242
|
- resolveTargetSmoothing (function): function resolveTargetSmoothing(config: ResolvedOrbitCameraConfig, dragging: boolean): number
|
|
243
|
+
- resolveTopDown (function): function resolveTopDown(config: TopDownCameraConfig | undefined): ResolvedTopDown
|
|
244
|
+
- rightVector (function): function rightVector(yaw: number): Vec3 — Screen-right of a yaw: `forward × up` with up = +Y.
|
|
245
|
+
- rtsPanKeysConflict (function): function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean
|
|
246
|
+
- savePlayerFov (function): function savePlayerFov(value: number, bounds: PlayerFovBounds = resolvePlayerFovBounds(), storage: Pick<Storage, "setItem"> | null | undefined = defaultStorage()): number
|
|
247
|
+
- seatPose (function): function seatPose(follow: Vec3, yaw: number, local: { x?: number; y?: number; z?: number }, fov: number): CameraPose — World-space seat pose (cockpit/hood/rear) rigidly attached to the vehicle.
|
|
248
|
+
- seedInspectionCamera (function): function seedInspectionCamera(config: ResolvedInspectionCameraConfig): { camera: Vec3; target: Vec3 } — Seeds the camera/target world position before OrbitControls mounts. Falls back to `initialDistance` behind `target` on the -Z axis, raised by 40% of that distance, when `initialPosition` is unset.
|
|
154
249
|
- seedOrbitFollowState (function): function seedOrbitFollowState(input: { entityPosition: readonly [number, number, number]; config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset" | "initialDistance" | "initialHeight">; }): OrbitFollowRuntimeState
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
164
|
-
-
|
|
250
|
+
- shakeOffset (function): function shakeOffset(state: TraumaState, config: { maxOffset?: number; maxRoll?: number; exponent?: number; frequency?: number; } | undefined): ShakeOffset — Positional + roll shake for the current trauma. Shake magnitude is `trauma^exponent` so small hits stay subtle; the oscillation is deterministic per-seed pseudo-noise (no per-frame RNG, so identical inputs reproduce).
|
|
251
|
+
- shoulderPose (function): function shoulderPose(follow: Vec3, yaw: number, pitch: number, sideSign: number, shoulder: ResolvedShoulder): CameraPose — Over-the-shoulder camera pose. The boom sits behind the follow point along `yaw`, raised by `heightOffset`, and pushed sideways by `shoulderOffset` (sign set by `sideSign`: +1 right, -1 left). `pitch` tilts the look point.
|
|
252
|
+
- sideScrollFollowBlend (function): function sideScrollFollowBlend(followSmoothing: number, dt: number): number — Frame-rate independent follow blend for the side-scroll rig; `followSmoothing <= 0` hard-locks (blend 1) instead of freezing.
|
|
253
|
+
- smoothBlend (function): function smoothBlend(deltaSeconds: number, speed: number): number — Frame-rate independent exponential smoothing factor in [0, 1].
|
|
254
|
+
- smoothYaw (function): function smoothYaw(current: number, desired: number, speed: number, dt: number): number — Exponential yaw smoothing that respects wrap-around.
|
|
255
|
+
- smoothstep (function): function smoothstep(t: number): number
|
|
256
|
+
- speedToFov (function): function speedToFov(speed: number, curve: { base?: number; max?: number; speedForMax?: number } | undefined): number — Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`.
|
|
257
|
+
- springArmStep (function): function springArmStep(current: Vec3, desired: Vec3, damping: number, dt: number): Vec3 — Exponential spring-arm approach toward a desired point (frame-rate independent).
|
|
258
|
+
- stepTrauma (function): function stepTrauma(state: TraumaState, decayPerSecond: number, dt: number): void — Decay trauma linearly toward 0 and advance the shake clock.
|
|
259
|
+
- topDownPose (function): function topDownPose(follow: Vec3, resolved: ResolvedTopDown, fov: number): CameraPose — Camera pose for a fixed top-down / isometric rig. `pitch` is the elevation of the camera→target ray above the ground (PI/2 = straight down); `yaw` is the fixed azimuth (PI/4 reads isometric). Height sets zoom; horizontal boom is derived so the look angle stays constant as height changes.
|
|
260
|
+
- useCameraShake (function): function useCameraShake(): CameraShakeChannel — The active rig's shake channel — call `.shake(...)` to add trauma from React UI.
|
|
261
|
+
- usePlayerFov (function): function usePlayerFov(): PlayerFovState
|
|
262
|
+
- yawTo (function): function yawTo(from: Vec3, to: Vec3): number — Yaw that points from `from` toward `to` on the XZ plane (matches shell forward = (sin, cos)).
|
|
165
263
|
|
|
166
264
|
### @jgengine/shell/camera/inspectionCameraMath
|
|
167
265
|
|
|
168
|
-
- resolveInspectionCameraConfig (function): function resolveInspectionCameraConfig(config?: InspectionCameraConfig): ResolvedInspectionCameraConfig
|
|
169
|
-
- seedInspectionCamera (function): function seedInspectionCamera(config: ResolvedInspectionCameraConfig): { camera: Vec3; target: Vec3 } — Seeds the camera/target world position before OrbitControls mounts. Falls back to `initialDistance` behind `target` on the -Z axis, raised by 40% of that distance, when `initialPosition` is unset.
|
|
170
|
-
- resolveInspectionZoomToCursor (function): function resolveInspectionZoomToCursor(anchor: InspectionZoomAnchor): boolean — Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag.
|
|
171
266
|
- ResolvedInspectionCameraConfig (interface): interface ResolvedInspectionCameraConfig
|
|
267
|
+
- resolveInspectionCameraConfig (function): function resolveInspectionCameraConfig(config?: InspectionCameraConfig): ResolvedInspectionCameraConfig
|
|
268
|
+
- resolveInspectionZoomToCursor (function): function resolveInspectionZoomToCursor(anchor: InspectionZoomAnchor): boolean — Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag.
|
|
269
|
+
- seedInspectionCamera (function): function seedInspectionCamera(config: ResolvedInspectionCameraConfig): { camera: Vec3; target: Vec3 } — Seeds the camera/target world position before OrbitControls mounts. Falls back to `initialDistance` behind `target` on the -Z axis, raised by 40% of that distance, when `initialPosition` is unset.
|
|
172
270
|
|
|
173
271
|
### @jgengine/shell/camera/orbitCameraMath
|
|
174
272
|
|
|
175
|
-
- orbitYawFromCamera (function): function orbitYawFromCamera(cameraX: number, cameraZ: number, targetX: number, targetZ: number): number — Horizontal facing derived from an orbit camera orbiting a target on the XZ plane.
|
|
176
|
-
- cameraLookPitch (function): function cameraLookPitch(camera: Vec3, target: Vec3): number — Elevation the camera looks along (radians): negative = aiming down, positive = aiming up, 0 = level. Feeds aim.pitch so vertical aim tracks the camera.
|
|
177
|
-
- smoothBlend (function): function smoothBlend(deltaSeconds: number, speed: number): number — Frame-rate independent exponential smoothing factor in [0, 1].
|
|
178
|
-
- resolveOrbitCameraConfig (function): function resolveOrbitCameraConfig(patch?: OrbitCameraConfig): ResolvedOrbitCameraConfig
|
|
179
|
-
- resolveTargetSmoothing (function): function resolveTargetSmoothing(config: ResolvedOrbitCameraConfig, dragging: boolean): number
|
|
180
|
-
- resolveFollowTargetFromPosition (function): function resolveFollowTargetFromPosition(position: readonly [number, number, number], config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset">): Vec3
|
|
181
|
-
- lerpVec3 (function): function lerpVec3(from: Vec3, to: Vec3, blend: number): Vec3
|
|
182
|
-
- distanceBetween (function): function distanceBetween(a: Vec3, b: Vec3): number
|
|
183
|
-
- seedOrbitFollowState (function): function seedOrbitFollowState(input: { entityPosition: readonly [number, number, number]; config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset" | "initialDistance" | "initialHeight">; }): OrbitFollowRuntimeState
|
|
184
|
-
- orbitFollowStep (function): function orbitFollowStep(input: { state: OrbitFollowRuntimeState; desiredTarget: Vec3; deltaSeconds: number; config: ResolvedOrbitCameraConfig; dragging: boolean; }): OrbitFollowRuntimeState & { distance: number } — Pure orbit follow step — smooth target tracking, camera carries target delta (OrbitControls alone keeps camera fixed when only target moves), optional distance lock. Call each frame before OrbitControls.update() in the shell.
|
|
185
|
-
- cameraFollowStep (function): function cameraFollowStep(input: { camera: Vec3; target: Vec3; previousTarget: Vec3 | null; lockedDistance: number | null; }): CameraFollowState
|
|
186
|
-
- Vec3 (interface): interface Vec3
|
|
187
273
|
- CameraFollowState (interface): interface CameraFollowState
|
|
188
|
-
- OrbitCameraConfig (interface): interface OrbitCameraConfig
|
|
189
|
-
- ResolvedOrbitCameraConfig (interface): interface ResolvedOrbitCameraConfig — Fully resolved shell config after merging with DEFAULT_ORBIT_CAMERA.
|
|
190
274
|
- DEFAULT_ORBIT_CAMERA (const): const DEFAULT_ORBIT_CAMERA: ResolvedOrbitCameraConfig
|
|
191
|
-
- GAME_SIM_FRAME_PRIORITY (const): const GAME_SIM_FRAME_PRIORITY: 0
|
|
192
|
-
- ORBIT_CAMERA_FRAME_PRIORITY (const): const ORBIT_CAMERA_FRAME_PRIORITY: -1
|
|
275
|
+
- GAME_SIM_FRAME_PRIORITY (const): const GAME_SIM_FRAME_PRIORITY: 0 — Run simulation/movement before orbit follow so poses are current.
|
|
276
|
+
- ORBIT_CAMERA_FRAME_PRIORITY (const): const ORBIT_CAMERA_FRAME_PRIORITY: -1 — Orbit follow reads the latest entity pose after GAME_SIM_FRAME_PRIORITY.
|
|
277
|
+
- OrbitCameraConfig (interface): interface OrbitCameraConfig
|
|
278
|
+
- OrbitCollisionConfig (interface): interface OrbitCollisionConfig — Spring-arm occlusion config for the orbit rig.
|
|
193
279
|
- OrbitFollowRuntimeState (interface): interface OrbitFollowRuntimeState
|
|
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.
|
|
282
|
+
- Vec3 (interface): interface Vec3
|
|
283
|
+
- cameraFollowStep (function): function cameraFollowStep(input: { camera: Vec3; target: Vec3; previousTarget: Vec3 | null; lockedDistance: number | null; }): CameraFollowState
|
|
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.
|
|
285
|
+
- distanceBetween (function): function distanceBetween(a: Vec3, b: Vec3): number
|
|
286
|
+
- lerpVec3 (function): function lerpVec3(from: Vec3, to: Vec3, blend: number): Vec3
|
|
287
|
+
- orbitFollowStep (function): function orbitFollowStep(input: { state: OrbitFollowRuntimeState; desiredTarget: Vec3; deltaSeconds: number; config: ResolvedOrbitCameraConfig; dragging: boolean; }): OrbitFollowRuntimeState & { distance: number } — Pure orbit follow step — smooth target tracking, camera carries target delta (OrbitControls alone keeps camera fixed when only target moves), optional distance lock. Call each frame before OrbitControls.update() in the shell.
|
|
288
|
+
- orbitYawFromCamera (function): function orbitYawFromCamera(cameraX: number, cameraZ: number, targetX: number, targetZ: number): number — Horizontal facing derived from an orbit camera orbiting a target on the XZ plane.
|
|
289
|
+
- resolveFollowTargetFromPosition (function): function resolveFollowTargetFromPosition(position: readonly [number, number, number], config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset">): Vec3
|
|
290
|
+
- resolveOrbitCameraConfig (function): function resolveOrbitCameraConfig(patch?: OrbitCameraConfig): ResolvedOrbitCameraConfig
|
|
291
|
+
- resolveTargetSmoothing (function): function resolveTargetSmoothing(config: ResolvedOrbitCameraConfig, dragging: boolean): number
|
|
292
|
+
- seedOrbitFollowState (function): function seedOrbitFollowState(input: { entityPosition: readonly [number, number, number]; config: Pick<ResolvedOrbitCameraConfig, "targetHeight" | "targetOffset" | "initialDistance" | "initialHeight">; }): OrbitFollowRuntimeState
|
|
293
|
+
- smoothBlend (function): function smoothBlend(deltaSeconds: number, speed: number): number — Frame-rate independent exponential smoothing factor in [0, 1].
|
|
194
294
|
|
|
195
295
|
### @jgengine/shell/camera/rigMath
|
|
196
296
|
|
|
197
|
-
- rtsPanKeysConflict (function): function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean
|
|
198
|
-
- clamp (function): function clamp(value: number, min: number, max: number): number
|
|
199
|
-
- lerp (function): function lerp(from: number, to: number, blend: number): number
|
|
200
|
-
- smoothstep (function): function smoothstep(t: number): number
|
|
201
|
-
- forwardVector (function): function forwardVector(yaw: number): Vec3
|
|
202
|
-
- rightVector (function): function rightVector(yaw: number): Vec3
|
|
203
|
-
- resolveTopDown (function): function resolveTopDown(config: TopDownCameraConfig | undefined): ResolvedTopDown
|
|
204
|
-
- topDownPose (function): function topDownPose(follow: Vec3, resolved: ResolvedTopDown, fov: number): CameraPose — Camera pose for a fixed top-down / isometric rig. `pitch` is the elevation of the camera→target ray above the ground (PI/2 = straight down); `yaw` is the fixed azimuth (PI/4 reads isometric). Height sets zoom; horizontal boom is derived so the look angle stays constant as height changes.
|
|
205
|
-
- springArmStep (function): function springArmStep(current: Vec3, desired: Vec3, damping: number, dt: number): Vec3 — Exponential spring-arm approach toward a desired point (frame-rate independent).
|
|
206
|
-
- resolveSideScroll (function): function resolveSideScroll(config: SideScrollCameraConfig | undefined): ResolvedSideScroll
|
|
207
|
-
- sideScrollFollowBlend (function): function sideScrollFollowBlend(followSmoothing: number, dt: number): number — Frame-rate independent follow blend for the side-scroll rig; `followSmoothing <= 0` hard-locks (blend 1) instead of freezing.
|
|
208
|
-
- resolveSideScrollPose (function): function resolveSideScrollPose(entityPos: Vec3, resolved: ResolvedSideScroll, fov: number): CameraPose — Fixed lateral 2.5D follow pose: the camera sits perpendicular to the travel axis at `distance`, above the entity by `height`, and looks at the entity raised by `lookHeight`. Axis "x" watches from +z; axis "z" watches from +x.
|
|
209
|
-
- speedToFov (function): function speedToFov(speed: number, curve: { base?: number; max?: number; speedForMax?: number } | undefined): number — Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`.
|
|
210
|
-
- yawTo (function): function yawTo(from: Vec3, to: Vec3): number — Yaw that points from `from` toward `to` on the XZ plane (matches shell forward = (sin, cos)).
|
|
211
|
-
- angleDelta (function): function angleDelta(a: number, b: number): number — Shortest signed angular delta from `a` to `b`, wrapped to (-PI, PI].
|
|
212
|
-
- smoothYaw (function): function smoothYaw(current: number, desired: number, speed: number, dt: number): number — Exponential yaw smoothing that respects wrap-around.
|
|
213
|
-
- resolveShoulder (function): function resolveShoulder(config: ShoulderCameraConfig | undefined, aiming: boolean): ResolvedShoulder
|
|
214
|
-
- blendShoulder (function): function blendShoulder(hip: ResolvedShoulder, ads: ResolvedShoulder, t: number): ResolvedShoulder — Blend two shoulder framings by an ADS factor in [0,1].
|
|
215
|
-
- shoulderPose (function): function shoulderPose(follow: Vec3, yaw: number, pitch: number, sideSign: number, shoulder: ResolvedShoulder): CameraPose — Over-the-shoulder camera pose. The boom sits behind the follow point along `yaw`, raised by `heightOffset`, and pushed sideways by `shoulderOffset` (sign set by `sideSign`: +1 right, -1 left). `pitch` tilts the look point.
|
|
216
|
-
- lockOnPose (function): function lockOnPose(player: Vec3, target: Vec3, config: LockOnCameraConfig | undefined, fov: number): { pose: CameraPose; yaw: number } — Lock-on pose: camera sits behind the player along the player→target vector so the target stays framed. The look point is biased between player and target by `framingBias`.
|
|
217
|
-
- resolveChase (function): function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase
|
|
218
|
-
- resolveObserver (function): function resolveObserver(config: ObserverCameraConfig | undefined): ResolvedObserver
|
|
219
|
-
- observerPose (function): function observerPose(subject: Vec3, angle: number, resolved: ResolvedObserver, fov: number): CameraPose — Detached spectator pose: orbits `subject` at a fixed distance/height, never reading player input.
|
|
220
|
-
- chaseDesiredPosition (function): function chaseDesiredPosition(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
|
|
221
|
-
- chaseLookAt (function): function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Look point ahead of / above a chased vehicle.
|
|
222
|
-
- seatPose (function): function seatPose(follow: Vec3, yaw: number, local: { x?: number; y?: number; z?: number }, fov: number): CameraPose — World-space seat pose (cockpit/hood/rear) rigidly attached to the vehicle.
|
|
223
|
-
- createTrauma (function): function createTrauma(): TraumaState
|
|
224
|
-
- addTrauma (function): function addTrauma(state: TraumaState, amount: number): void — Add trauma (0..1) and clamp; larger hits raise the ceiling toward 1.
|
|
225
|
-
- stepTrauma (function): function stepTrauma(state: TraumaState, decayPerSecond: number, dt: number): void — Decay trauma linearly toward 0 and advance the shake clock.
|
|
226
|
-
- shakeOffset (function): function shakeOffset(state: TraumaState, config: { maxOffset?: number; maxRoll?: number; exponent?: number; frequency?: number; } | undefined): ShakeOffset — Positional + roll shake for the current trauma. Shake magnitude is `trauma^exponent` so small hits stay subtle; the oscillation is deterministic per-seed pseudo-noise (no per-frame RNG, so identical inputs reproduce).
|
|
227
|
-
- crossfadePose (function): function crossfadePose(from: CameraPose, to: CameraPose, t: number): CameraPose — Linear cross-fade between two full camera poses (position, lookAt, fov).
|
|
228
|
-
- resolveDirectedCamera (function): function resolveDirectedCamera(director: DirectorCameraValues | undefined, staticConfig: StaticCameraValues): ResolvedDirectedCamera — Merges a `CameraDirector` runtime snapshot over the static `GameCameraConfig` (#196.2). `director` omitted, or its fields `undefined`/`null`, is a pure passthrough to `staticConfig` so mounting a director with no active override changes nothing.
|
|
229
|
-
- cinematicSample (function): function cinematicSample(keyframes: readonly CameraKeyframe[], elapsed: number, loop: boolean, fallbackFov: number): CinematicSample — Sample a keyframe path at `elapsed` seconds. Each keyframe's `duration` is the travel time from the previous keyframe into it; segment easing is per the destination keyframe. Reports `done` once past the final keyframe (unless looping, which wraps `elapsed` into the total path duration).
|
|
230
297
|
- CameraPose (interface): interface CameraPose
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
- ResolvedShoulder (interface): interface ResolvedShoulder
|
|
298
|
+
- CinematicSample (interface): interface CinematicSample
|
|
299
|
+
- DirectorCameraValues (interface): interface DirectorCameraValues
|
|
234
300
|
- ResolvedChase (interface): interface ResolvedChase
|
|
301
|
+
- ResolvedDirectedCamera (interface): interface ResolvedDirectedCamera
|
|
235
302
|
- ResolvedObserver (interface): interface ResolvedObserver
|
|
236
|
-
-
|
|
303
|
+
- ResolvedShoulder (interface): interface ResolvedShoulder
|
|
304
|
+
- ResolvedSideScroll (interface): interface ResolvedSideScroll
|
|
305
|
+
- ResolvedTopDown (interface): interface ResolvedTopDown
|
|
237
306
|
- ShakeOffset (interface): interface ShakeOffset
|
|
238
|
-
- DirectorCameraValues (interface): interface DirectorCameraValues
|
|
239
307
|
- StaticCameraValues (interface): interface StaticCameraValues
|
|
240
|
-
-
|
|
241
|
-
-
|
|
308
|
+
- TraumaState (interface): interface TraumaState
|
|
309
|
+
- addTrauma (function): function addTrauma(state: TraumaState, amount: number): void — Add trauma (0..1) and clamp; larger hits raise the ceiling toward 1.
|
|
310
|
+
- angleDelta (function): function angleDelta(a: number, b: number): number — Shortest signed angular delta from `a` to `b`, wrapped to (-PI, PI].
|
|
311
|
+
- bankRollStep (function): function bankRollStep(currentRoll: number, yaw: number, previousYaw: number, dt: number, resolved: ResolvedChase): number — One smoothing step of the chase rig's turn-bank roll (#286.10): roll opposes yaw rate, clamped and exponentially damped.
|
|
312
|
+
- blendShoulder (function): function blendShoulder(hip: ResolvedShoulder, ads: ResolvedShoulder, t: number): ResolvedShoulder — Blend two shoulder framings by an ADS factor in [0,1].
|
|
313
|
+
- chaseDesiredPosition (function): function chaseDesiredPosition(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
|
|
314
|
+
- chaseLookAt (function): function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3 — Look point ahead of / above a chased vehicle.
|
|
315
|
+
- cinematicSample (function): function cinematicSample(keyframes: readonly CameraKeyframe[], elapsed: number, loop: boolean, fallbackFov: number): CinematicSample — Sample a keyframe path at `elapsed` seconds. Each keyframe's `duration` is the travel time from the previous keyframe into it; segment easing is per the destination keyframe. Reports `done` once past the final keyframe (unless looping, which wraps `elapsed` into the total path duration).
|
|
316
|
+
- clamp (function): function clamp(value: number, min: number, max: number): number
|
|
317
|
+
- createTrauma (function): function createTrauma(): TraumaState
|
|
318
|
+
- crossfadePose (function): function crossfadePose(from: CameraPose, to: CameraPose, t: number): CameraPose — Linear cross-fade between two full camera poses (position, lookAt, fov).
|
|
319
|
+
- forwardVector (function): function forwardVector(yaw: number): Vec3
|
|
320
|
+
- leadFollowPoint (function): function leadFollowPoint(follow: Vec3, previous: Vec3, dt: number, resolved: ResolvedChase): Vec3 — Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`.
|
|
321
|
+
- lerp (function): function lerp(from: number, to: number, blend: number): number
|
|
322
|
+
- lockOnPose (function): function lockOnPose(player: Vec3, target: Vec3, config: LockOnCameraConfig | undefined, fov: number): { pose: CameraPose; yaw: number } — Lock-on pose: camera sits behind the player along the player→target vector so the target stays framed. The look point is biased between player and target by `framingBias`.
|
|
323
|
+
- observerPose (function): function observerPose(subject: Vec3, angle: number, resolved: ResolvedObserver, fov: number): CameraPose — Detached spectator pose: orbits `subject` at a fixed distance/height, never reading player input.
|
|
324
|
+
- resolveChase (function): function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase
|
|
325
|
+
- resolveDirectedCamera (function): function resolveDirectedCamera(director: DirectorCameraValues | undefined, staticConfig: StaticCameraValues): ResolvedDirectedCamera — Merges a `CameraDirector` runtime snapshot over the static `GameCameraConfig` (#196.2). `director` omitted, or its fields `undefined`/`null`, is a pure passthrough to `staticConfig` so mounting a director with no active override changes nothing.
|
|
326
|
+
- resolveObserver (function): function resolveObserver(config: ObserverCameraConfig | undefined): ResolvedObserver
|
|
327
|
+
- resolveShoulder (function): function resolveShoulder(config: ShoulderCameraConfig | undefined, aiming: boolean): ResolvedShoulder
|
|
328
|
+
- resolveSideScroll (function): function resolveSideScroll(config: SideScrollCameraConfig | undefined): ResolvedSideScroll
|
|
329
|
+
- resolveSideScrollPose (function): function resolveSideScrollPose(entityPos: Vec3, resolved: ResolvedSideScroll, fov: number): CameraPose — Fixed lateral 2.5D follow pose: the camera sits perpendicular to the travel axis at `distance`, above the entity by `height`, and looks at the entity raised by `lookHeight`. Axis "x" watches from +z; axis "z" watches from +x.
|
|
330
|
+
- resolveTopDown (function): function resolveTopDown(config: TopDownCameraConfig | undefined): ResolvedTopDown
|
|
331
|
+
- rightVector (function): function rightVector(yaw: number): Vec3 — Screen-right of a yaw: `forward × up` with up = +Y.
|
|
332
|
+
- rtsPanKeysConflict (function): function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean
|
|
333
|
+
- seatPose (function): function seatPose(follow: Vec3, yaw: number, local: { x?: number; y?: number; z?: number }, fov: number): CameraPose — World-space seat pose (cockpit/hood/rear) rigidly attached to the vehicle.
|
|
334
|
+
- shakeOffset (function): function shakeOffset(state: TraumaState, config: { maxOffset?: number; maxRoll?: number; exponent?: number; frequency?: number; } | undefined): ShakeOffset — Positional + roll shake for the current trauma. Shake magnitude is `trauma^exponent` so small hits stay subtle; the oscillation is deterministic per-seed pseudo-noise (no per-frame RNG, so identical inputs reproduce).
|
|
335
|
+
- shoulderPose (function): function shoulderPose(follow: Vec3, yaw: number, pitch: number, sideSign: number, shoulder: ResolvedShoulder): CameraPose — Over-the-shoulder camera pose. The boom sits behind the follow point along `yaw`, raised by `heightOffset`, and pushed sideways by `shoulderOffset` (sign set by `sideSign`: +1 right, -1 left). `pitch` tilts the look point.
|
|
336
|
+
- sideScrollFollowBlend (function): function sideScrollFollowBlend(followSmoothing: number, dt: number): number — Frame-rate independent follow blend for the side-scroll rig; `followSmoothing <= 0` hard-locks (blend 1) instead of freezing.
|
|
337
|
+
- smoothYaw (function): function smoothYaw(current: number, desired: number, speed: number, dt: number): number — Exponential yaw smoothing that respects wrap-around.
|
|
338
|
+
- smoothstep (function): function smoothstep(t: number): number
|
|
339
|
+
- speedToFov (function): function speedToFov(speed: number, curve: { base?: number; max?: number; speedForMax?: number } | undefined): number — Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`.
|
|
340
|
+
- springArmStep (function): function springArmStep(current: Vec3, desired: Vec3, damping: number, dt: number): Vec3 — Exponential spring-arm approach toward a desired point (frame-rate independent).
|
|
341
|
+
- stepTrauma (function): function stepTrauma(state: TraumaState, decayPerSecond: number, dt: number): void — Decay trauma linearly toward 0 and advance the shake clock.
|
|
342
|
+
- topDownPose (function): function topDownPose(follow: Vec3, resolved: ResolvedTopDown, fov: number): CameraPose — Camera pose for a fixed top-down / isometric rig. `pitch` is the elevation of the camera→target ray above the ground (PI/2 = straight down); `yaw` is the fixed azimuth (PI/4 reads isometric). Height sets zoom; horizontal boom is derived so the look angle stays constant as height changes.
|
|
343
|
+
- yawTo (function): function yawTo(from: Vec3, to: Vec3): number — Yaw that points from `from` toward `to` on the XZ plane (matches shell forward = (sin, cos)).
|
|
242
344
|
|
|
243
345
|
### @jgengine/shell/camera/rigResolve
|
|
244
346
|
|
|
245
|
-
- resolveRigKind (function): function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind
|
|
246
|
-
- turntableAsObserver (function): function turntableAsObserver(config: GameCameraConfig | undefined): GameCameraConfig
|
|
347
|
+
- resolveRigKind (function): function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind — Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least specific: an explicit `rig` field always wins; then `perspective: "first"` (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence of a rig's own config block selects that rig, checked in the fixed order below (#207.8) so a config carrying more than one block resolves deterministically instead of depending on object key order. Set `rig` explicitly to break a tie.
|
|
348
|
+
- turntableAsObserver (function): function turntableAsObserver(config: GameCameraConfig | undefined): GameCameraConfig — The turntable rig is a flat facade over the observer's point-orbit mode: map its `target`/`distance`/… onto an observer block so ObserverRig runs unchanged.
|
|
247
349
|
|
|
248
350
|
### @jgengine/shell/camera/shakeChannel
|
|
249
351
|
|
|
352
|
+
- CameraShakeChannel (interface): interface CameraShakeChannel
|
|
353
|
+
- CameraShakeContext (const): const CameraShakeContext: React.Context<CameraShakeChannel>
|
|
354
|
+
- cameraShake (function): function cameraShake(amplitude: number, decayPerSecond?: number): void — Feed the default camera-shake channel from anywhere (see G7 hitstop cross-cut).
|
|
250
355
|
- createCameraShakeChannel (function): function createCameraShakeChannel(defaultDecayPerSecond = 1.6): CameraShakeChannel
|
|
251
|
-
-
|
|
252
|
-
- useCameraShake (function): function useCameraShake(): CameraShakeChannel
|
|
356
|
+
- defaultCameraShakeChannel (const): const defaultCameraShakeChannel: CameraShakeChannel — Process-wide default channel. A shell mounts its own channel via `CameraShakeContext`, but game systems that have no React context (e.g. a `loop.onTick` reacting to `entity.died`) can import `cameraShake` and feed the default channel directly.
|
|
357
|
+
- useCameraShake (function): function useCameraShake(): CameraShakeChannel — The active rig's shake channel — call `.shake(...)` to add trauma from React UI.
|
|
358
|
+
|
|
359
|
+
### @jgengine/shell/camera/shakeChannelMath
|
|
360
|
+
|
|
253
361
|
- CameraShakeChannel (interface): interface CameraShakeChannel
|
|
254
|
-
-
|
|
255
|
-
|
|
362
|
+
- createCameraShakeChannel (function): function createCameraShakeChannel(defaultDecayPerSecond = 1.6): CameraShakeChannel
|
|
363
|
+
|
|
364
|
+
### @jgengine/shell/cartridge
|
|
365
|
+
|
|
366
|
+
- CartridgeAbilitySlot (interface): interface CartridgeAbilitySlot
|
|
367
|
+
- CartridgeConfig (type): type CartridgeConfig = CartridgeSpec & { name: string; panels: CartridgePanels; hud: { storageKey?: string; panels: readonly CartridgeHudPanelSpec[] }; screens: CartridgeScreens; theme?: Record<string, string>; world?: GameConfig["world"]; physics?: GameConfig["physics"]; assets?: GameConfig["assets…
|
|
368
|
+
- CartridgeHudPanelSpec (interface): interface CartridgeHudPanelSpec
|
|
369
|
+
- CartridgePanelItem (type): type CartridgePanelItem = | { kind: "vital"; stat: string; label: string; tone?: string; width?: number } | { kind: "xp"; width?: number } | { kind: "timer"; label: string } | { kind: "score"; source: "kills"; label: string; digits?: number } | { kind: "abilityBar"; icons: Record<string, string> } |…
|
|
370
|
+
- CartridgePanels (interface): interface CartridgePanels
|
|
371
|
+
- CartridgeResultLine (type): type CartridgeResultLine = { label: string; accent?: boolean } & ( | { source: "kills" | "level" } | { value: string | number } )
|
|
372
|
+
- CartridgeScreens (interface): interface CartridgeScreens
|
|
373
|
+
- cartridge (function): function cartridge(config: CartridgeConfig): PlayableGame
|
|
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.
|
|
256
381
|
|
|
257
382
|
### @jgengine/shell/defineGame
|
|
258
383
|
|
|
259
|
-
- defineGame (function): function defineGame<TAssetRef extends ModelAssetRef = ModelAssetRef>(config: GameConfig<TAssetRef>): PlayableGame
|
|
260
384
|
- GameConfig (type): type GameConfig<TAssetRef extends ModelAssetRef = ModelAssetRef> = EngineFields<TAssetRef> & PresentationFields
|
|
385
|
+
- defineGame (function): function defineGame<TAssetRef extends ModelAssetRef = ModelAssetRef>(config: GameConfig<TAssetRef>): PlayableGame
|
|
386
|
+
|
|
387
|
+
### @jgengine/shell/devtools/CollisionDebugWorld
|
|
388
|
+
|
|
389
|
+
- CollisionDebugWorld (function): function CollisionDebugWorld(): React.JSX.Element | null — World-space collision debugger. Performs zero scene scans and zero raycasts when every layer is off. Mount only when shell devtools are enabled.
|
|
261
390
|
|
|
262
391
|
### @jgengine/shell/devtools/DevtoolsOverlay
|
|
263
392
|
|
|
264
|
-
-
|
|
393
|
+
- DevtoolsOverlay (function): function DevtoolsOverlay({ open, ctx, playable, multiplayer, }: { open: boolean; ctx: GameContext; playable: PlayableGame; multiplayer: ShellMultiplayer | null; }): React.JSX.Element | null
|
|
394
|
+
- DevtoolsRendererProbe (function): function DevtoolsRendererProbe(): null
|
|
265
395
|
- applyStoredDevtoolsOverrides (function): function applyStoredDevtoolsOverrides(gameName: string): void
|
|
396
|
+
- buildFullReport (function): function buildFullReport(playable: PlayableGame): DevtoolsSnapshot & { game: string }
|
|
397
|
+
- buildLeanReport (function): function buildLeanReport(playable: PlayableGame): { game: any; at: any; why: string | null; frame: { fps: number; avgFrameMs: number; p95FrameMs: number; maxFrameMs: number; avgSimMs: number; maxSimMs: number; avgOutsideMs: number; maxOutsideMs: number; longFrames: any; samples: any; phases: any; } …
|
|
398
|
+
- persistDevtoolsOverrides (function): function persistDevtoolsOverrides(gameName: string): DevtoolsOverrides
|
|
266
399
|
- withDevtoolsLatency (function): function withDevtoolsLatency(multiplayer: ShellMultiplayer): ShellMultiplayer
|
|
267
|
-
|
|
268
|
-
|
|
400
|
+
|
|
401
|
+
### @jgengine/shell/devtools/collisionDebug
|
|
402
|
+
|
|
403
|
+
- AimProbeConfig (interface): interface AimProbeConfig
|
|
404
|
+
- COLLISION_DEBUG_LAYERS (const): const COLLISION_DEBUG_LAYERS: readonly CollisionDebugLayer[]
|
|
405
|
+
- CollisionDebugController (interface): interface CollisionDebugController
|
|
406
|
+
- CollisionDebugLayer (type): type CollisionDebugLayer = | "hitboxes" | "bodies" | "projectiles" | "muzzles" | "aimLaser"
|
|
407
|
+
- CollisionDebugLayers (type): type CollisionDebugLayers = Record<CollisionDebugLayer, boolean>
|
|
408
|
+
- CollisionDebugListener (type): type CollisionDebugListener = () => void
|
|
409
|
+
- CollisionDebugState (interface): interface CollisionDebugState
|
|
410
|
+
- ProjectileDebugTrace (interface): interface ProjectileDebugTrace
|
|
411
|
+
- aimProbeNeeded (function): function aimProbeNeeded(layers: CollisionDebugLayers): boolean
|
|
412
|
+
- anyCollisionLayerOn (function): function anyCollisionLayerOn(layers: CollisionDebugLayers): boolean
|
|
413
|
+
- colliderScanNeeded (function): function colliderScanNeeded(layers: CollisionDebugLayers): boolean
|
|
414
|
+
- collisionDebug (const): const collisionDebug: CollisionDebugController
|
|
415
|
+
- createCollisionDebugController (function): function createCollisionDebugController(initial: CollisionDebugState = createDefaultCollisionDebugState()): CollisionDebugController
|
|
416
|
+
- createDefaultCollisionDebugState (function): function createDefaultCollisionDebugState(): CollisionDebugState
|
|
417
|
+
- projectileListenNeeded (function): function projectileListenNeeded(layers: CollisionDebugLayers): boolean
|
|
418
|
+
|
|
419
|
+
### @jgengine/shell/devtools/collisionDebugMath
|
|
420
|
+
|
|
421
|
+
- AIM_DAMAGE_COLOR (const): const AIM_DAMAGE_COLOR: "#f87171"
|
|
422
|
+
- AIM_LASER_COLOR (const): const AIM_LASER_COLOR: "#a3e635"
|
|
423
|
+
- AIM_MISS_COLOR (const): const AIM_MISS_COLOR: "#94a3b8"
|
|
424
|
+
- AIM_SOLID_COLOR (const): const AIM_SOLID_COLOR: "#fbbf24"
|
|
425
|
+
- AimEndpointKind (type): type AimEndpointKind = "damage" | "solid" | "miss"
|
|
426
|
+
- AimLaserDebug (interface): interface AimLaserDebug
|
|
427
|
+
- BODY_WIRE_COLOR (const): const BODY_WIRE_COLOR: "#38bdf8"
|
|
428
|
+
- CollectDebugShapesInput (interface): interface CollectDebugShapesInput
|
|
429
|
+
- ComputeAimLaserInput (interface): interface ComputeAimLaserInput
|
|
430
|
+
- DebugShapeEntry (interface): interface DebugShapeEntry
|
|
431
|
+
- HITBOX_WIRE_COLOR (const): const HITBOX_WIRE_COLOR: "#f472b6"
|
|
432
|
+
- PROJECTILE_PATH_COLOR (const): const PROJECTILE_PATH_COLOR: "#fde68a"
|
|
433
|
+
- classifyAimEndpoint (function): function classifyAimEndpoint(hit: Pick<SceneRaycastHit, "damageEligible"> | null | undefined): AimEndpointKind
|
|
434
|
+
- collectDebugShapes (function): function collectDebugShapes(input: CollectDebugShapesInput): DebugShapeEntry[] — Collects world-space collider wireframe entries for enabled layers. Returns [] and performs no entity/object iteration when both hitbox/body layers are off.
|
|
435
|
+
- computeAimLaser (function): function computeAimLaser(input: ComputeAimLaserInput): AimLaserDebug | null — Builds the authoritative aim-laser segment using resolveShot + scene raycast (same seam as projectile prediction/settlement). Zero queries when aimLaser is off.
|
|
436
|
+
- muzzleMarkerFromOrigin (function): function muzzleMarkerFromOrigin(origin: EntityPosition): { center: EntityPosition; radius: number; color: string; }
|
|
437
|
+
- pointAlongRay (function): function pointAlongRay(origin: EntityPosition, direction: EntityPosition, distance: number): EntityPosition
|
|
438
|
+
- shapeWorldCenter (function): function shapeWorldCenter(collider: ResolvedCollider, position: EntityPosition, rotationY: number): EntityPosition
|
|
269
439
|
|
|
270
440
|
### @jgengine/shell/environment
|
|
271
441
|
|
|
272
|
-
- Daylight (function): function Daylight({ sky, fog, sun, ambient }: DaylightProps = {}): React.JSX.Element
|
|
273
|
-
-
|
|
274
|
-
- SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
|
|
275
|
-
- TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock }: TimeOfDayDaylightProps): React.JSX.Element — Drives `Daylight`'s sun/sky/fog from the world clock when `sky.timeOfDay` and `clock` are both present, sampling `daylightStateAt` every frame; otherwise renders the static preset look via `SkyDaylight`.
|
|
442
|
+
- Daylight (function): function Daylight({ sky, fog, sun, ambient, lights = true }: DaylightProps = {}): React.JSX.Element
|
|
443
|
+
- DaylightCycleConfig (interface): interface DaylightCycleConfig
|
|
276
444
|
- DaylightProps (interface): interface DaylightProps
|
|
277
|
-
-
|
|
278
|
-
- TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
|
|
445
|
+
- DaylightState (interface): interface DaylightState
|
|
279
446
|
- EnvironmentScene (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element
|
|
280
447
|
- EnvironmentSceneProps (interface): interface EnvironmentSceneProps
|
|
281
|
-
- daylightStateAt (function): function daylightStateAt(dayFraction: number, config: DaylightCycleConfig = {}): DaylightState — Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk), lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
|
|
282
|
-
- lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
|
|
283
448
|
- SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
|
|
284
|
-
-
|
|
285
|
-
-
|
|
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.
|
|
450
|
+
- SkyDaylightProps (interface): interface SkyDaylightProps
|
|
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
|
|
452
|
+
- SkyDomeProps (interface): interface SkyDomeProps
|
|
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).
|
|
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.
|
|
455
|
+
- TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
|
|
456
|
+
- daylightStateAt (function): function daylightStateAt(dayFraction: number, config: DaylightCycleConfig = {}): DaylightState — Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk), lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
|
|
457
|
+
- lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
|
|
458
|
+
- resolveSkyLightOwnership (function): function resolveSkyLightOwnership(hasAuthoredLighting: boolean): SkyLightOwnership
|
|
459
|
+
- skyEmitsLights (function): function skyEmitsLights(ownership: SkyLightOwnership): boolean
|
|
286
460
|
|
|
287
461
|
### @jgengine/shell/environment/Daylight
|
|
288
462
|
|
|
289
|
-
-
|
|
290
|
-
- Daylight (function): function Daylight({ sky, fog, sun, ambient }: DaylightProps = {}): React.JSX.Element
|
|
291
|
-
- SkyDaylight (function): function SkyDaylight({ sky }: { sky: SkyEnvironmentDescriptor }): React.JSX.Element — Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
|
|
292
|
-
- TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock }: TimeOfDayDaylightProps): React.JSX.Element — Drives `Daylight`'s sun/sky/fog from the world clock when `sky.timeOfDay` and `clock` are both present, sampling `daylightStateAt` every frame; otherwise renders the static preset look via `SkyDaylight`.
|
|
293
|
-
- SkyDomeProps (interface): interface SkyDomeProps
|
|
463
|
+
- Daylight (function): function Daylight({ sky, fog, sun, ambient, lights = true }: DaylightProps = {}): React.JSX.Element
|
|
294
464
|
- DaylightProps (interface): interface DaylightProps
|
|
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.
|
|
466
|
+
- SkyDaylightProps (interface): interface SkyDaylightProps
|
|
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
|
|
468
|
+
- SkyDomeProps (interface): interface SkyDomeProps
|
|
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.
|
|
295
470
|
- TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
|
|
296
471
|
|
|
297
|
-
### @jgengine/shell/environment/daylightCycle
|
|
298
|
-
|
|
299
|
-
- lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
|
|
300
|
-
- daylightStateAt (function): function daylightStateAt(dayFraction: number, config: DaylightCycleConfig = {}): DaylightState — Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk), lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
|
|
301
|
-
- DaylightCycleConfig (interface): interface DaylightCycleConfig
|
|
302
|
-
- DaylightState (interface): interface DaylightState
|
|
303
|
-
- DEFAULT_DAY_SUN_INTENSITY (const): const DEFAULT_DAY_SUN_INTENSITY: 1
|
|
304
|
-
- DEFAULT_DAY_AMBIENT_INTENSITY (const): const DEFAULT_DAY_AMBIENT_INTENSITY: 0.6
|
|
305
|
-
- DEFAULT_DAY_SKY_TOP (const): const DEFAULT_DAY_SKY_TOP: "#3fa4f2"
|
|
306
|
-
- DEFAULT_DAY_SKY_BOTTOM (const): const DEFAULT_DAY_SKY_BOTTOM: "#e3f4ff"
|
|
307
|
-
- SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
|
|
308
|
-
|
|
309
472
|
### @jgengine/shell/environment/EnvironmentScene
|
|
310
473
|
|
|
311
474
|
- EnvironmentScene (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element
|
|
@@ -316,97 +479,111 @@ Imports use deep paths: `@jgengine/shell/<path>`.
|
|
|
316
479
|
- GroundPad (function): function GroundPad({ pad, field }: GroundPadProps): React.JSX.Element
|
|
317
480
|
- GroundPadProps (interface): interface GroundPadProps
|
|
318
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
|
+
|
|
486
|
+
### @jgengine/shell/environment/daylightCycle
|
|
487
|
+
|
|
488
|
+
- DEFAULT_DAY_AMBIENT_INTENSITY (const): const DEFAULT_DAY_AMBIENT_INTENSITY: 0.6
|
|
489
|
+
- DEFAULT_DAY_SKY_BOTTOM (const): const DEFAULT_DAY_SKY_BOTTOM: "#e3f4ff"
|
|
490
|
+
- DEFAULT_DAY_SKY_TOP (const): const DEFAULT_DAY_SKY_TOP: "#3fa4f2"
|
|
491
|
+
- DEFAULT_DAY_SUN_INTENSITY (const): const DEFAULT_DAY_SUN_INTENSITY: 1
|
|
492
|
+
- DaylightCycleConfig (interface): interface DaylightCycleConfig
|
|
493
|
+
- DaylightState (interface): interface DaylightState
|
|
494
|
+
- SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
|
|
495
|
+
- daylightStateAt (function): function daylightStateAt(dayFraction: number, config: DaylightCycleConfig = {}): DaylightState — Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk), lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
|
|
496
|
+
- lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
|
|
497
|
+
|
|
319
498
|
### @jgengine/shell/environment/groundPadMath
|
|
320
499
|
|
|
321
|
-
- resolvePadShape (function): function resolvePadShape(size: PadSize): PadShape
|
|
322
|
-
- resolvePadSurfaceY (function): function resolvePadSurfaceY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height">): number
|
|
323
|
-
- resolvePadMeshY (function): function resolvePadMeshY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height">): number
|
|
324
500
|
- PAD_THICKNESS (const): const PAD_THICKNESS: 0.1
|
|
325
501
|
- PadShape (type): type PadShape = { circular: true; radius: number } | { circular: false; width: number; depth: number }
|
|
502
|
+
- resolvePadMeshY (function): function resolvePadMeshY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number
|
|
503
|
+
- resolvePadShape (function): function resolvePadShape(size: PadSize): PadShape
|
|
504
|
+
- resolvePadSurfaceY (function): function resolvePadSurfaceY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number
|
|
326
505
|
|
|
327
506
|
### @jgengine/shell/environment/index
|
|
328
507
|
|
|
329
|
-
- Daylight (function): function Daylight({ sky, fog, sun, ambient }: DaylightProps = {}): React.JSX.Element
|
|
330
|
-
-
|
|
331
|
-
- SkyDome (function): function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, materialRef, }: SkyDomeProps = {}): React.JSX.Element
|
|
332
|
-
- TimeOfDayDaylight (function): function TimeOfDayDaylight({ sky, clock }: TimeOfDayDaylightProps): React.JSX.Element — Drives `Daylight`'s sun/sky/fog from the world clock when `sky.timeOfDay` and `clock` are both present, sampling `daylightStateAt` every frame; otherwise renders the static preset look via `SkyDaylight`.
|
|
508
|
+
- Daylight (function): function Daylight({ sky, fog, sun, ambient, lights = true }: DaylightProps = {}): React.JSX.Element
|
|
509
|
+
- DaylightCycleConfig (interface): interface DaylightCycleConfig
|
|
333
510
|
- DaylightProps (interface): interface DaylightProps
|
|
334
|
-
-
|
|
335
|
-
- TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
|
|
511
|
+
- DaylightState (interface): interface DaylightState
|
|
336
512
|
- EnvironmentScene (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element
|
|
337
513
|
- EnvironmentSceneProps (interface): interface EnvironmentSceneProps
|
|
338
|
-
- daylightStateAt (function): function daylightStateAt(dayFraction: number, config: DaylightCycleConfig = {}): DaylightState — Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk), lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
|
|
339
|
-
- lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
|
|
340
514
|
- SKY_PRESET_DAY_FRACTION (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>
|
|
341
|
-
-
|
|
342
|
-
-
|
|
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.
|
|
516
|
+
- SkyDaylightProps (interface): interface SkyDaylightProps
|
|
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
|
|
518
|
+
- SkyDomeProps (interface): interface SkyDomeProps
|
|
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).
|
|
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.
|
|
521
|
+
- TimeOfDayDaylightProps (interface): interface TimeOfDayDaylightProps
|
|
522
|
+
- daylightStateAt (function): function daylightStateAt(dayFraction: number, config: DaylightCycleConfig = {}): DaylightState — Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk), lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
|
|
523
|
+
- lerpHexColor (function): function lerpHexColor(a: string, b: string, t: number): string
|
|
524
|
+
- resolveSkyLightOwnership (function): function resolveSkyLightOwnership(hasAuthoredLighting: boolean): SkyLightOwnership
|
|
525
|
+
- skyEmitsLights (function): function skyEmitsLights(ownership: SkyLightOwnership): boolean
|
|
343
526
|
|
|
344
|
-
### @jgengine/shell/
|
|
527
|
+
### @jgengine/shell/environment/skyLightingPolicy
|
|
345
528
|
|
|
346
|
-
-
|
|
347
|
-
-
|
|
529
|
+
- 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).
|
|
530
|
+
- resolveSkyLightOwnership (function): function resolveSkyLightOwnership(hasAuthoredLighting: boolean): SkyLightOwnership
|
|
531
|
+
- skyEmitsLights (function): function skyEmitsLights(ownership: SkyLightOwnership): boolean
|
|
348
532
|
|
|
349
|
-
### @jgengine/shell/
|
|
533
|
+
### @jgengine/shell/input/mouseLook
|
|
350
534
|
|
|
351
|
-
-
|
|
352
|
-
-
|
|
535
|
+
- MouseLookAim (interface): interface MouseLookAim
|
|
536
|
+
- MouseLookOptions (interface): interface MouseLookOptions
|
|
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.
|
|
538
|
+
- createMouseLookTracker (function): function createMouseLookTracker(element: HTMLElement, options: MouseLookOptions = {}): MouseLookTracker
|
|
353
539
|
|
|
354
|
-
### @jgengine/shell/
|
|
540
|
+
### @jgengine/shell/inputSink
|
|
355
541
|
|
|
356
|
-
-
|
|
357
|
-
-
|
|
358
|
-
-
|
|
359
|
-
-
|
|
360
|
-
-
|
|
361
|
-
- resolveWorldSky (function): function resolveWorldSky(world: WorldFeature | undefined): SkyEnvironmentDescriptor | undefined — The world's declared sky, when its world feature is an environment with one (#196.1).
|
|
362
|
-
- resolvePhysicsTuning (function): function resolvePhysicsTuning(physics: PhysicsConfig | undefined): MovementTuningOverrides | undefined — Maps the game's declared `physics` onto the movement controllers' tuning overrides. `PhysicsConfig.gravity` is a signed world acceleration (negative points down, matching every game's config and the Y-up convention), but the controllers integrate `velocityY -= gravityAcceleration * dt` and so expect a positive downward magnitude. Negating here is what keeps a down-pointing gravity pulling the player *down*; passing the signed value straight through flipped the sign and launched airborne players upward instead.
|
|
363
|
-
- hasEnvironmentTerrain (function): function hasEnvironmentTerrain(world: WorldFeature | undefined): boolean — True when the world is an environment feature with terrain, so the voxel controller should sample its height.
|
|
364
|
-
- GamePlayerShell (function): function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, }: { playable: PlayableGame; multiplayer?: ShellMultiplayer | null; }): React.JSX.Element
|
|
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.
|
|
365
547
|
|
|
366
|
-
### @jgengine/shell/
|
|
548
|
+
### @jgengine/shell/map/MapMarkerBeacons
|
|
367
549
|
|
|
368
|
-
-
|
|
369
|
-
-
|
|
370
|
-
- defaultUiScenario (const): const defaultUiScenario: UiPreviewScenario
|
|
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`.
|
|
551
|
+
- MapMarkerBeaconsProps (interface): interface MapMarkerBeaconsProps
|
|
371
552
|
|
|
372
553
|
### @jgengine/shell/map/index
|
|
373
554
|
|
|
374
|
-
- bakeTerrainMap (function): function bakeTerrainMap(field: TerrainField, bounds: MapBakeBounds, options: BakeTerrainMapOptions = {}): BakedMap | null — Bake a top-down image of a `TerrainField` (or `RegionField`) over `bounds` for the react `Minimap` / `WorldMap` background. Runs in the browser via a 2D canvas; renderer-side, so it lives in the shell.
|
|
375
|
-
- BakedMap (interface): interface BakedMap
|
|
376
555
|
- BakeTerrainMapOptions (interface): interface BakeTerrainMapOptions
|
|
556
|
+
- BakedMap (interface): interface BakedMap
|
|
377
557
|
- MapBakeBounds (interface): interface MapBakeBounds
|
|
378
|
-
- MapMarkerBeacons (function): function MapMarkerBeacons({ markers, kindStyles, height = 5 }: MapMarkerBeaconsProps): React.JSX.Element
|
|
379
|
-
- MapMarkerBeaconsProps (interface): interface MapMarkerBeaconsProps
|
|
380
|
-
|
|
381
|
-
### @jgengine/shell/map/MapMarkerBeacons
|
|
382
|
-
|
|
383
|
-
- MapMarkerBeacons (function): function MapMarkerBeacons({ markers, kindStyles, height = 5 }: MapMarkerBeaconsProps): React.JSX.Element — World-space beacons for map markers (the visible in-world side of a ping): a floating diamond over a soft light beam, colored by marker kind. Wire it through `PlayableGame.WorldOverlay`.
|
|
558
|
+
- MapMarkerBeacons (function): function MapMarkerBeacons({ markers, kindStyles, height = 5 }: MapMarkerBeaconsProps): React.JSX.Element — World-space beacons for map markers (the visible in-world side of a ping): a floating diamond over a soft light beam, colored by marker kind. Wire it through `PlayableGame.WorldOverlay`.
|
|
384
559
|
- MapMarkerBeaconsProps (interface): interface MapMarkerBeaconsProps
|
|
560
|
+
- bakeTerrainMap (function): function bakeTerrainMap(field: TerrainField, bounds: MapBakeBounds, options: BakeTerrainMapOptions = {}): BakedMap | null — Bake a top-down image of a `TerrainField` (or `RegionField`) over `bounds` for the react `Minimap` / `WorldMap` background. Runs in the browser via a 2D canvas; renderer-side, so it lives in the shell.
|
|
385
561
|
|
|
386
562
|
### @jgengine/shell/map/terrainMap
|
|
387
563
|
|
|
388
|
-
- bakeTerrainMap (function): function bakeTerrainMap(field: TerrainField, bounds: MapBakeBounds, options: BakeTerrainMapOptions = {}): BakedMap | null — Bake a top-down image of a `TerrainField` (or `RegionField`) over `bounds` for the react `Minimap` / `WorldMap` background. Runs in the browser via a 2D canvas; renderer-side, so it lives in the shell.
|
|
389
|
-
- MapBakeBounds (interface): interface MapBakeBounds
|
|
390
564
|
- BakeTerrainMapOptions (interface): interface BakeTerrainMapOptions
|
|
391
565
|
- BakedMap (interface): interface BakedMap
|
|
566
|
+
- MapBakeBounds (interface): interface MapBakeBounds
|
|
567
|
+
- bakeTerrainMap (function): function bakeTerrainMap(field: TerrainField, bounds: MapBakeBounds, options: BakeTerrainMapOptions = {}): BakedMap | null — Bake a top-down image of a `TerrainField` (or `RegionField`) over `bounds` for the react `Minimap` / `WorldMap` background. Runs in the browser via a 2D canvas; renderer-side, so it lives in the shell.
|
|
392
568
|
|
|
393
569
|
### @jgengine/shell/materialOverride
|
|
394
570
|
|
|
395
|
-
-
|
|
571
|
+
- MaterialOverrideOptions (interface): interface MaterialOverrideOptions
|
|
572
|
+
- applyMaterialOverride (function): function applyMaterialOverride(root: THREE.Object3D, override: ModelMaterialOverride, options?: MaterialOverrideOptions): void
|
|
396
573
|
|
|
397
574
|
### @jgengine/shell/multiplayer
|
|
398
575
|
|
|
399
|
-
- randomPlayerId (function): function randomPlayerId(): string
|
|
400
|
-
- resolveShellMultiplayer (function): function resolveShellMultiplayer(args: ResolveShellMultiplayerArgs): ShellMultiplayer | null
|
|
401
|
-
- resolvePeerShellMultiplayer (function): function resolvePeerShellMultiplayer(args: { gameId: string; role: "host" | "join"; room?: string; userId?: string; feedActions?: string[]; }): Promise<ShellMultiplayer & { close: () => void }>
|
|
402
|
-
- ShellMultiplayer (type): type ShellMultiplayer = MultiplayerSession
|
|
403
576
|
- DEFAULT_FEED_ACTIONS (const): const DEFAULT_FEED_ACTIONS: string[]
|
|
404
577
|
- ResolveShellMultiplayerArgs (type): type ResolveShellMultiplayerArgs = { game: GameDefinition; gameId: string; url?: string; userId?: string; force?: boolean; feedActions?: string[]; }
|
|
578
|
+
- ShellMultiplayer (type): type ShellMultiplayer = MultiplayerSession
|
|
579
|
+
- randomPlayerId (function): function randomPlayerId(): string
|
|
580
|
+
- resolvePeerShellMultiplayer (function): function resolvePeerShellMultiplayer(args: { gameId: string; role: "host" | "join"; room?: string; userId?: string; feedActions?: string[]; }): Promise<ShellMultiplayer & { close: () => void }>
|
|
581
|
+
- resolveShellMultiplayer (function): function resolveShellMultiplayer(args: ResolveShellMultiplayerArgs): ShellMultiplayer | null
|
|
405
582
|
|
|
406
583
|
### @jgengine/shell/pointer/PointerOverlays
|
|
407
584
|
|
|
408
|
-
- MarqueeBox (function): function MarqueeBox({ rect }: { rect: ScreenRect }): React.JSX.Element
|
|
409
585
|
- ContextMenuView (function): function ContextMenuView({ menu, x, y, onPick, onClose, }: { menu: ContextMenu; x: number; y: number; onPick: (verb: ContextVerb) => void; onClose: () => void; }): React.JSX.Element
|
|
586
|
+
- MarqueeBox (function): function MarqueeBox({ rect }: { rect: ScreenRect }): React.JSX.Element
|
|
410
587
|
|
|
411
588
|
### @jgengine/shell/pointer/PointerProbe
|
|
412
589
|
|
|
@@ -414,118 +591,174 @@ Imports use deep paths: `@jgengine/shell/<path>`.
|
|
|
414
591
|
|
|
415
592
|
### @jgengine/shell/pointer/pointerService
|
|
416
593
|
|
|
417
|
-
- createPointerService (function): function createPointerService(): PointerService
|
|
418
594
|
- POINTER_ENTITY_KEY (const): const POINTER_ENTITY_KEY: "jgEntityId"
|
|
419
595
|
- POINTER_OBJECT_KEY (const): const POINTER_OBJECT_KEY: "jgObjectId"
|
|
596
|
+
- PointerHitFilter (type): type PointerHitFilter = (object: THREE.Object3D) => boolean
|
|
420
597
|
- PointerService (interface): interface PointerService
|
|
598
|
+
- createPointerService (function): function createPointerService(): PointerService
|
|
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.
|
|
421
607
|
|
|
422
608
|
### @jgengine/shell/registry
|
|
423
609
|
|
|
424
|
-
-
|
|
610
|
+
- GameRegistry (type): type GameRegistry = Record<string, () => Promise<PlayableGame>>
|
|
611
|
+
- PlayableGame (type): type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity, RenderObject>
|
|
425
612
|
- RenderEntity (type): type RenderEntity = (entity: SceneEntity) => ReactNode
|
|
426
613
|
- RenderObject (type): type RenderObject = (object: SceneObject) => ReactNode
|
|
427
|
-
-
|
|
428
|
-
- GameRegistry (type): type GameRegistry = Record<string, () => Promise<PlayableGame>>
|
|
614
|
+
- resolveGameLoader (function): function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined
|
|
429
615
|
|
|
430
616
|
### @jgengine/shell/render/modelRender
|
|
431
617
|
|
|
432
|
-
-
|
|
433
|
-
-
|
|
618
|
+
- MaterialCache (interface): interface MaterialCache
|
|
619
|
+
- PAINT_TEXTURE_SIZE (const): const PAINT_TEXTURE_SIZE: 512
|
|
620
|
+
- PaintCanvas (interface): interface PaintCanvas
|
|
621
|
+
- applyPaintTexture (function): function applyPaintTexture(root: THREE.Object3D, paint: PaintCanvas): void
|
|
622
|
+
- applyPaintTextureToMaterials (function): function applyPaintTextureToMaterials(materials: readonly THREE.MeshStandardMaterial[], paint: PaintCanvas): void
|
|
623
|
+
- cacheStandardMaterials (function): function cacheStandardMaterials(root: THREE.Object3D, into?: MaterialCache | null): MaterialCache
|
|
624
|
+
- cloneModelScene (function): function cloneModelScene(source: THREE.Object3D, options?: { cloneMaterials?: boolean }): THREE.Object3D
|
|
434
625
|
- createPaintCanvas (function): function createPaintCanvas(seed: THREE.MeshStandardMaterial, size = PAINT_TEXTURE_SIZE): PaintCanvas
|
|
626
|
+
- disposeClonedMaterials (function): function disposeClonedMaterials(root: THREE.Object3D): void
|
|
435
627
|
- drawPaintStrokes (function): function drawPaintStrokes(paint: PaintCanvas, strokes: readonly PaintStroke[]): void
|
|
436
|
-
-
|
|
628
|
+
- standardMaterialsOf (function): function standardMaterialsOf(root: THREE.Object3D): THREE.MeshStandardMaterial[]
|
|
437
629
|
- syncPaintCanvas (function): function syncPaintCanvas(paint: PaintCanvas, seedColor: THREE.Color, strokes: readonly PaintStroke[], drawnCount: number): number
|
|
438
|
-
|
|
439
|
-
|
|
630
|
+
|
|
631
|
+
### @jgengine/shell/render/resolveModel
|
|
632
|
+
|
|
633
|
+
- ModelResolveContext (interface): interface ModelResolveContext
|
|
634
|
+
- resolveModel (function): function resolveModel(value: string | ModelConfig | undefined, assets: AssetCatalog, context?: ModelResolveContext): ModelConfig | undefined — Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog ids throw — silent generic-primitive fallback only happens when the mapping omits the key entirely (or uses tryResolveCatalogModel for optional ids).
|
|
635
|
+
- tryResolveCatalogModel (function): function tryResolveCatalogModel(id: string, assets: AssetCatalog): ModelConfig | undefined — Soft lookup used when an object catalog id may double as a model asset id.
|
|
440
636
|
|
|
441
637
|
### @jgengine/shell/replay/useSessionRecorder
|
|
442
638
|
|
|
443
|
-
- useSessionRecorder (function): function useSessionRecorder(entityId: string, options?: RecordingBufferOptions): RecordingBuffer<RecordedPose> — Session-recording buffer (#120) for replay / photo mode / kill-cam: records an entity's pose on game-time every frame into a `RecordingBuffer`, which a game can then `seek()` to scrub, drive an observer cam ghost, or export a kill-cam clip. Recording rides on `ctx.time.now()`, so pause/fast-forward scrub the recording exactly like the live sim.
|
|
444
639
|
- RecordedPose (interface): interface RecordedPose
|
|
640
|
+
- useSessionRecorder (function): function useSessionRecorder(entityId: string, options?: RecordingBufferOptions): RecordingBuffer<RecordedPose> — Session-recording buffer (#120) for replay / photo mode / kill-cam: records an entity's pose on game-time every frame into a `RecordingBuffer`, which a game can then `seek()` to scrub, drive an observer cam ghost, or export a kill-cam clip. Recording rides on `ctx.time.now()`, so pause/fast-forward scrub the recording exactly like the live sim.
|
|
641
|
+
|
|
642
|
+
### @jgengine/shell/settings/QuickControls
|
|
643
|
+
|
|
644
|
+
- QuickControls (function): function QuickControls({ controller }: { controller: SettingsController }): React.JSX.Element | null
|
|
645
|
+
|
|
646
|
+
### @jgengine/shell/settings/SettingsChrome
|
|
647
|
+
|
|
648
|
+
- SettingsChrome (function): function SettingsChrome(): React.JSX.Element | null
|
|
649
|
+
|
|
650
|
+
### @jgengine/shell/settings/SettingsMenu
|
|
651
|
+
|
|
652
|
+
- SettingsMenu (function): function SettingsMenu({ controller, onClose, initialTab, }: { controller: SettingsController; onClose: () => void; initialTab?: string; }): React.JSX.Element | null
|
|
653
|
+
|
|
654
|
+
### @jgengine/shell/settings/SettingsRuntime
|
|
655
|
+
|
|
656
|
+
- SettingsRuntime (function): function SettingsRuntime({ variant, surface, actions, children, ...input }: SettingsRuntimeProps): React.JSX.Element
|
|
657
|
+
- SettingsRuntimeProps (interface): interface SettingsRuntimeProps extends SettingsControllerInput
|
|
658
|
+
|
|
659
|
+
### @jgengine/shell/settings/appliedSettings
|
|
660
|
+
|
|
661
|
+
- AudioSettingsBridge (function): function AudioSettingsBridge({ store, engine, buses, }: { store: SettingsStore; engine: AudioEngine; buses: Record<string, AudioBusDef> | undefined; }): null
|
|
662
|
+
- useGraphicsSettings (function): function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean): { shadows: boolean; dpr: number; uiScale: number }
|
|
663
|
+
- useSettingsRevision (function): function useSettingsRevision(store: SettingsStore): number
|
|
664
|
+
|
|
665
|
+
### @jgengine/shell/settings/settingsController
|
|
666
|
+
|
|
667
|
+
- SettingsControllerInput (interface): interface SettingsControllerInput
|
|
668
|
+
- useSettingsCategories (function): function useSettingsCategories(config: SettingsControllerInput): SettingsCategoryView[]
|
|
445
669
|
|
|
446
670
|
### @jgengine/shell/structures
|
|
447
671
|
|
|
448
672
|
- BuildingBlock (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element
|
|
449
|
-
- GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
|
|
450
673
|
- BuildingBlockProps (interface): interface BuildingBlockProps
|
|
674
|
+
- GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
|
|
451
675
|
- GeneratedBuildingProps (interface): interface GeneratedBuildingProps
|
|
676
|
+
- InstancedBuildingPlacement (interface): interface InstancedBuildingPlacement
|
|
677
|
+
- InstancedBuildings (function): function InstancedBuildings({ buildings, palette, visibleKinds }: InstancedBuildingsProps): React.JSX.Element | null
|
|
678
|
+
- InstancedBuildingsProps (interface): interface InstancedBuildingsProps
|
|
452
679
|
- PlacementGhost (function): function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }: PlacementGhostProps): React.JSX.Element | null
|
|
453
680
|
- PlacementGhostProps (interface): interface PlacementGhostProps
|
|
454
681
|
|
|
455
682
|
### @jgengine/shell/structures/GeneratedBuilding
|
|
456
683
|
|
|
457
684
|
- BuildingBlock (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element
|
|
458
|
-
-
|
|
685
|
+
- BuildingBlockProps (interface): interface BuildingBlockProps
|
|
459
686
|
- BuildingFacade (type): type BuildingFacade = "front" | "back" | "left" | "right" | "roof"
|
|
687
|
+
- BuildingKitRenderer (interface): interface BuildingKitRenderer
|
|
688
|
+
- BuildingMaterialPalette (interface): interface BuildingMaterialPalette
|
|
460
689
|
- BuildingPartKind (type): type BuildingPartKind = | "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner"
|
|
461
690
|
- BuildingPartPlacement (interface): interface BuildingPartPlacement
|
|
691
|
+
- GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
|
|
462
692
|
- GeneratedBuildingData (interface): interface GeneratedBuildingData
|
|
463
|
-
- BuildingMaterialPalette (interface): interface BuildingMaterialPalette
|
|
464
|
-
- BuildingKitRenderer (interface): interface BuildingKitRenderer
|
|
465
|
-
- BuildingBlockProps (interface): interface BuildingBlockProps
|
|
466
693
|
- GeneratedBuildingProps (interface): interface GeneratedBuildingProps
|
|
694
|
+
- InstancedBuildingPlacement (interface): interface InstancedBuildingPlacement
|
|
695
|
+
- InstancedBuildings (function): function InstancedBuildings({ buildings, palette, visibleKinds }: InstancedBuildingsProps): React.JSX.Element | null
|
|
696
|
+
- InstancedBuildingsProps (interface): interface InstancedBuildingsProps
|
|
467
697
|
|
|
468
|
-
### @jgengine/shell/structures/
|
|
698
|
+
### @jgengine/shell/structures/PlacementGhost
|
|
469
699
|
|
|
470
|
-
- BuildingBlock (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element
|
|
471
|
-
- GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
|
|
472
|
-
- BuildingBlockProps (interface): interface BuildingBlockProps
|
|
473
|
-
- GeneratedBuildingProps (interface): interface GeneratedBuildingProps
|
|
474
700
|
- PlacementGhost (function): function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }: PlacementGhostProps): React.JSX.Element | null
|
|
475
701
|
- PlacementGhostProps (interface): interface PlacementGhostProps
|
|
476
702
|
|
|
477
|
-
### @jgengine/shell/structures/
|
|
703
|
+
### @jgengine/shell/structures/index
|
|
478
704
|
|
|
705
|
+
- BuildingBlock (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element
|
|
706
|
+
- BuildingBlockProps (interface): interface BuildingBlockProps
|
|
707
|
+
- GeneratedBuilding (function): function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): React.JSX.Element
|
|
708
|
+
- GeneratedBuildingProps (interface): interface GeneratedBuildingProps
|
|
709
|
+
- InstancedBuildingPlacement (interface): interface InstancedBuildingPlacement
|
|
710
|
+
- InstancedBuildings (function): function InstancedBuildings({ buildings, palette, visibleKinds }: InstancedBuildingsProps): React.JSX.Element | null
|
|
711
|
+
- InstancedBuildingsProps (interface): interface InstancedBuildingsProps
|
|
479
712
|
- PlacementGhost (function): function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }: PlacementGhostProps): React.JSX.Element | null
|
|
480
713
|
- PlacementGhostProps (interface): interface PlacementGhostProps
|
|
481
714
|
|
|
482
715
|
### @jgengine/shell/terrain
|
|
483
716
|
|
|
484
|
-
- CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element
|
|
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.
|
|
485
718
|
- CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
486
|
-
- GrassField (function): function GrassField({ count = 6000, density = 1, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = false, ...meshProps }: GrassFieldProps): React.JSX.Element
|
|
487
|
-
- GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
488
|
-
- ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
|
|
489
|
-
- ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
490
|
-
- EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
|
|
491
|
-
- EditableGroundProps (interface): interface EditableGroundProps
|
|
492
|
-
- TerraformBrushCursor (function): function TerraformBrushCursor({ center, y = 0.05, radius, mode }: TerraformBrushCursorProps): React.JSX.Element | null
|
|
493
|
-
- TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
|
|
494
|
-
- createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
|
|
495
|
-
- resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
|
|
496
|
-
- resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
|
|
497
|
-
- GrassBladeGeometryOptions (interface): interface GrassBladeGeometryOptions
|
|
498
|
-
- GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
|
|
499
|
-
- ResolvedGrassBladeGeometryOptions (interface): interface ResolvedGrassBladeGeometryOptions
|
|
500
|
-
- createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
|
|
501
719
|
- DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
|
|
502
|
-
-
|
|
720
|
+
- EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
|
|
721
|
+
- EditableGroundProps (interface): interface EditableGroundProps
|
|
722
|
+
- FieldGroundOptions (interface): interface FieldGroundOptions
|
|
723
|
+
- GrassBladeGeometryOptions (interface): interface GrassBladeGeometryOptions
|
|
724
|
+
- GrassField (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, ...meshPr…
|
|
725
|
+
- GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
503
726
|
- GrassMaterialHandle (interface): interface GrassMaterialHandle
|
|
504
727
|
- GrassMaterialOptions (interface): interface GrassMaterialOptions
|
|
728
|
+
- GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
|
|
505
729
|
- GrassShaderUniforms (interface): interface GrassShaderUniforms
|
|
506
730
|
- GrassWindOptions (interface): interface GrassWindOptions
|
|
507
|
-
-
|
|
508
|
-
-
|
|
509
|
-
-
|
|
731
|
+
- ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
|
|
732
|
+
- ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
733
|
+
- ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
|
|
734
|
+
- ResolvedGrassBladeGeometryOptions (interface): interface ResolvedGrassBladeGeometryOptions
|
|
735
|
+
- ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
|
|
736
|
+
- ResolvedTerrainSize (interface): interface ResolvedTerrainSize
|
|
737
|
+
- TerraformBrushCursor (function): function TerraformBrushCursor({ center, y = 0.05, radius, mode }: TerraformBrushCursorProps): React.JSX.Element | null
|
|
738
|
+
- TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
|
|
739
|
+
- TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
|
|
740
|
+
- TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
|
|
510
741
|
- TerrainSeed (type): type TerrainSeed = number | string
|
|
511
|
-
-
|
|
742
|
+
- TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
|
|
743
|
+
- createFieldGroundGeometry (function): function createFieldGroundGeometry(field: TerrainField, options: FieldGroundOptions = {}): THREE.BufferGeometry — Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
744
|
+
- createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
|
|
745
|
+
- createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
|
|
512
746
|
- createProceduralGroundGeometry (function): function createProceduralGroundGeometry(config: ProceduralTerrainConfig = {}, colors: TerrainVertexColorOptions = {}): THREE.BufferGeometry
|
|
513
747
|
- createProceduralTerrainSampler (function): function createProceduralTerrainSampler(config: ProceduralTerrainConfig = {}): TerrainHeightSampler
|
|
748
|
+
- createSeededRandom (function): function createSeededRandom(seed: TerrainSeed = 1): () => number
|
|
749
|
+
- hashNoise2 (function): function hashNoise2(x: number, z: number, seed: TerrainSeed = 1): number
|
|
514
750
|
- normalizeHeightBlend (function): function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number
|
|
751
|
+
- resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
|
|
752
|
+
- resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
|
|
753
|
+
- resolveGrassWind (function): function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>
|
|
515
754
|
- resolveTerrainSegments (function): function resolveTerrainSegments(segments: ProceduralTerrainConfig["segments"] = 96): ResolvedTerrainSegments
|
|
516
755
|
- resolveTerrainSize (function): function resolveTerrainSize(size: TerrainArea = 40): ResolvedTerrainSize
|
|
756
|
+
- seedToUint32 (function): function seedToUint32(seed: TerrainSeed = 1): number
|
|
517
757
|
- toNoiseFieldConfig (function): function toNoiseFieldConfig(config: ProceduralTerrainConfig = {}): NoiseFieldConfig
|
|
518
|
-
- FieldGroundOptions (interface): interface FieldGroundOptions
|
|
519
|
-
- ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
|
|
520
|
-
- ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
|
|
521
|
-
- ResolvedTerrainSize (interface): interface ResolvedTerrainSize
|
|
522
|
-
- TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
|
|
523
|
-
- TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
|
|
524
|
-
- TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
|
|
525
758
|
|
|
526
759
|
### @jgengine/shell/terrain/CarvedTerrain
|
|
527
760
|
|
|
528
|
-
- CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element
|
|
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.
|
|
529
762
|
- CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
530
763
|
|
|
531
764
|
### @jgengine/shell/terrain/EditableGround
|
|
@@ -535,167 +768,201 @@ Imports use deep paths: `@jgengine/shell/<path>`.
|
|
|
535
768
|
|
|
536
769
|
### @jgengine/shell/terrain/GrassField
|
|
537
770
|
|
|
538
|
-
-
|
|
771
|
+
- DEFAULT_GRASS_COUNT (const): const DEFAULT_GRASS_COUNT: 1500
|
|
772
|
+
- DEFAULT_GRASS_DENSITY (const): const DEFAULT_GRASS_DENSITY: 1
|
|
773
|
+
- GrassField (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, ...meshPr…
|
|
539
774
|
- GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
775
|
+
- resolveGrassInstanceBudget (function): function resolveGrassInstanceBudget(count: number, density: number, budget?: number): number
|
|
776
|
+
|
|
777
|
+
### @jgengine/shell/terrain/ProceduralGround
|
|
778
|
+
|
|
779
|
+
- ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
|
|
780
|
+
- ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
781
|
+
|
|
782
|
+
### @jgengine/shell/terrain/TerraformBrushCursor
|
|
783
|
+
|
|
784
|
+
- TerraformBrushCursor (function): function TerraformBrushCursor({ center, y = 0.05, radius, mode }: TerraformBrushCursorProps): React.JSX.Element | null
|
|
785
|
+
- TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
|
|
786
|
+
|
|
787
|
+
### @jgengine/shell/terrain/grassBudget
|
|
788
|
+
|
|
789
|
+
- DEFAULT_GRASS_COUNT (const): const DEFAULT_GRASS_COUNT: 1500
|
|
790
|
+
- DEFAULT_GRASS_DENSITY (const): const DEFAULT_GRASS_DENSITY: 1
|
|
791
|
+
- resolveGrassInstanceBudget (function): function resolveGrassInstanceBudget(count: number, density: number, budget?: number): number
|
|
540
792
|
|
|
541
793
|
### @jgengine/shell/terrain/grassGeometry
|
|
542
794
|
|
|
543
|
-
- resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
|
|
544
|
-
- resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
|
|
545
|
-
- createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
|
|
546
|
-
- GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
|
|
547
795
|
- GrassBladeGeometryOptions (interface): interface GrassBladeGeometryOptions
|
|
796
|
+
- GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
|
|
548
797
|
- ResolvedGrassBladeGeometryOptions (interface): interface ResolvedGrassBladeGeometryOptions
|
|
798
|
+
- createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
|
|
799
|
+
- resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
|
|
800
|
+
- resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
|
|
549
801
|
|
|
550
802
|
### @jgengine/shell/terrain/grassMaterial
|
|
551
803
|
|
|
552
|
-
-
|
|
553
|
-
-
|
|
554
|
-
- GrassWindOptions (interface): interface GrassWindOptions
|
|
804
|
+
- DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
|
|
805
|
+
- GrassMaterialHandle (interface): interface GrassMaterialHandle
|
|
555
806
|
- GrassMaterialOptions (interface): interface GrassMaterialOptions
|
|
556
807
|
- GrassShaderUniforms (interface): interface GrassShaderUniforms
|
|
557
|
-
-
|
|
558
|
-
-
|
|
808
|
+
- GrassWindOptions (interface): interface GrassWindOptions
|
|
809
|
+
- createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
|
|
810
|
+
- resolveGrassWind (function): function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>
|
|
559
811
|
|
|
560
812
|
### @jgengine/shell/terrain/index
|
|
561
813
|
|
|
562
|
-
- CarvedTerrain (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element
|
|
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.
|
|
563
815
|
- CarvedTerrainProps (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
564
|
-
-
|
|
565
|
-
- GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
566
|
-
- ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
|
|
567
|
-
- ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
816
|
+
- DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
|
|
568
817
|
- EditableGround (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element
|
|
569
818
|
- EditableGroundProps (interface): interface EditableGroundProps
|
|
570
|
-
-
|
|
571
|
-
- TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
|
|
572
|
-
- createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
|
|
573
|
-
- resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
|
|
574
|
-
- resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
|
|
819
|
+
- FieldGroundOptions (interface): interface FieldGroundOptions
|
|
575
820
|
- GrassBladeGeometryOptions (interface): interface GrassBladeGeometryOptions
|
|
576
|
-
-
|
|
577
|
-
-
|
|
578
|
-
- createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
|
|
579
|
-
- DEFAULT_GRASS_WIND (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions>
|
|
580
|
-
- resolveGrassWind (function): function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>
|
|
821
|
+
- GrassField (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, ...meshPr…
|
|
822
|
+
- GrassFieldProps (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
581
823
|
- GrassMaterialHandle (interface): interface GrassMaterialHandle
|
|
582
824
|
- GrassMaterialOptions (interface): interface GrassMaterialOptions
|
|
825
|
+
- GrassRange (type): type GrassRange = number | readonly [min: number, max: number]
|
|
583
826
|
- GrassShaderUniforms (interface): interface GrassShaderUniforms
|
|
584
827
|
- GrassWindOptions (interface): interface GrassWindOptions
|
|
585
|
-
-
|
|
586
|
-
-
|
|
587
|
-
-
|
|
828
|
+
- ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
|
|
829
|
+
- ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
830
|
+
- ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
|
|
831
|
+
- ResolvedGrassBladeGeometryOptions (interface): interface ResolvedGrassBladeGeometryOptions
|
|
832
|
+
- ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
|
|
833
|
+
- ResolvedTerrainSize (interface): interface ResolvedTerrainSize
|
|
834
|
+
- TerraformBrushCursor (function): function TerraformBrushCursor({ center, y = 0.05, radius, mode }: TerraformBrushCursorProps): React.JSX.Element | null
|
|
835
|
+
- TerraformBrushCursorProps (interface): interface TerraformBrushCursorProps
|
|
836
|
+
- TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
|
|
837
|
+
- TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
|
|
588
838
|
- TerrainSeed (type): type TerrainSeed = number | string
|
|
589
|
-
-
|
|
839
|
+
- TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
|
|
840
|
+
- createFieldGroundGeometry (function): function createFieldGroundGeometry(field: TerrainField, options: FieldGroundOptions = {}): THREE.BufferGeometry — Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
841
|
+
- createGrassBladeGeometry (function): function createGrassBladeGeometry(options: GrassBladeGeometryOptions = {}): THREE.InstancedBufferGeometry
|
|
842
|
+
- createGrassMaterial (function): function createGrassMaterial(options: GrassMaterialOptions = {}): GrassMaterialHandle
|
|
590
843
|
- createProceduralGroundGeometry (function): function createProceduralGroundGeometry(config: ProceduralTerrainConfig = {}, colors: TerrainVertexColorOptions = {}): THREE.BufferGeometry
|
|
591
844
|
- createProceduralTerrainSampler (function): function createProceduralTerrainSampler(config: ProceduralTerrainConfig = {}): TerrainHeightSampler
|
|
845
|
+
- createSeededRandom (function): function createSeededRandom(seed: TerrainSeed = 1): () => number
|
|
846
|
+
- hashNoise2 (function): function hashNoise2(x: number, z: number, seed: TerrainSeed = 1): number
|
|
592
847
|
- normalizeHeightBlend (function): function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number
|
|
848
|
+
- resolveGrassBladeGeometryOptions (function): function resolveGrassBladeGeometryOptions(options: GrassBladeGeometryOptions = {}): ResolvedGrassBladeGeometryOptions
|
|
849
|
+
- resolveGrassRange (function): function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number]
|
|
850
|
+
- resolveGrassWind (function): function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>
|
|
593
851
|
- resolveTerrainSegments (function): function resolveTerrainSegments(segments: ProceduralTerrainConfig["segments"] = 96): ResolvedTerrainSegments
|
|
594
852
|
- resolveTerrainSize (function): function resolveTerrainSize(size: TerrainArea = 40): ResolvedTerrainSize
|
|
853
|
+
- seedToUint32 (function): function seedToUint32(seed: TerrainSeed = 1): number
|
|
595
854
|
- toNoiseFieldConfig (function): function toNoiseFieldConfig(config: ProceduralTerrainConfig = {}): NoiseFieldConfig
|
|
596
|
-
- FieldGroundOptions (interface): interface FieldGroundOptions
|
|
597
|
-
- ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
|
|
598
|
-
- ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
|
|
599
|
-
- ResolvedTerrainSize (interface): interface ResolvedTerrainSize
|
|
600
|
-
- TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
|
|
601
|
-
- TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
|
|
602
|
-
- TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
|
|
603
|
-
|
|
604
|
-
### @jgengine/shell/terrain/ProceduralGround
|
|
605
|
-
|
|
606
|
-
- ProceduralGround (function): function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }: ProceduralGroundProps): React.JSX.Element
|
|
607
|
-
- ProceduralGroundProps (interface): interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
608
855
|
|
|
609
856
|
### @jgengine/shell/terrain/random
|
|
610
857
|
|
|
611
|
-
-
|
|
858
|
+
- TerrainSeed (type): type TerrainSeed = number | string
|
|
612
859
|
- createSeededRandom (function): function createSeededRandom(seed: TerrainSeed = 1): () => number
|
|
613
860
|
- hashNoise2 (function): function hashNoise2(x: number, z: number, seed: TerrainSeed = 1): number
|
|
614
|
-
-
|
|
861
|
+
- seedToUint32 (function): function seedToUint32(seed: TerrainSeed = 1): number
|
|
615
862
|
|
|
616
|
-
### @jgengine/shell/terrain/
|
|
863
|
+
### @jgengine/shell/terrain/terrainDetailMaterial
|
|
617
864
|
|
|
618
|
-
-
|
|
619
|
-
-
|
|
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.
|
|
620
867
|
|
|
621
868
|
### @jgengine/shell/terrain/terrainMath
|
|
622
869
|
|
|
623
|
-
-
|
|
624
|
-
- resolveTerrainSize (function): function resolveTerrainSize(size: TerrainArea = 40): ResolvedTerrainSize
|
|
625
|
-
- resolveTerrainSegments (function): function resolveTerrainSegments(segments: ProceduralTerrainConfig["segments"] = 96): ResolvedTerrainSegments
|
|
626
|
-
- toNoiseFieldConfig (function): function toNoiseFieldConfig(config: ProceduralTerrainConfig = {}): NoiseFieldConfig
|
|
627
|
-
- createProceduralTerrainSampler (function): function createProceduralTerrainSampler(config: ProceduralTerrainConfig = {}): TerrainHeightSampler
|
|
628
|
-
- createFieldGroundGeometry (function): function createFieldGroundGeometry(field: TerrainField, options: FieldGroundOptions = {}): THREE.BufferGeometry — Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
629
|
-
- createProceduralGroundGeometry (function): function createProceduralGroundGeometry(config: ProceduralTerrainConfig = {}, colors: TerrainVertexColorOptions = {}): THREE.BufferGeometry
|
|
630
|
-
- TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
|
|
631
|
-
- TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
|
|
870
|
+
- FieldGroundOptions (interface): interface FieldGroundOptions
|
|
632
871
|
- ProceduralTerrainConfig (interface): interface ProceduralTerrainConfig
|
|
633
|
-
- ResolvedTerrainSize (interface): interface ResolvedTerrainSize
|
|
634
872
|
- ResolvedTerrainSegments (interface): interface ResolvedTerrainSegments
|
|
873
|
+
- ResolvedTerrainSize (interface): interface ResolvedTerrainSize
|
|
874
|
+
- TerrainArea (type): type TerrainArea = number | readonly [width: number, depth: number]
|
|
875
|
+
- TerrainHeightSampler (type): type TerrainHeightSampler = (x: number, z: number) => number
|
|
876
|
+
- TerrainPaletteSampler (type): type TerrainPaletteSampler = (x: number, z: number) => { low: string; high: string; waterline?: string } — Per-position palette override for multi-biome ground coloring — `createTerrainPaletteSampler` from `@jgengine/core/world/terrain` returns exactly this shape.
|
|
635
877
|
- TerrainVertexColorOptions (interface): interface TerrainVertexColorOptions
|
|
636
|
-
-
|
|
878
|
+
- createFieldGroundGeometry (function): function createFieldGroundGeometry(field: TerrainField, options: FieldGroundOptions = {}): THREE.BufferGeometry — Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
879
|
+
- createProceduralGroundGeometry (function): function createProceduralGroundGeometry(config: ProceduralTerrainConfig = {}, colors: TerrainVertexColorOptions = {}): THREE.BufferGeometry
|
|
880
|
+
- createProceduralTerrainSampler (function): function createProceduralTerrainSampler(config: ProceduralTerrainConfig = {}): TerrainHeightSampler
|
|
881
|
+
- normalizeHeightBlend (function): function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number
|
|
882
|
+
- resolveTerrainSegments (function): function resolveTerrainSegments(segments: ProceduralTerrainConfig["segments"] = 96): ResolvedTerrainSegments
|
|
883
|
+
- resolveTerrainSize (function): function resolveTerrainSize(size: TerrainArea = 40): ResolvedTerrainSize
|
|
884
|
+
- toNoiseFieldConfig (function): function toNoiseFieldConfig(config: ProceduralTerrainConfig = {}): NoiseFieldConfig
|
|
885
|
+
|
|
886
|
+
### @jgengine/shell/touch/OrientationHint
|
|
887
|
+
|
|
888
|
+
- OrientationHint (function): function OrientationHint({ wanted }: { wanted: "landscape" | "portrait" }): React.JSX.Element | null
|
|
637
889
|
|
|
638
890
|
### @jgengine/shell/touch/TouchControlsOverlay
|
|
639
891
|
|
|
640
|
-
- TouchPlaySurface (function): function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }: { scheme: TouchScheme; sink: TouchCodeSink; yawRef: MutableRefObject<number>; pitchRef: MutableRefObject<number>; maxPitch: number; onPrimaryTap: () => void; }): React.JSX.Element
|
|
641
|
-
- TouchControlsDock (function): function TouchControlsDock({ scheme, sink }: { scheme: TouchScheme; sink: TouchCodeSink }): React.JSX.Element
|
|
642
892
|
- TouchCodeSink (interface): interface TouchCodeSink
|
|
893
|
+
- TouchControlsDock (function): function TouchControlsDock({ scheme, sink, scale = 1 }: { scheme: TouchScheme; sink: TouchCodeSink; scale?: number }): React.JSX.Element
|
|
894
|
+
- TouchPlaySurface (function): function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }: { scheme: TouchScheme; sink: TouchCodeSink; yawRef: MutableRefObject<number>; pitchRef: MutableRefObject<number>; maxPitch: number; onPrimaryTap: () => void; }): React.JSX.Element
|
|
895
|
+
- primaryButtonOffsets (function): function primaryButtonOffsets(count: number, scale = 1): { right: number; bottom: number }[] | null — Thumb-arc placement for primary buttons around the bottom-right corner: up to three on an inner ring, the rest on an outer ring. Null means too many buttons for an arc — the dock falls back to a wrapping grid.
|
|
896
|
+
- touchDockClearance (function): function touchDockClearance(scheme: TouchScheme | null, scale = 1): number — Vertical space (px, excluding device safe areas) the dock occupies above the bottom edge. The shell publishes it as `--jg-hud-dock-clearance` so `HudCanvas` regions never collide with touch controls.
|
|
897
|
+
|
|
898
|
+
### @jgengine/shell/visibility/CullingProvider
|
|
899
|
+
|
|
900
|
+
- CullingProvider (function): function CullingProvider({ config, children }: { config: VisibilityConfig | undefined; children: ReactNode }): ReactNode — Drives automatic frustum + distance culling for every entity and placed object. It reads the live render camera each frame, updates the engine VisibilitySystem, and exposes a predicate the entity/object markers consult to toggle `group.visible` — objects fully outside the view (plus a conservative preload margin) are never submitted to the renderer, without unmounting them or touching gameplay. UI, sky, terrain, and environment live outside this subtree and are unaffected.
|
|
901
|
+
- useRenderVisibility (function): function useRenderVisibility(): MutableRefObject<VisiblePredicate> — Read the current render-visibility predicate. Backward compatible: with no CullingProvider mounted (or culling disabled) it returns an always-visible ref, so a marker that consults it behaves exactly as before this feature existed.
|
|
643
902
|
|
|
644
903
|
### @jgengine/shell/vision/FrustumSensorHud
|
|
645
904
|
|
|
646
|
-
- useFrustumSensor (function): function useFrustumSensor(options: FrustumSensorProbeOptions): FrustumSample | null — View-frustum sensor (#117) driven by the live render camera: which held-camera subjects are in frame, how well framed, and their dwell time on-screen (Content Warning-style "is this monster filmed well" scoring). Returns the best-framed in-view subject each frame for a photo-mode HUD readout.
|
|
647
|
-
- FrustumSensorReadout (function): function FrustumSensorReadout(props: FrustumSensorProbeOptions & { wrapperClassName?: string; className?: string }): React.JSX.Element — Renders inside the Canvas (needs the live camera via `useFrame`/`useThree`) but portals a real HTML readout via drei's `Html fullscreen` — a photo-mode "is this subject framed" HUD.
|
|
648
905
|
- FrustumSensorProbeOptions (interface): interface FrustumSensorProbeOptions extends FramingConfig
|
|
906
|
+
- FrustumSensorReadout (function): function FrustumSensorReadout(props: FrustumSensorProbeOptions & { wrapperClassName?: string; className?: string }): React.JSX.Element — Renders inside the Canvas (needs the live camera via `useFrame`/`useThree`) but portals a real HTML readout via drei's `Html fullscreen` — a photo-mode "is this subject framed" HUD.
|
|
907
|
+
- frustumSampleDisplayEqual (function): function frustumSampleDisplayEqual(a: FrustumSample | null, b: FrustumSample | null): boolean
|
|
908
|
+
- useFrustumSensor (function): function useFrustumSensor(options: FrustumSensorProbeOptions): FrustumSample | null
|
|
649
909
|
|
|
650
910
|
### @jgengine/shell/vision/HiddenStateProbeHud
|
|
651
911
|
|
|
652
|
-
-
|
|
653
|
-
- SensorReadoutMeter (function): function SensorReadoutMeter({ label, reading, className }: SensorReadoutMeterProps): React.JSX.Element — A handheld-sensor readout: needle strength bar + the raw reading, or a "no signal" idle state.
|
|
912
|
+
- SensorReadoutMeter (function): function SensorReadoutMeter({ label, reading, className }: SensorReadoutMeterProps): React.JSX.Element — A handheld-sensor readout: needle strength bar + the raw reading, or a "no signal" idle state.
|
|
654
913
|
- SensorReadoutMeterProps (interface): interface SensorReadoutMeterProps
|
|
914
|
+
- useHiddenStateProbe (function): function useHiddenStateProbe(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): any — Reads a hidden zone/entity state variable in range (EMF / thermometer / geiger style sensor verb, #116).
|
|
655
915
|
|
|
656
916
|
### @jgengine/shell/vision/RevealVision
|
|
657
917
|
|
|
658
|
-
-
|
|
659
|
-
- RevealHighlights (function): function RevealHighlights(props: RevealHighlightsProps): React.JSX.Element | null — Screen-space reveal effect (#115) — highlights tagged entities through occluders (Dark Sight / detective-vision / wallhack style). Renders with `depthTest: false` so the highlight draws over any wall standing between the origin and the revealed entity, rather than the usual depth-sorted scene.
|
|
660
|
-
- RevealScreenTint (function): function RevealScreenTint({ enabled, color = "rgba(56, 189, 248, 0.16)", className }: RevealScreenTintProps): React.JSX.Element | null — Full-screen desaturating tint that reads as "vision mode is on" (Dark Sight / thermal / detective vision).
|
|
661
|
-
- RevealVisionOptions (interface): interface RevealVisionOptions
|
|
918
|
+
- RevealHighlights (function): function RevealHighlights(props: RevealHighlightsProps): React.JSX.Element | null — Screen-space reveal effect (#115) — highlights tagged entities through occluders (Dark Sight / detective-vision / wallhack style). Renders with `depthTest: false` so the highlight draws over any wall standing between the origin and the revealed entity, rather than the usual depth-sorted scene.
|
|
662
919
|
- RevealHighlightsProps (interface): interface RevealHighlightsProps extends RevealVisionOptions
|
|
920
|
+
- RevealScreenTint (function): function RevealScreenTint({ enabled, color = "rgba(56, 189, 248, 0.16)", className }: RevealScreenTintProps): React.JSX.Element | null — Full-screen desaturating tint that reads as "vision mode is on" (Dark Sight / thermal / detective vision).
|
|
663
921
|
- RevealScreenTintProps (interface): interface RevealScreenTintProps
|
|
922
|
+
- RevealVisionOptions (interface): interface RevealVisionOptions
|
|
923
|
+
- useRevealHits (function): function useRevealHits(options: RevealVisionOptions): readonly RevealHit[] — Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene.
|
|
924
|
+
|
|
925
|
+
### @jgengine/shell/vision/frustumSampleEqual
|
|
926
|
+
|
|
927
|
+
- frustumSampleDisplayEqual (function): function frustumSampleDisplayEqual(a: FrustumSample | null, b: FrustumSample | null): boolean
|
|
664
928
|
|
|
665
929
|
### @jgengine/shell/water
|
|
666
930
|
|
|
667
|
-
- Ocean (function): function Ocean({ config, ...meshProps }: OceanProps): React.JSX.Element
|
|
668
|
-
- OceanProps (interface): interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
669
931
|
- DEFAULT_OCEAN_CONFIG (const): const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig
|
|
932
|
+
- DEFAULT_OCEAN_WAVE_SCALE (const): const DEFAULT_OCEAN_WAVE_SCALE: 18 — Shared with `@jgengine/core/world/water` — primary wavelength in world units.
|
|
670
933
|
- MAX_OCEAN_WAVES (const): const MAX_OCEAN_WAVES: 6
|
|
671
934
|
- OCEAN_QUALITY_PRESETS (const): const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, { size: number; resolution: number }>
|
|
672
|
-
-
|
|
673
|
-
- createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
|
|
935
|
+
- Ocean (function): function Ocean({ config, ...meshProps }: OceanProps): React.JSX.Element
|
|
674
936
|
- OceanColorConfig (interface): interface OceanColorConfig
|
|
675
937
|
- OceanConfig (interface): interface OceanConfig
|
|
676
938
|
- OceanDirectionVector (interface): interface OceanDirectionVector
|
|
677
939
|
- OceanFoamConfig (interface): interface OceanFoamConfig
|
|
940
|
+
- OceanMaterialUniforms (interface): interface OceanMaterialUniforms
|
|
941
|
+
- OceanProps (interface): interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
678
942
|
- OceanQualityPreset (type): type OceanQualityPreset = "low" | "medium" | "high" | "ultra"
|
|
943
|
+
- OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
|
|
679
944
|
- OceanWaveConfig (interface): interface OceanWaveConfig
|
|
680
945
|
- OceanWaveDirection (type): type OceanWaveDirection = number | OceanDirectionVector
|
|
681
946
|
- ResolvedOceanColorConfig (interface): interface ResolvedOceanColorConfig
|
|
682
947
|
- ResolvedOceanConfig (interface): interface ResolvedOceanConfig
|
|
683
948
|
- ResolvedOceanFoamConfig (interface): interface ResolvedOceanFoamConfig
|
|
684
949
|
- ResolvedOceanWaveConfig (interface): interface ResolvedOceanWaveConfig
|
|
950
|
+
- buildOceanWaveUniforms (function): function buildOceanWaveUniforms(config: ResolvedOceanConfig): { directions: THREE.Vector2[]; params: THREE.Vector4[]; }
|
|
951
|
+
- createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
|
|
685
952
|
- createOceanMaterial (function): function createOceanMaterial(config: ResolvedOceanConfig): OceanShaderMaterial
|
|
686
953
|
- syncOceanMaterial (function): function syncOceanMaterial(material: OceanShaderMaterial, config: ResolvedOceanConfig, elapsedSeconds: number): void
|
|
687
|
-
- OceanMaterialUniforms (interface): interface OceanMaterialUniforms
|
|
688
|
-
- OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
|
|
689
954
|
|
|
690
|
-
### @jgengine/shell/water/
|
|
955
|
+
### @jgengine/shell/water/Ocean
|
|
691
956
|
|
|
692
957
|
- Ocean (function): function Ocean({ config, ...meshProps }: OceanProps): React.JSX.Element
|
|
693
958
|
- OceanProps (interface): interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
959
|
+
|
|
960
|
+
### @jgengine/shell/water/OceanConfig
|
|
961
|
+
|
|
694
962
|
- DEFAULT_OCEAN_CONFIG (const): const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig
|
|
963
|
+
- DEFAULT_OCEAN_WAVE_SCALE (const): const DEFAULT_OCEAN_WAVE_SCALE: 18 — Shared with `@jgengine/core/world/water` — primary wavelength in world units.
|
|
695
964
|
- MAX_OCEAN_WAVES (const): const MAX_OCEAN_WAVES: 6
|
|
696
965
|
- OCEAN_QUALITY_PRESETS (const): const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, { size: number; resolution: number }>
|
|
697
|
-
- buildOceanWaveUniforms (function): function buildOceanWaveUniforms(config: ResolvedOceanConfig): { directions: THREE.Vector2[]; params: THREE.Vector4[]; }
|
|
698
|
-
- createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
|
|
699
966
|
- OceanColorConfig (interface): interface OceanColorConfig
|
|
700
967
|
- OceanConfig (interface): interface OceanConfig
|
|
701
968
|
- OceanDirectionVector (interface): interface OceanDirectionVector
|
|
@@ -707,159 +974,243 @@ Imports use deep paths: `@jgengine/shell/<path>`.
|
|
|
707
974
|
- ResolvedOceanConfig (interface): interface ResolvedOceanConfig
|
|
708
975
|
- ResolvedOceanFoamConfig (interface): interface ResolvedOceanFoamConfig
|
|
709
976
|
- ResolvedOceanWaveConfig (interface): interface ResolvedOceanWaveConfig
|
|
710
|
-
-
|
|
711
|
-
-
|
|
977
|
+
- buildOceanWaveUniforms (function): function buildOceanWaveUniforms(config: ResolvedOceanConfig): { directions: THREE.Vector2[]; params: THREE.Vector4[]; }
|
|
978
|
+
- createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
|
|
979
|
+
|
|
980
|
+
### @jgengine/shell/water/OceanMaterial
|
|
981
|
+
|
|
712
982
|
- OceanMaterialUniforms (interface): interface OceanMaterialUniforms
|
|
713
983
|
- OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
|
|
984
|
+
- createOceanMaterial (function): function createOceanMaterial(config: ResolvedOceanConfig): OceanShaderMaterial
|
|
985
|
+
- syncOceanMaterial (function): function syncOceanMaterial(material: OceanShaderMaterial, config: ResolvedOceanConfig, elapsedSeconds: number): void
|
|
714
986
|
|
|
715
|
-
### @jgengine/shell/water/
|
|
987
|
+
### @jgengine/shell/water/OceanShader
|
|
716
988
|
|
|
717
|
-
-
|
|
718
|
-
-
|
|
989
|
+
- oceanFragmentShader (const): const oceanFragmentShader: "\nuniform vec3 uShallowColor;\nuniform vec3 uDeepColor;\nuniform vec3 uCrestColor;\nuniform vec3 uFoamColor;\nuniform float uOpacity;\nuniform float uFresnelStrength;\nuniform float uHorizonBlend;\nuniform float uFoamIntensity;\n\nvarying vec3 vWorldPosition;\nvarying vec…
|
|
990
|
+
- oceanVertexShader (const): const oceanVertexShader: "\nuniform float uTime;\nuniform vec2 uWaveDirections[6];\nuniform vec4 uWaveParams[6];\nuniform float uChoppiness;\nuniform float uFoamThreshold;\nuniform float uFoamSoftness;\nuniform float uFoamCoverage;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying floa…
|
|
719
991
|
|
|
720
|
-
### @jgengine/shell/water/
|
|
992
|
+
### @jgengine/shell/water/index
|
|
721
993
|
|
|
722
|
-
-
|
|
723
|
-
-
|
|
994
|
+
- DEFAULT_OCEAN_CONFIG (const): const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig
|
|
995
|
+
- DEFAULT_OCEAN_WAVE_SCALE (const): const DEFAULT_OCEAN_WAVE_SCALE: 18 — Shared with `@jgengine/core/world/water` — primary wavelength in world units.
|
|
724
996
|
- MAX_OCEAN_WAVES (const): const MAX_OCEAN_WAVES: 6
|
|
725
|
-
-
|
|
726
|
-
-
|
|
727
|
-
- OceanWaveDirection (type): type OceanWaveDirection = number | OceanDirectionVector
|
|
728
|
-
- OceanWaveConfig (interface): interface OceanWaveConfig
|
|
997
|
+
- OCEAN_QUALITY_PRESETS (const): const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, { size: number; resolution: number }>
|
|
998
|
+
- Ocean (function): function Ocean({ config, ...meshProps }: OceanProps): React.JSX.Element
|
|
729
999
|
- OceanColorConfig (interface): interface OceanColorConfig
|
|
730
|
-
- OceanFoamConfig (interface): interface OceanFoamConfig
|
|
731
1000
|
- OceanConfig (interface): interface OceanConfig
|
|
1001
|
+
- OceanDirectionVector (interface): interface OceanDirectionVector
|
|
1002
|
+
- OceanFoamConfig (interface): interface OceanFoamConfig
|
|
1003
|
+
- OceanMaterialUniforms (interface): interface OceanMaterialUniforms
|
|
1004
|
+
- OceanProps (interface): interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material">
|
|
1005
|
+
- OceanQualityPreset (type): type OceanQualityPreset = "low" | "medium" | "high" | "ultra"
|
|
1006
|
+
- OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
|
|
1007
|
+
- OceanWaveConfig (interface): interface OceanWaveConfig
|
|
1008
|
+
- OceanWaveDirection (type): type OceanWaveDirection = number | OceanDirectionVector
|
|
732
1009
|
- ResolvedOceanColorConfig (interface): interface ResolvedOceanColorConfig
|
|
1010
|
+
- ResolvedOceanConfig (interface): interface ResolvedOceanConfig
|
|
733
1011
|
- ResolvedOceanFoamConfig (interface): interface ResolvedOceanFoamConfig
|
|
734
1012
|
- ResolvedOceanWaveConfig (interface): interface ResolvedOceanWaveConfig
|
|
735
|
-
-
|
|
736
|
-
-
|
|
737
|
-
- DEFAULT_OCEAN_CONFIG (const): const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig
|
|
738
|
-
|
|
739
|
-
### @jgengine/shell/water/OceanMaterial
|
|
740
|
-
|
|
1013
|
+
- buildOceanWaveUniforms (function): function buildOceanWaveUniforms(config: ResolvedOceanConfig): { directions: THREE.Vector2[]; params: THREE.Vector4[]; }
|
|
1014
|
+
- createOceanConfig (function): function createOceanConfig(patch: OceanConfig = {}): ResolvedOceanConfig
|
|
741
1015
|
- createOceanMaterial (function): function createOceanMaterial(config: ResolvedOceanConfig): OceanShaderMaterial
|
|
742
1016
|
- syncOceanMaterial (function): function syncOceanMaterial(material: OceanShaderMaterial, config: ResolvedOceanConfig, elapsedSeconds: number): void
|
|
743
|
-
- OceanMaterialUniforms (interface): interface OceanMaterialUniforms
|
|
744
|
-
- OceanShaderMaterial (type): type OceanShaderMaterial = THREE.ShaderMaterial & { uniforms: OceanMaterialUniforms }
|
|
745
|
-
|
|
746
|
-
### @jgengine/shell/water/OceanShader
|
|
747
|
-
|
|
748
|
-
- oceanVertexShader (const): const oceanVertexShader: "\nuniform float uTime;\nuniform vec2 uWaveDirections[6];\nuniform vec4 uWaveParams[6];\nuniform float uChoppiness;\nuniform float uFoamThreshold;\nuniform float uFoamSoftness;\nuniform float uFoamCoverage;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying floa…
|
|
749
|
-
- oceanFragmentShader (const): const oceanFragmentShader: "\nuniform vec3 uShallowColor;\nuniform vec3 uDeepColor;\nuniform vec3 uCrestColor;\nuniform vec3 uFoamColor;\nuniform float uOpacity;\nuniform float uFresnelStrength;\nuniform float uHorizonBlend;\nuniform float uFoamIntensity;\n\nvarying vec3 vWorldPosition;\nvarying vec…
|
|
750
1017
|
|
|
751
1018
|
### @jgengine/shell/weather/FireSpreadLayer
|
|
752
1019
|
|
|
753
1020
|
- FireSpreadLayer (function): function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }: FireSpreadLayerProps): React.JSX.Element
|
|
754
1021
|
- FireSpreadLayerProps (interface): interface FireSpreadLayerProps
|
|
755
1022
|
|
|
756
|
-
### @jgengine/shell/weather/
|
|
1023
|
+
### @jgengine/shell/weather/LightningStrike
|
|
757
1024
|
|
|
758
|
-
- FireSpreadLayer (function): function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }: FireSpreadLayerProps): React.JSX.Element
|
|
759
|
-
- FireSpreadLayerProps (interface): interface FireSpreadLayerProps
|
|
760
1025
|
- LightningStrike (function): function LightningStrike({ origin, target, strikeKey = 0, seed = 451, visible = true, duration = 0.18, color = DEFAULT_COLOR, glow = 2.4, branches = 5, jaggedness = 0.08, impactLight = 26, renderOrder = 20, }: LightningStrikeProps): React.JSX.Element
|
|
761
1026
|
- LightningStrikeProps (interface): interface LightningStrikeProps
|
|
762
|
-
|
|
1027
|
+
|
|
1028
|
+
### @jgengine/shell/weather/RainField
|
|
1029
|
+
|
|
1030
|
+
- RainField (function): function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 22, length = 1.35, width = 0.018, opacity = 0.48, color = DEFAULT_RAIN_COLOR, lightning, timeScale, seed = 11939, renderOrder …
|
|
763
1031
|
- RainFieldProps (interface): interface RainFieldProps
|
|
764
|
-
|
|
1032
|
+
|
|
1033
|
+
### @jgengine/shell/weather/SnowField
|
|
1034
|
+
|
|
1035
|
+
- SnowField (function): function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 3.2, size = 0.11, sway = 0.62, opacity = 0.86, color = DEFAULT_SNOW_COLOR, timeScale, seed = 72931, renderOrder = 11, frustumC…
|
|
765
1036
|
- SnowFieldProps (interface): interface SnowFieldProps
|
|
1037
|
+
|
|
1038
|
+
### @jgengine/shell/weather/WeatherLayer
|
|
1039
|
+
|
|
766
1040
|
- WeatherLayer (function): function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, rain, snow, enabled = true, children, }: WeatherLayerProps): React.JSX.Element | null
|
|
767
1041
|
- WeatherLayerMode (type): type WeatherLayerMode = "clear" | "rain" | "snow" | "mixed"
|
|
768
1042
|
- WeatherLayerProps (interface): interface WeatherLayerProps
|
|
769
|
-
- createWeatherUniformSet (function): function createWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
|
|
770
|
-
- WeatherUniformProvider (function): function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & { children: ReactNode }): React.JSX.Element
|
|
771
|
-
- useWeatherUniformSet (function): function useWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
|
|
772
|
-
- WeatherUniformOptions (interface): interface WeatherUniformOptions
|
|
773
|
-
- WeatherUniformSet (interface): interface WeatherUniformSet
|
|
774
|
-
- WeatherVector (type): type WeatherVector = readonly [number, number, number]
|
|
775
1043
|
|
|
776
|
-
### @jgengine/shell/weather/
|
|
1044
|
+
### @jgengine/shell/weather/fireSpreadPose
|
|
777
1045
|
|
|
778
|
-
-
|
|
779
|
-
- LightningStrikeProps (interface): interface LightningStrikeProps
|
|
1046
|
+
- setBillboardQuaternion (function): function setBillboardQuaternion(quaternion: Quaternion, euler: Euler, yaw: number): void
|
|
780
1047
|
|
|
781
|
-
### @jgengine/shell/weather/
|
|
1048
|
+
### @jgengine/shell/weather/index
|
|
782
1049
|
|
|
783
|
-
-
|
|
1050
|
+
- FireSpreadLayer (function): function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }: FireSpreadLayerProps): React.JSX.Element
|
|
1051
|
+
- FireSpreadLayerProps (interface): interface FireSpreadLayerProps
|
|
1052
|
+
- LightningStrike (function): function LightningStrike({ origin, target, strikeKey = 0, seed = 451, visible = true, duration = 0.18, color = DEFAULT_COLOR, glow = 2.4, branches = 5, jaggedness = 0.08, impactLight = 26, renderOrder = 20, }: LightningStrikeProps): React.JSX.Element
|
|
1053
|
+
- LightningStrikeProps (interface): interface LightningStrikeProps
|
|
1054
|
+
- RainField (function): function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 22, length = 1.35, width = 0.018, opacity = 0.48, color = DEFAULT_RAIN_COLOR, lightning, timeScale, seed = 11939, renderOrder …
|
|
784
1055
|
- RainFieldProps (interface): interface RainFieldProps
|
|
785
|
-
|
|
786
|
-
### @jgengine/shell/weather/SnowField
|
|
787
|
-
|
|
788
|
-
- SnowField (function): function SnowField({ count = 6000, density = 0.5, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 3.2, size = 0.11, sway = 0.62, opacity = 0.86, color = DEFAULT_SNOW_COLOR, timeScale, seed = 72931, renderOrder = 11, }: SnowFieldProps): React.JSX.Element
|
|
1056
|
+
- SnowField (function): function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 3.2, size = 0.11, sway = 0.62, opacity = 0.86, color = DEFAULT_SNOW_COLOR, timeScale, seed = 72931, renderOrder = 11, frustumC…
|
|
789
1057
|
- SnowFieldProps (interface): interface SnowFieldProps
|
|
1058
|
+
- WeatherLayer (function): function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, rain, snow, enabled = true, children, }: WeatherLayerProps): React.JSX.Element | null
|
|
1059
|
+
- WeatherLayerMode (type): type WeatherLayerMode = "clear" | "rain" | "snow" | "mixed"
|
|
1060
|
+
- WeatherLayerProps (interface): interface WeatherLayerProps
|
|
1061
|
+
- WeatherUniformOptions (interface): interface WeatherUniformOptions
|
|
1062
|
+
- WeatherUniformProvider (function): function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & { children: ReactNode }): React.JSX.Element
|
|
1063
|
+
- WeatherUniformSet (interface): interface WeatherUniformSet
|
|
1064
|
+
- WeatherVector (type): type WeatherVector = readonly [number, number, number]
|
|
1065
|
+
- createWeatherUniformSet (function): function createWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
|
|
1066
|
+
- useWeatherUniformSet (function): function useWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
|
|
790
1067
|
|
|
791
1068
|
### @jgengine/shell/weather/weatherGeometry
|
|
792
1069
|
|
|
793
1070
|
- createWeatherQuadGeometry (function): function createWeatherQuadGeometry(maxCount: number, seed: number): THREE.InstancedBufferGeometry
|
|
794
1071
|
|
|
795
|
-
### @jgengine/shell/weather/WeatherLayer
|
|
796
|
-
|
|
797
|
-
- WeatherLayer (function): function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, rain, snow, enabled = true, children, }: WeatherLayerProps): React.JSX.Element | null
|
|
798
|
-
- WeatherLayerMode (type): type WeatherLayerMode = "clear" | "rain" | "snow" | "mixed"
|
|
799
|
-
- WeatherLayerProps (interface): interface WeatherLayerProps
|
|
800
|
-
|
|
801
1072
|
### @jgengine/shell/weather/weatherMath
|
|
802
1073
|
|
|
1074
|
+
- DEFAULT_RAIN_COUNT (const): const DEFAULT_RAIN_COUNT: 2000
|
|
1075
|
+
- DEFAULT_RAIN_DENSITY (const): const DEFAULT_RAIN_DENSITY: 0.45
|
|
1076
|
+
- DEFAULT_SNOW_COUNT (const): const DEFAULT_SNOW_COUNT: 1500
|
|
1077
|
+
- DEFAULT_SNOW_DENSITY (const): const DEFAULT_SNOW_DENSITY: 0.5
|
|
1078
|
+
- WeatherSeedAttributes (interface): interface WeatherSeedAttributes
|
|
803
1079
|
- clampWeatherRatio (function): function clampWeatherRatio(value: number): number
|
|
804
|
-
- resolveWeatherInstanceCount (function): function resolveWeatherInstanceCount(maxCount: number, density: number): number
|
|
805
1080
|
- createWeatherSeedAttributes (function): function createWeatherSeedAttributes(maxCount: number, seed: number): WeatherSeedAttributes
|
|
806
|
-
-
|
|
1081
|
+
- resolveWeatherInstanceCount (function): function resolveWeatherInstanceCount(maxCount: number, density: number, budget?: number): number
|
|
807
1082
|
|
|
808
1083
|
### @jgengine/shell/weather/weatherUniforms
|
|
809
1084
|
|
|
810
|
-
-
|
|
1085
|
+
- WeatherUniformOptions (interface): interface WeatherUniformOptions
|
|
811
1086
|
- WeatherUniformProvider (function): function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & { children: ReactNode }): React.JSX.Element
|
|
812
|
-
- useWeatherUniformSet (function): function useWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
|
|
813
|
-
- WeatherVector (type): type WeatherVector = readonly [number, number, number]
|
|
814
1087
|
- WeatherUniformSet (interface): interface WeatherUniformSet
|
|
815
|
-
-
|
|
1088
|
+
- WeatherVector (type): type WeatherVector = readonly [number, number, number]
|
|
1089
|
+
- createWeatherUniformSet (function): function createWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
|
|
1090
|
+
- useWeatherUniformSet (function): function useWeatherUniformSet(options: WeatherUniformOptions = {}): WeatherUniformSet
|
|
816
1091
|
|
|
817
1092
|
### @jgengine/shell/world/DataObjects
|
|
818
1093
|
|
|
819
|
-
- DataObjects (function): function DataObjects<T>({ data, position, height, color, cellSize = 0.28, hovered = null, hoverColor = "#ffffff", onHover, grow, castShadow = true, receiveShadow = true, renderItem, }: DataObjectsProps<T>): React.JSX.Element
|
|
820
|
-
- DataObjectsProps (interface): interface DataObjectsProps<T>
|
|
821
|
-
|
|
822
|
-
### @jgengine/shell/world/floatTextStyle
|
|
823
|
-
|
|
824
|
-
- resolveFloatTextStyle (function): function resolveFloatTextStyle(info: FloatTextInfo): FloatTextStyle
|
|
825
|
-
- FloatTextInfo (interface): interface FloatTextInfo
|
|
826
|
-
- FloatTextStyle (interface): interface FloatTextStyle
|
|
1094
|
+
- DataObjects (function): function DataObjects<T>({ data, position, height, color, cellSize = 0.28, hovered = null, hoverColor = "#ffffff", onHover, grow, castShadow = true, receiveShadow = true, renderItem, }: DataObjectsProps<T>): React.JSX.Element | null
|
|
1095
|
+
- DataObjectsProps (interface): interface DataObjectsProps<T> — Renders one placed 3D object per data item — a 3D bar chart, a heatmap, a city of buildings, a crowd. By default each item is an extruded box (all boxes share one `InstancedMesh`, so hundreds cost one draw call), sized and colored from the item; `renderItem` swaps the box for arbitrary content (a sprite, a GLB, a full entity). Genre-agnostic: the caller owns the data `T`, this owns the placement.
|
|
827
1096
|
|
|
828
1097
|
### @jgengine/shell/world/GridWorldScene
|
|
829
1098
|
|
|
830
|
-
- GridWorldScene (function): function GridWorldScene({ feature }: GridWorldSceneProps): React.JSX.Element | null
|
|
1099
|
+
- GridWorldScene (function): function GridWorldScene({ feature }: GridWorldSceneProps): React.JSX.Element | null — Data-driven renderer for the `biomes()`/`voxel()`/`plots()`/`tilemap()` world-feature kinds (#207.1): one `InstancedMesh` of extruded, colored boxes built from each feature's declared `cells`, following the same direct-buffer pattern as `InstancedBodies`.
|
|
831
1100
|
- GridWorldSceneProps (interface): interface GridWorldSceneProps
|
|
832
1101
|
|
|
833
1102
|
### @jgengine/shell/world/InstancedBodies
|
|
834
1103
|
|
|
835
|
-
- InstancedBodies (function): function InstancedBodies({ world, debugTint = false, baseColors, epoch = 0 }: InstancedBodiesProps): React.JSX.Element
|
|
1104
|
+
- InstancedBodies (function): function InstancedBodies({ world, debugTint = false, baseColors, epoch = 0 }: InstancedBodiesProps): React.JSX.Element — Renders a PhysicsWorld's box bodies as a single InstancedMesh — one draw call per batch. Transforms are written directly into `instanceMatrix.array` each frame (bodies never touch the per-entity React path). Reusable by any game: hand it a physics world and go.
|
|
836
1105
|
- InstancedBodiesProps (interface): interface InstancedBodiesProps
|
|
837
1106
|
|
|
838
1107
|
### @jgengine/shell/world/InstancedJoints
|
|
839
1108
|
|
|
840
|
-
- InstancedJoints (function): function InstancedJoints({ world, color = "#f5c542" }: InstancedJointsProps): React.JSX.Element
|
|
1109
|
+
- InstancedJoints (function): function InstancedJoints({ world, color = "#f5c542" }: InstancedJointsProps): React.JSX.Element — Debug overlay drawing a PhysicsWorld's joints (suspension, ragdoll links, carry tethers) as one LineSegments batch. Endpoints are streamed each frame from `world.readJointSegments`; pair with `InstancedBodies` to see the constraint structure over the bodies.
|
|
841
1110
|
- InstancedJointsProps (interface): interface InstancedJointsProps
|
|
842
1111
|
|
|
1112
|
+
### @jgengine/shell/world/SpriteBatch
|
|
1113
|
+
|
|
1114
|
+
- SpriteBatch (function): function SpriteBatch({ url, columns = 1, rows = 1, capacity = DEFAULT_CAPACITY, instances, plane = "xy", billboard = false, pixelated = true, alphaTest = DEFAULT_ALPHA_TEST, opacity = 1, }: SpriteBatchProps): React.JSX.Element — Renders a sprite sheet / tile atlas as a single InstancedMesh — one draw call for the whole batch. Each instance picks its atlas frame via a per-instance UV offset attribute patched into the material's vertex shader, so platformer/puzzle-grid presentation never needs one draw call per sprite. Transforms and UV offsets are written directly into the mesh's typed arrays each frame from a plain instance list (bodies never touch the per-entity React path).
|
|
1115
|
+
- SpriteBatchInstance (interface): interface SpriteBatchInstance
|
|
1116
|
+
- SpriteBatchProps (interface): interface SpriteBatchProps
|
|
1117
|
+
|
|
843
1118
|
### @jgengine/shell/world/WorldHud
|
|
844
1119
|
|
|
845
|
-
- WorldEntityBars (function): function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, }: { statId: string; height?: number; roles?: readonly CatalogEntityRole[]; resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined; }): React.JSX.Element
|
|
846
|
-
- WorldFloatText (function): function WorldFloatText({ height = 1.9, lifeMs = 950 }: { height?: number; lifeMs?: number }): React.JSX.Element
|
|
847
|
-
- WorldTelegraphs (function): function WorldTelegraphs(): React.JSX.Element
|
|
848
1120
|
- CombatCameraShake (function): function CombatCameraShake(): null
|
|
849
1121
|
- ProjectileTracers (function): function ProjectileTracers({ lifeMs = 130 }: { lifeMs?: number }): React.JSX.Element
|
|
850
1122
|
- Reticle (function): function Reticle({ className }: { className?: string }): React.JSX.Element
|
|
1123
|
+
- WorldBarSample (interface): interface WorldBarSample
|
|
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…
|
|
1125
|
+
- WorldFloatText (function): function WorldFloatText({ height = 1.9, lifeMs = 950 }: { height?: number; lifeMs?: number }): React.JSX.Element
|
|
1126
|
+
- WorldTelegraphs (function): function WorldTelegraphs(): React.JSX.Element
|
|
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:…
|
|
1128
|
+
- paintWorldBarSamples (function): function paintWorldBarSamples(canvas: { width: number; height: number; getContext(kind: "2d"): CanvasRenderingContext2D | null }, samples: readonly WorldBarSample[], dpr: number, barWidthPx = 112, barHeightPx = 10): void
|
|
1129
|
+
- telegraphPulseOpacity (function): function telegraphPulseOpacity(bornMs: number, windupMs: number, nowMs: number): number
|
|
851
1130
|
|
|
852
1131
|
### @jgengine/shell/world/WorldItems
|
|
853
1132
|
|
|
854
|
-
- WorldItems (function): function WorldItems({ config }: { config?: WorldItemRenderConfig }): React.JSX.Element
|
|
1133
|
+
- WorldItems (function): function WorldItems({ config }: { config?: WorldItemRenderConfig }): React.JSX.Element — Rarity→beam/color/label render binding + loot-filter overlay (#32/#33) for every dropped `worldItem`.
|
|
1134
|
+
|
|
1135
|
+
### @jgengine/shell/world/entityPose
|
|
1136
|
+
|
|
1137
|
+
- PoseSource (interface): interface PoseSource
|
|
1138
|
+
- PoseWritable (interface): interface PoseWritable
|
|
1139
|
+
- posesEqual (function): function posesEqual(a: PoseSource, b: PoseSource): boolean
|
|
1140
|
+
- writeEntityPose (function): function writeEntityPose(target: PoseWritable, source: PoseSource): void
|
|
1141
|
+
|
|
1142
|
+
### @jgengine/shell/world/floatTextStyle
|
|
1143
|
+
|
|
1144
|
+
- FloatTextInfo (interface): interface FloatTextInfo
|
|
1145
|
+
- FloatTextStyle (interface): interface FloatTextStyle
|
|
1146
|
+
- resolveFloatTextStyle (function): function resolveFloatTextStyle(info: FloatTextInfo): FloatTextStyle
|
|
1147
|
+
|
|
1148
|
+
### @jgengine/shell/world/telegraphPulse
|
|
1149
|
+
|
|
1150
|
+
- telegraphPulseOpacity (function): function telegraphPulseOpacity(bornMs: number, windupMs: number, nowMs: number): number
|
|
1151
|
+
|
|
1152
|
+
### @jgengine/shell/world/worldBarSamples
|
|
1153
|
+
|
|
1154
|
+
- Projectable (interface): interface Projectable
|
|
1155
|
+
- WorldBarSample (interface): interface WorldBarSample
|
|
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:…
|
|
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
|
|
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"`.
|
|
855
1162
|
|
|
856
1163
|
## Guides
|
|
857
1164
|
|
|
858
|
-
# JGengine
|
|
1165
|
+
# JGengine
|
|
1166
|
+
|
|
1167
|
+
JGengine is a pure-TypeScript game-engine SDK for games built by AI coding agents. Its skills teach the agent how to use the SDK. When a user says “make X with JGengine” or “make X with jgengine.com,” treat that as sufficient intake. If the skills are not installed, run `npx jgengine skills`; then use this skill and build on the SDK. Do not make the user discover or name skills.
|
|
1168
|
+
|
|
1169
|
+
## Intake
|
|
1170
|
+
|
|
1171
|
+
State the reading as a short numbered list that is easy to correct, then proceed unless the user changes it. Prefer concrete values and actions over prose:
|
|
1172
|
+
|
|
1173
|
+
1. **POV:** first-person
|
|
1174
|
+
2. **World:** custom 3D wasteland with three settlements
|
|
1175
|
+
3. **Core loop:** get quests by talking to people → defeat enemies → return to redeem quests
|
|
1176
|
+
4. **Interaction:** collect ground items by walking over them; interact with people and doors at close range
|
|
1177
|
+
5. **Combat:** ranged weapons, damage, death, loot
|
|
1178
|
+
6. **Progression:** inventory, currency, quest rewards, upgrades
|
|
1179
|
+
7. **Players:** single-player, or name the multiplayer topology and synchronized systems
|
|
1180
|
+
8. **UI:** visible controls, objective tracker, health, inventory feedback
|
|
1181
|
+
9. **Art direction:** one aesthetic, palette, asset family, and UI voice
|
|
1182
|
+
10. **Done looks like:** one observable end-to-end play scenario
|
|
1183
|
+
|
|
1184
|
+
Keep this compact—roughly one line per item. It is a build map, not a large specification or an approval gate. Infer conventional details from the named game or genre. Ask only when two plausible readings would fundamentally change the game.
|
|
1185
|
+
|
|
1186
|
+
## Route selectively
|
|
859
1187
|
|
|
860
|
-
|
|
1188
|
+
This skill is the foundation for every task (packages, project shape, defineGame, context, catalogs). After intake, also read only the domain skills the work needs:
|
|
861
1189
|
|
|
862
|
-
|
|
1190
|
+
| Need | Read |
|
|
1191
|
+
| --- | --- |
|
|
1192
|
+
| Terrain, scenes, camera, movement, physics, maps, sensors | `jgengine-world` |
|
|
1193
|
+
| Seeded generation, terrain/environment generation, grids, buildings, simulation | `jgengine-procedural` |
|
|
1194
|
+
| Damage, effects, weapons, targeting, projectiles, loot, death | `jgengine-combat` |
|
|
1195
|
+
| Items, quests, dialogue, economy, crafting, objectives, turns, social systems | `jgengine-gameplay` |
|
|
1196
|
+
| Networking adapters, authority, rooms/topology, persistence/backend seams | `jgengine-multiplayer` |
|
|
1197
|
+
| React HUD, shell affordances, controls, feedback, accessibility | `jgengine-ui` |
|
|
1198
|
+
| Models, sprites, textures, audio, catalogs, attribution | `jgengine-assets` |
|
|
1199
|
+
| Proof and screenshots | `jgengine-verify` after implementation |
|
|
1200
|
+
|
|
1201
|
+
Do not read every domain by default. Build through documented engine surfaces; do not infer APIs from gallery games. Inspect engine source only when a documented surface appears wrong or a missing primitive blocks the work.
|
|
1202
|
+
|
|
1203
|
+
## Build behavior
|
|
1204
|
+
|
|
1205
|
+
Scaffold with `npx jgengine create game-name --name "Game Name"` when needed. Build the requested game continuously from the intake, keeping systems end-to-end rather than leaving registered-but-unusable pieces. Use real assets and visible feedback early. Verify at completion with `jgengine-verify`.
|
|
1206
|
+
|
|
1207
|
+
Cartridge-shaped games (declarative config, engine-owned loop): see [reference-cartridge.md](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine/reference-cartridge.md).
|
|
1208
|
+
|
|
1209
|
+
---
|
|
1210
|
+
|
|
1211
|
+
The engine ships **verbs and primitives**; your game ships **nouns** (catalogs) and thin handlers. This skill is that foundation plus intake. Use domain skills only when needed; use `jgengine-verify` afterward. UI guidance lives in [`../jgengine-ui/reference.md`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md); assets live in `jgengine-assets`.
|
|
1212
|
+
|
|
1213
|
+
Load detailed references only for the selected domain: [`jgengine-combat`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-combat/reference.md), [`jgengine-world`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-world/reference.md), [`jgengine-multiplayer`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-multiplayer/reference.md), and [`jgengine-ui`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md). Each domain skill explains when its reference is needed.
|
|
863
1214
|
|
|
864
1215
|
## Packages
|
|
865
1216
|
|
|
@@ -893,7 +1244,7 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
893
1244
|
|---------|----------------------------------|-----------|
|
|
894
1245
|
| Game boot | `game/defineGame` | `defineGame`, `GameDefinition`, `GameLoop`, `InventoryDeclaration`, `PhysicsConfig`, `GameServerConfig`, `TimeConfig` |
|
|
895
1246
|
| Simulation clock | `time/simClock` | `createSimClock`, `SimClock`, `TimeConfig`, `ClockSnapshot`, `CalendarTime` |
|
|
896
|
-
| Runner contract | `game/playableGame` | `PlayableGame`, `GameCameraConfig`, `CameraRigKind`, `TopDownCameraConfig`, `RtsCameraConfig`, `ShoulderCameraConfig`, `LockOnCameraConfig`, `ChaseCameraConfig`, `ObserverCameraConfig`, `CameraShakeConfig`, `CinematicCameraConfig`, `CameraKeyframe`, `EntitySpriteConfig` |
|
|
1247
|
+
| Runner contract | `game/playableGame` | `PlayableGame`, `GameCameraConfig`, `CameraRigKind`, `CameraProjection`, `SideScrollCameraConfig`, `TopDownCameraConfig`, `RtsCameraConfig`, `ShoulderCameraConfig`, `LockOnCameraConfig`, `ChaseCameraConfig`, `ObserverCameraConfig`, `CameraShakeConfig`, `CinematicCameraConfig`, `CameraKeyframe`, `EntitySpriteConfig` |
|
|
897
1248
|
| Runtime ctx | `runtime/gameContext` | `createGameContext`, `GameContext`, `GameContextContent`, `GameContextItemEntry`, `GameContextEntityEntry`, `GameContextObjectEntry`, `CatalogEntityRole` |
|
|
898
1249
|
| Behaviour lifecycle | `behaviour/behaviour` | `Behaviour` (`onAwake`→`onEnable`→`onStart`→`onUpdate(dt)`→`onDisable`→`onDestroy`), `BehaviourModule`, `createBehaviourWorld`, `BehaviourWorld`, `JGEngineRegister`, `RegisterField`, `BehaviourModules` — Unity-style lifecycle over an id-keyed node tree (`setActive` cascade, lazy update dispatch); key nodes by entity instance ids. Games augment `JGEngineRegister` via `declare module "@jgengine/core/behaviour/behaviour"` for typed `world.modules`. Three.js binding: `Object3DBehaviour`, `attachObject3D`, `useBehaviourWorld` from `@jgengine/shell/behaviour` |
|
|
899
1250
|
| Reactive keyed store | `store/observableKeyedStore` | `createObservableKeyedStore`, `ObservableKeyedStore` — backs `ctx.game.store` |
|
|
@@ -909,10 +1260,13 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
909
1260
|
| Loadout | `game/loadout` | `LoadoutDef`, `LoadoutItemEntry`, `Loadouts` |
|
|
910
1261
|
| Cosmetic loadout | `game/cosmetics` | `createCosmetics`, `Cosmetics`, `CosmeticLoadoutDef` |
|
|
911
1262
|
| Quest | `game/quest` | `QuestDef`, `QuestRewards`, `QuestObjective`, `QuestJournal` |
|
|
912
|
-
| World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building` |
|
|
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 |
|
|
913
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 |
|
|
914
1267
|
| Terrain field | `world/terrain` | `TerrainField`, `noiseField`, `resolveTerrainField`, `rollingField`, `fractalNoise`, `valueNoise`, `withNormal`, `arenaField`, `flatField`, `resolveGroundStep`, `snapToGround`, `snapEntityToGround`, `resolveTerrainPalette`, `TERRAIN_MATERIAL_PALETTES` |
|
|
915
1268
|
| Seeded RNG | `random/rng` | `seededRng`, `seededStreams` |
|
|
1269
|
+
| Seed share link | `random/seedLink` | `withSeedParam`, `seedFromUrl`, `seedFromSearch`, `dailySeed`, `DEFAULT_SEED_PARAM` — encode/decode a world seed to/from a shareable URL query param; `dailySeed` is the UTC daily-run seed |
|
|
916
1270
|
| Name generator | `random/nameGen` | `createNameGenerator`, `pickFrom`, `fillTemplate`, `NameGenerator`, `NameGeneratorOptions`, `SyllableBank` |
|
|
917
1271
|
| Regions | `world/regions` | `createRegionField`, `isRegionField`, `RegionDef`, `RegionField`, `RegionSample` |
|
|
918
1272
|
| Wind field | `world/wind` | `windField`, `WindField`, `WindFieldConfig`, `WindVector` |
|
|
@@ -943,6 +1297,7 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
943
1297
|
| Persistence scopes | `runtime/persistenceScope` | `partitionScopes`, `resetRun`, `mergeScopes`, `clearRunFields`, `applyRunReset`, `planScenarioReset`, `ScopeSchema`, `ScenarioReset`, `PersistenceScope` |
|
|
944
1298
|
| Inventory | `inventory/inventoryModel` | `InventoryLayout`, `InventorySet`, `ItemTraits` |
|
|
945
1299
|
| Progression | `game/progression` | `curve`, `evalCurve`, `leveling`, `Curve`, `LevelingTrack`, `LevelProgress` |
|
|
1300
|
+
| Talent tree | `game/talents` | `createTalentTree`, `TalentTree`, `TalentTreeConfig`, `TalentNodeDef`, `TalentRequirement`, `TalentAllocateResult`, `ResolvedTalents`, `TalentSnapshot` — point spends gated by prereqs + points-in-branch, resolved once into a cached flat `StatModifierSet` + ability grants |
|
|
946
1301
|
| Inventory slots | `inventory/slotModel` | `createSlots`, `placeAt`, `removeAt`, `moveSlot`, `firstEmpty`, `compactSlots`, `Slot`, `SlotGrid` |
|
|
947
1302
|
| Shaped inventory | `inventory/shapedGrid` | `createShapedGrid`, `placeShaped`, `moveShaped`, `removeShaped`, `canPlace`, `rotateFootprint`, `occupiedCells`, `gridAdjacencyQuery`, `cellFromPoint`, `ShapedGrid`, `Footprint`, `Placement`, `Rotation` |
|
|
948
1303
|
| Card piles | `cards/cardPile` | `createCardPile`, `createCardPileState`, `draw`, `moveCards`, `shuffleZone`, `pileRng`, `CardPile`, `CardPileState`, `CardPileConfig` |
|
|
@@ -960,6 +1315,7 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
960
1315
|
| Build permissions | `world/buildPermissions` | `createPlotPermissions`, `createContributionPool`, `PlotPermissions`, `ContributionPool`, `BuildRole`, `ContributionGoal` |
|
|
961
1316
|
| Interiors | `world/interiors` | `createInteriors`, `Interior`, `Exterior`, `SpaceRef` |
|
|
962
1317
|
| Game clock | `time/gameClock` | `getScaledElapsedMs`, `computeGameDay`, `SECONDS_PER_GAME_DAY` |
|
|
1318
|
+
| Idle / offline catch-up | `time/idleProgress` | `idleWindow`, `linearCatchUp`, `exponentialCatchUp`, `steppedCatchUp`, `IdleWindow`, `IdleWindowConfig`, `SteppedCatchUpResult` — elapsed-real-time production/growth/decay for a game reopened after being closed |
|
|
963
1319
|
| Scene behaviors | `scene/behaviors` | `wander`, `patrol`, `promptable`, `talkable`, `player` |
|
|
964
1320
|
| Capture check | `scene/captureCheck` | `captureChance`, `rollCapture`, `CaptureCheckInput` |
|
|
965
1321
|
| Owned roster | `scene/roster` | `createRoster`, `Roster`, `RosterEntry`, `RosterCaptureOptions` |
|
|
@@ -988,6 +1344,7 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
988
1344
|
| JSON data source | `data/jsonDataSource` | `createJsonDataSource`, `JsonDataSourceOptions` |
|
|
989
1345
|
| Dev proxy routing | `data/devProxy` | `proxiedUrl`, `parseDevProxyTable`, `DevProxyTable`, `ProxiedUrlOptions`, `DEFAULT_DEV_PROXY_PREFIX` |
|
|
990
1346
|
| Grid-cell world rendering | `world/gridInstances` | `resolveGridInstances`, `GridInstanceTransform` |
|
|
1347
|
+
| Swarm LOD scheduler | `world/lod` | `createLodScheduler`, `LodScheduler`, `LodSchedulerConfig`, `LodBand` — distance→band index for render detail, `step(id, distance, dt)` throttles per-entity updates by band interval (staggered, accumulates skipped time); pairs with `@jgengine/shell/world/SpriteBatch` for 1000+ entity swarms |
|
|
991
1348
|
| Turn loop | `turn/turnLoop` | `createTurnLoop`, `TurnLoop`, `TurnLoopConfig`, `TurnState`, `PoolConfig`, `PoolState`, `TurnLoopSnapshot` |
|
|
992
1349
|
| Declared-action intent board | `turn/intent` | `createIntentBoard`, `IntentBoard`, `DeclaredIntent` — `declare(participantId, intent)`, `peek`, `all`, `consume`, `clear` |
|
|
993
1350
|
| Commit modes | `turn/commit` | `createCommitController`, `CommitController`, `CommitMode`, `CommitOutcome`, `SubmittedAction` |
|
|
@@ -1007,8 +1364,10 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
1007
1364
|
| Beat clock | `time/beatClock` | `createBeatClock`, `createBeatInputBuffer`, `nextBeatTime`, `BeatClock`, `BeatClockConfig`, `BeatSnapshot`, `BeatInputBuffer`, `BufferedAction` |
|
|
1008
1365
|
| Spawn director | `ai/spawnDirector` | `createSpawnDirectorState`, `advanceSpawnDirector`, `advanceWave`, `raiseAlert`, `pickSpawnPoint`, `SpawnDirectorConfig`, `WaveManifest`, `SpawnEntry`, `SpawnRequest`, `DirectorContext` |
|
|
1009
1366
|
| Threat table | `ai/threat` | `createThreatTable`, `ThreatTable`, `ThreatTableConfig`, `ThreatEntry`, `HighestThreatOptions` |
|
|
1367
|
+
| Group-assist aggro | `ai/groupAssist` | `createAssistNetwork`, `AssistNetwork`, `AssistNetworkConfig`, `AssistMember` — propagates one member's threat gains to same-group members (optional radius + `distanceBetween` gating) so a single pull rallies the group |
|
|
1010
1368
|
| Job board | `ai/jobBoard` | `createJobBoard`, `JobBoard`, `JobDef`, `Job`, `JobPhase`, `WorkerState`, `JobReport`, `JobTickContext` |
|
|
1011
1369
|
| Crowd flow | `ai/crowd` | `computeFlowField`, `createCrowdField`, `selectPoi`, `FlowField`, `FlowFieldOptions`, `CrowdField`, `Poi`, `SelectPoiOptions` |
|
|
1370
|
+
| Factions & reputation | `faction/factions`, `faction/reputation` | `createFactionGraph`, `createFactionRoster`, `FactionRelation`, `FactionDef`, `FactionGraph`, `FactionRoster`, `createReputationLedger`, `DEFAULT_REPUTATION_TIERS`, `tierForStanding`, `effectiveRelation`, `ReputationTier`, `ReputationLedger` |
|
|
1012
1371
|
| Physics actors | `physics/ragdoll`, `physics/carryable`, `physics/forceVolume`, `physics/spatialGrid` | `createRagdoll`, `Ragdoll`, `Carryable`, `carrySpeedMultiplier`, `ForceVolume`, `PlatformCarry`, `SpatialGrid` |
|
|
1013
1372
|
| Traversal (grapple/glide) | `physics/traversal` | `Grapple`, `GrappleConfig`, `Glide`, `GlideConfig` |
|
|
1014
1373
|
| Structural destruction | `physics/structure` | `StructureGraph`, `StructureNodeSpec`, `StructureEdgeSpec`, `StructureMaterial`, `StructureMaterialTable`, `CollapseEvent`, `DebrisConfig` |
|
|
@@ -1024,6 +1383,7 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
1024
1383
|
| Session matchmaking | `multiplayer/matchmaking` | `browseSessions`, `findByJoinCode`, `quickMatch`, `matchesFilter`, `normalizeJoinCode`, `generateJoinCode`, `SessionListing`, `MatchFilter` |
|
|
1025
1384
|
| Auth identity | `multiplayer/identity` | `AuthSession`, `PlayerIdentity`, `sessionPlayer`, `resolveGuestSession` |
|
|
1026
1385
|
| Text chat | `game/chat`, `multiplayer/chatContract` | `createChat`, `Chat`, `ChatMessage`, `ChatChannelDef`, `whisperChannelId`, `createChatRateLimiter`, `ChatTransport`, `ChatSync`, `createLocalChatTransport` |
|
|
1386
|
+
| Chat filter | `game/chatFilter` | `createChatFilter`, `normalizeChatText`, `ChatFilter`, `ChatFilterConfig`, `ChatFilterResult` — mask/reject blocked words (leet-normalized token match); wire via `ChatDeps.filter` (word lists are game data, the engine ships the mechanism) |
|
|
1027
1387
|
| Voice seam | `multiplayer/voiceContract` | `VoiceTransport`, `VoiceParticipant`, `VoiceRoute`, `createLocalVoiceTransport`, `createPushToTalk`, `PushToTalkMode` |
|
|
1028
1388
|
| Race state | `game/race` | `raceTrack`, `RaceTrack`, `createRaceState`, `RaceState`, `RaceEvent`, `RaceWinCondition`, `firstPastPost`, `topK`, `lastStanding`, `everyoneFinishes` |
|
|
1029
1389
|
| Reveal query | `sensor/revealQuery` | `createRevealQuery`, `RevealQuery`, `RevealQueryOptions`, `RevealHit` |
|
|
@@ -1042,6 +1402,8 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
1042
1402
|
| Telegraph | `combat/telegraph` | `pointInTelegraph`, `telegraphProgress`, `telegraphFired`, `telegraphTurnProgress`, `telegraphFiredAtTurn`, `telegraphTurnsRemaining`, `TelegraphShape`, `TelegraphConfig` |
|
|
1043
1403
|
| Dash / dodge | `movement/dash` | `createDashState`, `DashState`, `DashConfig`, `DashBurst`, `iframeActive`, `dashOffset` |
|
|
1044
1404
|
| Ability kit | `combat/abilityKit` | `createAbilityKit`, `AbilityKit`, `AbilitySlotConfig`, `AbilitySlotSnapshot`, `AbilitySlotState`, `AbilityCastType`, `AbilityCastResult`, `AbilitySlotRetune` |
|
|
1405
|
+
| Resource pool | `combat/resourcePool` | `createResourcePool`, `ResourcePool`, `ResourcePoolConfig` — current/max with per-second regen/decay and spend/gain; `pool.current()` is the ability kit's `resourceAvailable` |
|
|
1406
|
+
| Combo points | `combat/comboPoints` | `createComboPoints`, `ComboPoints`, `ComboPointsConfig` — discrete points accrued on action, expiring after a timeout from the last gain, spent in bulk |
|
|
1045
1407
|
| Event meter | `stats/eventMeter` | `createEventMeter`, `EventMeter`, `EventMeterConfig`, `EventMeterFeedResult` |
|
|
1046
1408
|
| Auto-target policy | `scene/autoTarget` | `selectAutoTarget`, `createAutoTargeter`, `AutoTargetPolicy`, `AutoTargeter`, `AutoTargetDeps` |
|
|
1047
1409
|
| Resistance matrix | `combat/resistance` | `resolveResistance`, `resistanceScale`, `ResistanceMatrix`, `ResistVerdict`, `ResistanceResult` |
|
|
@@ -1056,6 +1418,15 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
1056
1418
|
|
|
1057
1419
|
## Getting started (new project)
|
|
1058
1420
|
|
|
1421
|
+
Fastest path — the `jgengine` CLI scaffolds the entire canonical shape below (harness, skeleton, stub game, verify test, AGENTS.md) as a booting game:
|
|
1422
|
+
|
|
1423
|
+
```sh
|
|
1424
|
+
npx jgengine create my-game # then: cd my-game && bun dev
|
|
1425
|
+
npx jgengine doctor # later, if the setup drifts (version skew, unstyled HUD, shape strays)
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
Manual equivalent:
|
|
1429
|
+
|
|
1059
1430
|
```sh
|
|
1060
1431
|
bun add @jgengine/core @jgengine/react @jgengine/shell react react-dom three three-stdlib @react-three/fiber @react-three/drei
|
|
1061
1432
|
bun add -d @tailwindcss/vite tailwindcss # HUD styling (Vite + Tailwind v4)
|
|
@@ -1183,7 +1554,7 @@ A voxel block is an object. A rack is an object with a slot inventory. A GPU is
|
|
|
1183
1554
|
|
|
1184
1555
|
## Game repo layout
|
|
1185
1556
|
|
|
1186
|
-
Every game is one shape, enforced by `check-game-shape` (part of `check-types`): the top of `src/` holds only the skeleton, and every game-specific module, UI component, and test lives under `src/game/`. Dense files — one `catalog.ts` per domain, never one file per entry.
|
|
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.
|
|
1187
1558
|
|
|
1188
1559
|
```
|
|
1189
1560
|
src/
|
|
@@ -1214,6 +1585,8 @@ src/
|
|
|
1214
1585
|
|
|
1215
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.
|
|
1216
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
|
+
|
|
1217
1590
|
```ts
|
|
1218
1591
|
// game.config.ts — imports only, nothing inline
|
|
1219
1592
|
import { defineGame } from "@jgengine/shell/defineGame";
|
|
@@ -1275,7 +1648,7 @@ export const physics: PhysicsConfig = { gravity: -32 };
|
|
|
1275
1648
|
|
|
1276
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.
|
|
1277
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.
|
|
1278
|
-
- **Keybind → command convention.** The shell fires a command for any bound action that isn't reserved: pressing an action runs a command of the **same name** if one is defined, else a `ui.<action>` fallback (so `openBackpack` → `ui.openBackpack`). Just declare the binding and a matching command — no per-game `keydown` listener. Reserved actions the shell consumes natively and never routes to a command: `moveForward/moveBack/moveLeft/moveRight`, `turnLeft/turnRight`, `sprint`, `jump`, `tabTarget`, `clearTarget`, `useAbility`, `interact`, and any `hotbarSlotN`/`slotN`. `tabTarget`/`clearTarget` run `target.cycle`/`target.clear` (native `cycleTarget`/`setTarget` fallback).
|
|
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`).
|
|
1279
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.
|
|
1280
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.
|
|
1281
1654
|
- `server.mode` is a string your loop/commands interpret — the engine ships no gamemode presets.
|
|
@@ -1330,6 +1703,8 @@ Optional render/world fields the shell also reads: `entitySprites` / `entityMode
|
|
|
1330
1703
|
|
|
1331
1704
|
**Lighting and backdrop.** `PlayableGame.lighting` (`LightingConfig`, `@jgengine/core/game/playableGame`) replaces the shell's hardcoded ambient/directional default when present, regardless of world kind: `ambient?: { color?, intensity? }`, `directional?: { color?, intensity?, position, castShadow? }[]`, `hemisphere?: { skyColor?, groundColor?, intensity? }`. `PlayableGame.backdrop` (`BackdropConfig`) is a generic background/sky/fog for **any** world kind, including a custom `environment` component: `background?: string` (CSS color), `sky?: SkyEnvironmentConfig` (same descriptor `environment()`'s `sky` field takes), `fog?: { color?, near?, far?, density? }` (`density` set switches to exponential `FogExp2` and `near`/`far` are ignored). Both are optional and additive to whatever the world/`environment` renderer already draws.
|
|
1332
1705
|
|
|
1706
|
+
**Visibility & streaming (automatic).** Every 3D game gets camera frustum + distance culling for free — the shell's `CullingProvider` reads the live camera each frame, runs the engine `createVisibilitySystem` (`@jgengine/core/visibility/visibilitySystem`) over the scene's entities and placed objects, and toggles `group.visible` so off-screen objects are never submitted to the renderer (never unmounted — gameplay and simulation are untouched). Defaults are conservative (a preload margin larger than the view, hysteresis, `Infinity` default render distance) so existing games only benefit. Tune or opt out via `PlayableGame.visibility` (`VisibilityConfig`, `@jgengine/core/visibility/config`): `enabled: false` disables it; `culling`/`streaming` patch the global `CullingSettings`/`StreamingSettings` (`@jgengine/core/visibility/settings`); `scene` sets scene-wide overrides; `entities`/`objects` override by kind name / catalog id (`alwaysVisible`, `maxRenderDistance`, custom `bounds`, `pinned`, `cullingDisabled`, …). The engine layer also ships `@jgengine/core/visibility/spatialIndex` (the 3D hash the culler queries instead of scanning every object), `@jgengine/core/visibility/assetStreaming` (dedup/budget/grace-period asset loading), and `@jgengine/core/visibility/simulationCulling` (opt-in, off by default — throttles low-priority off-screen updates, never protected entities). Full reference: `packages/core/src/visibility/README.md`.
|
|
1707
|
+
|
|
1333
1708
|
**Player movement tuning** — the `movement` field (`PlayerMovementConfig`) tunes the shell's local-player walk controller beyond `physics.gravity`/`jumpVelocity`: `mode` (`"free"` camera-relative default, `"axis"` locks travel to one world `axis`, `"grid"` snaps each committed step to `cellSize` centers), `collideObjects` (collide against placed scene objects as unit-box AABBs even without `collision.voxel`), and `beforeCommit(frame)` — an escape hatch called each frame with `{ entityId, current, next, dt }` that can return a replacement `[x, y, z]` to constrain or redirect the step (rails, bounds, custom collision) before it commits and before `onTick` runs.
|
|
1334
1709
|
|
|
1335
1710
|
The runner boots `createGameContext({ definition, content, player: { userId, isNew } })`, calls `loop.onInit(ctx)` then `loop.onNewPlayer(ctx)`, and drives `loop.onTick(ctx, dt)` per frame. **Convention: `onNewPlayer` spawns the player entity with `id === ctx.player.userId`** — bounded stats, targeting, and kill attribution key off that.
|
|
@@ -1358,7 +1733,7 @@ Catalog-first, no per-game audio glue. The `audio` field of `defineGame({...})`
|
|
|
1358
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:
|
|
1359
1734
|
| `rig` | For | Key config (`camera.<rig>`) |
|
|
1360
1735
|
|-------|-----|------------------------------|
|
|
1361
|
-
| `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 |
|
|
1362
1737
|
| `first` | FPS mouse-look | `firstPerson: { eyeHeight, sensitivity, maxPitch, reticle, viewmodel }` |
|
|
1363
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`) |
|
|
1364
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 |
|
|
@@ -1417,384 +1792,662 @@ ctx.time advance, now, calendar, snapshot; pause, play, toggle,
|
|
|
1417
1792
|
ctx.subscribe / ctx.version change signal — UI layers bind via useSyncExternalStore
|
|
1418
1793
|
```
|
|
1419
1794
|
|
|
1420
|
-
`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.
|
|
1796
|
+
|
|
1797
|
+
### Two tiers: `ctx` runtime vs pure factories
|
|
1798
|
+
|
|
1799
|
+
The `ctx` surface above is the **stateful runtime** — it's what game code uses. Every subsystem it wires is *also* exported as a **pure factory** that `createGameContext` composes internally: `createTradeSystem`, `createDeathSystem`, `createEffectSystem`, `createProjectileSystem`, `createSpatialApi`, `createEntityStatsApi`, `createEntityStore`, `createObjectStore`, `createStats`, `createLoadouts`, `createLootRegistry`, `createQuestJournal`, `createSocial`, `createSlots`, `createInteriors` (plus stateless helpers beside each — `canAffordCosts`/`resolveBuy` in `game/trade`, `getStatValue`/`applyPoolDelta` in `scene/entityStats`, and so on). **Build a game through `ctx`, not these** — reach for the factories only for unit tests of pure game math, headless servers, or a custom runtime. Import the domain deep path (`@jgengine/core/combat/death`, `@jgengine/core/game/trade`, `@jgengine/core/stats/statModifiers`, …) and read the `.d.ts`; each is a real export in the published package.
|
|
1800
|
+
|
|
1801
|
+
`createSpatialApi`'s optional `grid: { cellSize }` opts `inRadius`/`queryArc` into a lazily-built x/z broadphase index over `candidates()` instead of a linear scan — worth it once candidate counts run into the hundreds+. The index is reused across calls until `invalidate()` is called, so call it after any position change (move, spawn, despawn); a candidate outside the index at query time still resolves exactly (never silently skipped), only a *moved* one can be missed until invalidated.
|
|
1802
|
+
|
|
1803
|
+
## `loop` — lifecycle
|
|
1804
|
+
|
|
1805
|
+
```ts
|
|
1806
|
+
export function onInit(ctx: GameContext) {
|
|
1807
|
+
ctx.item.use.register(itemUseHandlers);
|
|
1808
|
+
ctx.player.loadout.register(loadouts);
|
|
1809
|
+
for (const table of lootTables) ctx.game.loot.register(table);
|
|
1810
|
+
ctx.game.quest.register(quests);
|
|
1811
|
+
ctx.game.quest.bind("entity.died");
|
|
1812
|
+
ctx.game.feed.bind("entity.died");
|
|
1813
|
+
ctx.game.events.on("entity.died", (evt) => onEntityDied(ctx, evt));
|
|
1814
|
+
setupWorld(ctx);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
export function onNewPlayer(ctx: GameContext) {
|
|
1818
|
+
ctx.scene.entity.spawn("player_default", { id: ctx.player.userId, position: spawnPoint });
|
|
1819
|
+
if (ctx.player.isNew) ctx.player.applyLoadout(ctx.player.userId, "starterKit");
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
export function onTick(ctx: GameContext, dt: number) {
|
|
1823
|
+
// AI, regen, respawn timers — dt is GAME time (see ctx.time). Never death detection (see entity.died)
|
|
1824
|
+
}
|
|
1825
|
+
```
|
|
1826
|
+
|
|
1827
|
+
`onInit` runs once per boot; register everything there. Loot tables register through `ctx.game.loot.register` — `lootTable()` is a pure validating factory, there is no global side-effect registry.
|
|
1828
|
+
|
|
1829
|
+
## `ctx.time` — the simulation clock
|
|
1830
|
+
|
|
1831
|
+
`onTick`'s `dt` is **game time, not real time**: the shell scales each frame's real delta by `definition.time.scale` (real→game seconds at 1×) and the live speed multiplier, so writing decay/regen/AI as `rate * dt` makes it obey pause and fast-forward for free — never read wall-clock in a tick. Configure via `defineGame({ time: { scale?, speeds?, dayLength?, start?, startPaused?, daysPerYear?, seasons? } })` (all optional; default is real-time 1:1 with speeds `[1,2,3,4]`, 365-day years).
|
|
1832
|
+
|
|
1833
|
+
- **Continuous** work scales through `dt`. **Scheduled** work uses game-time timers: `ctx.time.after(sec, cb)`, `ctx.time.every(sec, cb)`, `ctx.time.at(gameSec, cb)` — measured in game-seconds, so 4× fires them 4× sooner and pause freezes them. Each returns a cancel handle.
|
|
1834
|
+
- **Controls** (drive from a HUD or a command): `pause()`, `play()`, `toggle()`, `setSpeed(mult)` (0 pauses), `cycleSpeed()`. Read state with `ctx.time.snapshot()` / `ctx.time.calendar()` (`{ day, hour, minute, second, dayFraction, year, dayOfYear, yearFraction, season? }`), or in React with `useGameClock()` → snapshot + `controls`. Speeding to 4× or pausing affects **everything** on the tick — no per-system wiring.
|
|
1835
|
+
- **Calendar year/season** rides the same day counter, no separate clock: `year`/`dayOfYear` fall out of `day` divided by `TimeConfig.daysPerYear` (default 365), `yearFraction` is progress through the current year (0..1). Setting `TimeConfig.seasons: string[]` (e.g. `["spring","summer","fall","winter"]`) slices the year into equal named segments and populates `calendar().season`; omit `seasons` and the field is absent — a living-world sim names its own season boundaries this way instead of a hand-rolled `dayOfYear % ...` module.
|
|
1836
|
+
|
|
1837
|
+
### Beat clock — BPM signal + input quantization
|
|
1838
|
+
|
|
1839
|
+
`@jgengine/core/time/beatClock` is a separate, purpose-built signal from `simClock` — a BPM tick generator for rhythm games (Hi-Fi Rush–style quantized combat), not a day/pause clock. `createBeatClock({ bpm, beatsPerBar? }, onBeat?)` returns a `BeatClock`: call `advance(gameDt)` from `onTick` with the same **game-time** `dt` (never wall-clock) — it fires `onBeat(beatIndex)` once per newly crossed integer beat and returns a `BeatSnapshot` (`beat`, `beatIndex`, `bar`, `beatInBar`, `phase`). `createBeatInputBuffer<T>(beatDurationSec)` is the auto-correct input buffer: `buffer(action, nowSec)` quantizes an off-beat press to fire on the next beat tick (or immediately if pressed exactly on one); `advance(nowSec)` drains and returns every action whose beat has arrived. `nextBeatTime(nowSec, beatDurationSec)` is the underlying pure quantization function. Feed a music track's actual BPM in; the buffer is what makes an early/late input still land on-beat.
|
|
1840
|
+
|
|
1841
|
+
## Content catalogs
|
|
1842
|
+
## `ctx.game.store` — reactive game state
|
|
1843
|
+
|
|
1844
|
+
```ts
|
|
1845
|
+
ctx.game.store.set("health", 100) // any key, any value type
|
|
1846
|
+
ctx.game.store.get("health") // T | undefined
|
|
1847
|
+
ctx.game.store.has("health")
|
|
1848
|
+
ctx.game.store.delete("health")
|
|
1849
|
+
ctx.game.store.subscribe(listener) // change-signal fires on set/delete
|
|
1850
|
+
ctx.game.store.mapSnapshot() / arraySnapshot()
|
|
1851
|
+
```
|
|
1852
|
+
|
|
1853
|
+
A reactive per-game keyed store (`ObservableKeyedStore<unknown>`) attached to `GameContext` — reach for it instead of a module-level singleton store for ad-hoc reactive game state (turn trackers, deck UIs, anything that doesn't already have a `ctx` surface). `set`/`delete` bump `ctx.version()` and notify `ctx.subscribe` listeners; `get`/`has` are plain reads. Unlike a per-slot handle, there is no `define`/seed step — a key simply doesn't exist until the first `set`.
|
|
1854
|
+
|
|
1855
|
+
## `ctx.game.cards` / `ctx.game.turn` — lazily-created piles and turn loops
|
|
1856
|
+
|
|
1857
|
+
`ctx.game.cards.pile(id, config?)` and `ctx.game.turn.loop(id, config?)` lazily create (config required on first call) or return the existing notify-wrapped `CardPile`/`TurnLoop` for `id` — call with just the id after the first `onInit` seed to fetch the same instance; every mutating method is wrapped so it bumps `ctx.version()`/notifies `ctx.subscribe` automatically, same as every other `ctx` surface. This replaces manually constructing `createCardPile`/`createTurnLoop` and wiring notification yourself.
|
|
1858
|
+
|
|
1859
|
+
## Movement, pose, input
|
|
1860
|
+
## External data — `data/dataSource` and the dev proxy
|
|
1861
|
+
|
|
1862
|
+
Renderer-free async-state primitives (`@jgengine/core/data`) for a game that reads a live external source (a leaderboard API, a session browser, remote config) — distinct from `ctx.game.store`/multiplayer, which are for the game's own authoritative state.
|
|
1863
|
+
|
|
1864
|
+
- **`createDataSource(load, options?)`** (`data/dataSource`) → `DataSource<T>` wraps one `load(signal)` async call as `{ status: "idle"|"loading"|"ready"|"error", data, error }`. `getState()` reads the current snapshot, `subscribe(listener)` fires on every change, `refresh({ force? })` re-runs `load` (de-duplicates a call already in flight unless `force`; aborts the prior call first when forced), `startPolling(intervalMs?)`/`stopPolling()` run `refresh` on an interval (`intervalMs` falls back to the one passed at construction; throws if neither is given), `dispose()` tears down polling and in-flight requests for good. Pass `options.clock` (`{ setInterval, clearInterval }`) to swap the timer source in tests.
|
|
1865
|
+
- **`fetchJson<T>(url, options?)`** (`data/fetchJson`) — `fetch` + JSON-parse in one call; throws `HttpStatusError` (`status`, `statusText`, `url`) on a non-OK response and `JsonParseError` (`url`, `cause`) on unparsable JSON, so a `DataSource`'s `error` is always one of these two typed shapes, never a bare `Error`. `options.fetchImpl` swaps the fetch implementation for tests/SSR.
|
|
1866
|
+
- **`createJsonDataSource<T>(url, options?)`** (`data/jsonDataSource`) — sugar combining the two above: a `DataSource<T>` whose `load` calls `fetchJson(url, options)`.
|
|
1867
|
+
- **Dev proxy (`data/devProxy`)** — same-origin routing for external APIs during `bun dev` so browser CORS never blocks a game's `fetchJson` call against a third-party host. `parseDevProxyTable(raw)` parses a `VITE_JGENGINE_DEV_PROXY` env value (a JSON object of `{ routeName: "https://api.example.com" }`) into a `DevProxyTable`; `proxiedUrl(target, { dev?, table?, prefix? })` rewrites a `target` URL whose prefix matches a table entry into `/proxy/<routeName>/<rest>` (default prefix `/proxy`) when `dev` is true (defaults to `import.meta.env.DEV`) — else returns `target` unchanged, so the same call hits the real host in production. `apps/dev`'s `vite.config.ts` reads the same env var and wires a matching Vite server `proxy` entry per route (`changeOrigin: true`, strips the `/proxy/<routeName>` prefix) — set `VITE_JGENGINE_DEV_PROXY` once and both sides (the URL rewrite and the actual proxy route) agree.
|
|
1868
|
+
|
|
1869
|
+
## Multiplayer and the backend seam
|
|
1870
|
+
## Genre cheat sheet
|
|
1871
|
+
|
|
1872
|
+
- **Voxel/crafting**: objects for blocks/machines, `voxel()`, `object.break`/`object.placeFromInventory`.
|
|
1873
|
+
- **Tycoon/lab**: objects + `slotInventory`, `plots()`, configure via prompt → command.
|
|
1874
|
+
- **Shooter**: `fireProjectile`/`settleProjectile`; grenades settle → `effect({ at, radius })`; `movement.poses`/`aim` + zoom modifier; `servers({ … })` + game-owned `server.mode`; loadout classes from commands.
|
|
1875
|
+
- **MMO/RPG**: bounded stats + `leveling()` over a game XP curve; `tabTarget` → `cycleTarget`; handlers read `getTarget`; quests bound to `entity.died`/`inventory.added`; social party + `partyShare`; `server: "persistent"`.
|
|
1876
|
+
- **All combat games**: react to `entity.died` (feed/leaderboard/score) — never poll HP.
|
|
1877
|
+
|
|
1878
|
+
## Anti-patterns
|
|
1879
|
+
|
|
1880
|
+
| Wrong | Right |
|
|
1881
|
+
|-------|-------|
|
|
1882
|
+
| Player tuning in `defineGame` | Entity catalog `movement` + stats |
|
|
1883
|
+
| `behaviors: […]` on place/spawn | Catalog entry |
|
|
1884
|
+
| Engine `weapon.fire` / `consumable.use` / `combat.*` | `item.use` + catalog `use` → game handler |
|
|
1885
|
+
| `ItemUseInput.to` for targets | `getTarget(from)` in handlers |
|
|
1886
|
+
| `effect({ to })` for gunshots | `fireProjectile` + `settleProjectile` |
|
|
1887
|
+
| Polling HP in `onTick` for kills | `entity.died` event |
|
|
1888
|
+
| `combat.lootTable` / `loot.enemy` | `onDeath` on the entity that died |
|
|
1889
|
+
| Hand-rolled `Math.random()` loot in commands | `lootTable()` + `ctx.game.loot.roll` |
|
|
1890
|
+
| Hand-rolled `xpForLevel`/`levelFromXp` | `game/progression` `curve()` + `leveling()` |
|
|
1891
|
+
| Hardcoded shop arrays | `item.trade.shops` + `tradableAt` |
|
|
1892
|
+
| Kit seeding via scattered `put`/`grant` | `applyLoadout` |
|
|
1893
|
+
| Per-user quest state hand-rolled | `game.quest.register` + binds |
|
|
1894
|
+
| `useKillFeed` / per-domain feed hooks | `useFeed({ action })` |
|
|
1895
|
+
| Raw keys in game logic | `defineGame` input actions |
|
|
1896
|
+
| Positioning inside `ui/components/` or on primitives (`CurrencyPill className="absolute …"`) | Screen wrappers in `GameUI.tsx` only |
|
|
1897
|
+
| Game UI classes without `@source` in host CSS | `@source` entries for your game dirs + `node_modules/@jgengine/{shell,react}` |
|
|
1898
|
+
| One file per catalog entry / per brand | Dense `<domain>/catalog.ts` |
|
|
1899
|
+
| Convex mutations called from game code | `commands.run` through the `GameBackend` transport |
|
|
1900
|
+
| Half a system: quest without tracker, cooldown without sweep, keybind never shown, stub "coming soon" modal | Finish the system end to end — or cut it whole (see `jgengine`) |
|
|
1901
|
+
| Game-side workaround for a missing engine primitive | File the gap at github.com/Noisemaker111/jgengine/issues (or PR the primitive) and cut or scope the dependent system honestly |
|
|
1902
|
+
| Game nouns in this skill | Engine primitives + placeholder ids only |
|
|
1903
|
+
|
|
1904
|
+
## New-game definition of done
|
|
1905
|
+
|
|
1906
|
+
This is a gate, not a suggestion — every box, in one pass (workflow: **`jgengine`** skill). "Compiles and the hooks are wired" is not done; a declared system with no UI, no feedback, or no way to exercise it is not done — finish the system or cut it whole.
|
|
1907
|
+
|
|
1908
|
+
- [ ] `game.config.ts` (`defineGame` from `@jgengine/shell/defineGame`) + `index.tsx` (barrel) + `main.tsx` (standalone host) + `loop.ts` + `game/content.ts`
|
|
1909
|
+
- [ ] Catalogs: `game/entities/<role>/catalog.ts`, `game/items/<domain>/catalog.ts`, `game/objects/catalog.ts`; loot tables beside their domain
|
|
1910
|
+
- [ ] Entity `stats` + `receive` orders aligned on the same stat ids; `role` set (drives targeting + camera)
|
|
1911
|
+
- [ ] `game/items/use-handlers.ts` registered in `onInit`; handlers read `getTarget`/`aim`, never a target input
|
|
1912
|
+
- [ ] `game/loadouts.ts` + `applyLoadout` in `onNewPlayer` (gated on `isNew`)
|
|
1913
|
+
- [ ] `game/quests/catalog.ts` + binds; if using xp/level, a game-owned curve fed to `game/progression` (`curve`/`leveling`) — **with their HUD/tracker, or cut**
|
|
1914
|
+
- [ ] `onInit`: register handlers/loadouts/loot/quests, event listeners, feed binds, leaderboard tracks; `setupWorld`
|
|
1915
|
+
- [ ] Player spawns with `id === ctx.player.userId`
|
|
1916
|
+
- [ ] `game/ui/GameUI.tsx` owns layout; components use `@jgengine/react` hooks
|
|
1917
|
+
- [ ] UI passes the **quality bar** above (contrast, scale, framing, genre fit) — not just hook wiring
|
|
1918
|
+
- [ ] Camera tuned via `camera` in `defineGame({...})` — defaults untouched means the feel was never checked
|
|
1919
|
+
- [ ] For an `environment()` world: a `<game>.world.test.ts` asserts `summarizeEnvironment(world)` (`@jgengine/core/world/environmentSummary`) is non-empty with the expected counts — the browserless scene-correctness gate
|
|
1920
|
+
- [ ] HUD screenshotted over a staged `GameUiPreview` scenario and **judged by looking at the image** against the UI quality bar in [`../jgengine-ui/reference.md`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md) — the final human glance, not the verification loop
|
|
1921
|
+
- [ ] Co-located bun tests for pure game math (curves, cooldowns, spawn logic)
|
|
1922
|
+
- [ ] Multiplayer via adapter config only; no direct backend calls
|
|
1923
|
+
|
|
1924
|
+
## Quick reference
|
|
1925
|
+
|
|
1926
|
+
```
|
|
1927
|
+
defineGame (shell) engine fields (assets, world, physics, inventories, input, server, save, time, feed, multiplayer)
|
|
1928
|
+
+ presentation fields (content, loop, GameUI, camera, environment, shadows, movement, devtools, …) in one call — smart defaults fill the rest
|
|
1929
|
+
defineGame (core) the underlying engine-only primitive: assets, world, physics, inventories, input, server, save, time, feed, multiplayer, loop
|
|
1930
|
+
PlayableGame { game, content, loop, GameUI, camera, … } — the runner contract `defineGame` (shell) returns
|
|
1931
|
+
GameContext ctx.scene / ctx.game / ctx.player / ctx.item / ctx.camera / ctx.input + subscribe/version
|
|
1932
|
+
scene.object place, remove, move, rotate, at, setVisual (per-instance ObjectVisual: scale/color/opacity override)
|
|
1933
|
+
scene.entity spawn (anchor/offset), despawn, setPose, update; stats; targeting; effects;
|
|
1934
|
+
|
|
1935
|
+
# jgengine-ui
|
|
1936
|
+
|
|
1937
|
+
Use this skill for the **visual and interaction design of the game interface**: title screens, HUDs, menus, prompts, maps, inventories, dialogue, touch controls, transitions, pause/results states, accessibility, and screenshot critique.
|
|
1938
|
+
|
|
1939
|
+
Do not use this skill as a React, routing, state-management, or hooks reference. The main `jgengine` skill owns routing and points to the engine APIs. When implementation needs `@jgengine/react` hooks or shell APIs, follow the links in the main skill and the compact API appendix in [reference.md](reference.md); keep this skill focused on what the player sees and feels.
|
|
1940
|
+
|
|
1941
|
+
## Required outcome
|
|
1942
|
+
|
|
1943
|
+
A JGengine game must read as a self-contained game, not a responsive website with a canvas inside it.
|
|
1944
|
+
|
|
1945
|
+
Before shipping UI:
|
|
1946
|
+
|
|
1947
|
+
1. Give the game a concise UI art direction.
|
|
1948
|
+
2. Compose explicit desktop/mobile game layouts instead of document flow.
|
|
1949
|
+
3. Keep persistent HUD information sparse and hierarchical.
|
|
1950
|
+
4. Adapt touch controls to the genre and reserve their screen zones.
|
|
1951
|
+
5. Implement authored focus, pressed, selected, disabled, success, failure, and warning states.
|
|
1952
|
+
6. Add purposeful motion and feedback.
|
|
1953
|
+
7. Capture screenshots and revise what actually renders.
|
|
1954
|
+
|
|
1955
|
+
## Ownership boundary
|
|
1956
|
+
|
|
1957
|
+
The main `jgengine` skill owns intake, engine architecture, API routing, hooks, commands, state, and verification routing. This skill owns presentation quality.
|
|
1958
|
+
|
|
1959
|
+
Read [reference.md](reference.md) when building or reviewing a game interface. It contains the implementation quality bar, layout rules, art-direction template, touch-control requirements, acceptance criteria, and the compact existing React API surface.
|
|
1960
|
+
|
|
1961
|
+
## Non-negotiable defaults
|
|
1962
|
+
|
|
1963
|
+
- Active play owns the viewport; no marketing header, page title bar, document scrolling, or website container.
|
|
1964
|
+
- Screen placement belongs in the game's `ui/GameUI.tsx` composition layer.
|
|
1965
|
+
- Persistent gameplay information is frameless unless a physical/diegetic frame is part of the game's art direction.
|
|
1966
|
+
- Instructions are contextual and temporary, not permanent keyboard grids.
|
|
1967
|
+
- Mobile controls share input mechanics but not one universal visual skin.
|
|
1968
|
+
- Themes change geometry, composition, typography roles, icons, motion, materials, sound, and density—not only colors.
|
|
1969
|
+
- Ordinary rounded cards, pill buttons, generic dark modals, and dashboard grids are fallback failures, not defaults.
|
|
1970
|
+
|
|
1971
|
+
## Preview states ship with the UI
|
|
1972
|
+
|
|
1973
|
+
Every game ships `src/preview.tsx`: a static default frame (default export, used by the website card) plus a `states` named export (`GamePreviewStates` from `@jgengine/react/preview`) keying named UI states — `stage_1`, `game_over`, `boss_intro` — to components. Build state entries from the game's **real UI components** with fixture snapshots (canned props/state), not redrawn lookalikes; that turns every key into a capturable render test. Capture any state instantly with `bun run shoot <game> --preview <stateKey>` — no sim, no three.js, no hang risk — and use it as the screenshot-critique loop for HUD/menu/overlay work before any full-shell `--mode ui`/`play` glance.
|
|
1974
|
+
|
|
1975
|
+
## Rejection test
|
|
1976
|
+
|
|
1977
|
+
Reject and revise the UI when it could be mistaken for a SaaS dashboard, landing page, admin panel, documentation page, or generic emulator overlay.
|
|
1978
|
+
|
|
1979
|
+
# JGengine UI — game presentation reference
|
|
1980
|
+
|
|
1981
|
+
This reference defines the required visual and interaction quality for JGengine games. The main `jgengine` skill owns engine architecture, hooks, input commands, and routing. This document owns what the interface looks like, how it is composed, how it responds, and how it is verified.
|
|
1982
|
+
|
|
1983
|
+
## The rule
|
|
1984
|
+
|
|
1985
|
+
A game must visually own its viewport. It must not resemble a dashboard, landing page, documentation page, or ordinary responsive web app.
|
|
1986
|
+
|
|
1987
|
+
HTML and React are valid implementation tools. Website visual grammar is not the default.
|
|
1988
|
+
|
|
1989
|
+
## 1. Start with a concise UI art direction
|
|
1990
|
+
|
|
1991
|
+
Before implementing screens, write this short block:
|
|
1992
|
+
|
|
1993
|
+
```md
|
|
1994
|
+
UI ART DIRECTION
|
|
1995
|
+
|
|
1996
|
+
Player fantasy:
|
|
1997
|
+
Emotional tone:
|
|
1998
|
+
Shape language:
|
|
1999
|
+
Material language:
|
|
2000
|
+
Typography roles: display / body / numerical / labels
|
|
2001
|
+
Motion language:
|
|
2002
|
+
Icon language:
|
|
2003
|
+
Sound language:
|
|
2004
|
+
Information hierarchy:
|
|
2005
|
+
Forbidden patterns:
|
|
2006
|
+
```
|
|
2007
|
+
|
|
2008
|
+
Keep it practical. It should directly influence layout, silhouettes, controls, timing, and materials.
|
|
2009
|
+
|
|
2010
|
+
Example forbidden patterns:
|
|
2011
|
+
|
|
2012
|
+
- generic rounded dashboard cards
|
|
2013
|
+
- pill buttons
|
|
2014
|
+
- long centered paragraphs during play
|
|
2015
|
+
- ordinary two-column form layouts
|
|
2016
|
+
- persistent keyboard-instruction grids
|
|
2017
|
+
- multiple equally weighted bordered panels
|
|
2018
|
+
- generic translucent mobile circles
|
|
2019
|
+
- large website-style modals
|
|
2020
|
+
- document-flow wrapping used as HUD layout
|
|
2021
|
+
|
|
2022
|
+
A theme is not complete when only colors and fonts change. It must also affect composition, geometry, spacing rhythm, borders, icons, animation, sound, information density, terminology, button construction, and touch controls.
|
|
2023
|
+
|
|
2024
|
+
## 2. Screen inventory and hierarchy
|
|
2025
|
+
|
|
2026
|
+
Identify the screens the game actually needs:
|
|
2027
|
+
|
|
2028
|
+
- boot/loading
|
|
2029
|
+
- title or attract screen
|
|
2030
|
+
- mode selection
|
|
2031
|
+
- onboarding/tutorial
|
|
2032
|
+
- gameplay HUD
|
|
2033
|
+
- pause
|
|
2034
|
+
- settings
|
|
2035
|
+
- map/inventory/dialogue where relevant
|
|
2036
|
+
- victory/results
|
|
2037
|
+
- failure/retry
|
|
2038
|
+
|
|
2039
|
+
For each screen, define:
|
|
2040
|
+
|
|
2041
|
+
- the player’s primary question
|
|
2042
|
+
- the primary action
|
|
2043
|
+
- the most important information
|
|
2044
|
+
- what can be hidden
|
|
2045
|
+
- what belongs in-world instead of in the HUD
|
|
2046
|
+
|
|
2047
|
+
### HUD tiers
|
|
2048
|
+
|
|
2049
|
+
**Tier 1 — immediate action and survival**
|
|
2050
|
+
Health, timer, current target, ammo, danger, capture state.
|
|
2051
|
+
|
|
2052
|
+
**Tier 2 — short-term decisions**
|
|
2053
|
+
Objective progress, route progress, cooldowns, combo, pursuit distance.
|
|
2054
|
+
|
|
2055
|
+
**Tier 3 — reference information**
|
|
2056
|
+
Full map, inventory, controls, schedule, mission details, lore.
|
|
2057
|
+
|
|
2058
|
+
Tier 1 is immediately readable. Tier 2 is quieter. Tier 3 is usually hidden until requested.
|
|
2059
|
+
|
|
2060
|
+
Do not style every datum as an equally important bordered box.
|
|
2061
|
+
|
|
2062
|
+
## 3. Full-viewport game composition
|
|
2063
|
+
|
|
2064
|
+
The active game should behave like an application mode:
|
|
2065
|
+
|
|
2066
|
+
- own the full viewport
|
|
2067
|
+
- avoid document scrolling
|
|
2068
|
+
- avoid site navigation and marketing chrome during play
|
|
2069
|
+
- respect safe-area insets
|
|
2070
|
+
- keep exit/settings/fullscreen controls minimal
|
|
2071
|
+
- separate world, HUD, controls, screens, and system overlays
|
|
2072
|
+
|
|
2073
|
+
Recommended layer contract:
|
|
2074
|
+
|
|
2075
|
+
```tsx
|
|
2076
|
+
<GamePlayer>
|
|
2077
|
+
<WorldLayer />
|
|
2078
|
+
<HudLayer />
|
|
2079
|
+
<ControlLayer />
|
|
2080
|
+
<ScreenLayer />
|
|
2081
|
+
<SystemLayer />
|
|
2082
|
+
</GamePlayer>
|
|
2083
|
+
```
|
|
2084
|
+
|
|
2085
|
+
- `WorldLayer`: game renderer
|
|
2086
|
+
- `HudLayer`: non-blocking gameplay information
|
|
2087
|
+
- `ControlLayer`: touch/input surfaces
|
|
2088
|
+
- `ScreenLayer`: title, pause, settings, tutorial, victory, failure, transitions
|
|
2089
|
+
- `SystemLayer`: exit, fullscreen, engine settings, devtools
|
|
2090
|
+
|
|
2091
|
+
Do not place unrelated interface pieces into one ordinary DOM flow.
|
|
2092
|
+
|
|
2093
|
+
## 4. Explicit game layout modes
|
|
1421
2094
|
|
|
1422
|
-
|
|
2095
|
+
Do not rely on generic responsive wrapping. Compose explicit modes such as:
|
|
1423
2096
|
|
|
1424
|
-
|
|
2097
|
+
- desktop-wide
|
|
2098
|
+
- desktop-compact
|
|
2099
|
+
- mobile-landscape
|
|
2100
|
+
- mobile-portrait
|
|
1425
2101
|
|
|
1426
|
-
|
|
2102
|
+
A mobile layout is not a shrunken desktop HUD.
|
|
1427
2103
|
|
|
1428
|
-
|
|
2104
|
+
On mobile:
|
|
1429
2105
|
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
ctx.game.events.on("entity.died", (evt) => onEntityDied(ctx, evt));
|
|
1439
|
-
setupWorld(ctx);
|
|
1440
|
-
}
|
|
2106
|
+
- reserve thumb-control zones
|
|
2107
|
+
- keep critical HUD out of those zones
|
|
2108
|
+
- hide keyboard legends
|
|
2109
|
+
- reduce persistent information
|
|
2110
|
+
- move Tier 3 information behind contextual panels
|
|
2111
|
+
- respect browser and device safe areas
|
|
2112
|
+
- support portrait only when intentionally designed
|
|
2113
|
+
- otherwise show a polished rotate-device state
|
|
1441
2114
|
|
|
1442
|
-
|
|
1443
|
-
ctx.scene.entity.spawn("player_default", { id: ctx.player.userId, position: spawnPoint });
|
|
1444
|
-
if (ctx.player.isNew) ctx.player.applyLoadout(ctx.player.userId, "starterKit");
|
|
1445
|
-
}
|
|
2115
|
+
All viewport anchoring should live in the game’s top-level UI composition file. Child components own their internal layout, not their screen position.
|
|
1446
2116
|
|
|
1447
|
-
|
|
1448
|
-
// AI, regen, respawn timers — dt is GAME time (see ctx.time). Never death detection (see entity.died)
|
|
1449
|
-
}
|
|
1450
|
-
```
|
|
2117
|
+
### Design-resolution fit (`platforms` + `hudFit`)
|
|
1451
2118
|
|
|
1452
|
-
`
|
|
2119
|
+
Design-resolution fit is on by default for every game: each `HudCanvas` auto-scales from `hudFit.designSize` (default 1600×900) down to the live viewport, clamped by `hudFit.minScale`/`maxScale` (default 0.4–1), so the authored layout shrinks instead of overflowing a phone. No declaration needed. `hudFit.mobile` overrides the fit on compact displays only — tune the phone presentation there instead of hand-rolling media queries. The player's Graphics → UI scale setting multiplies the computed scale on every platform. Declaring `platforms: ["web"]` (without `"mobile"`) opts a desktop-only game out; its compact displays keep the legacy fixed 0.85 zoom.
|
|
1453
2120
|
|
|
1454
|
-
|
|
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.
|
|
1455
2122
|
|
|
1456
|
-
|
|
2123
|
+
### Phase-gated HUD visibility (`showDuring`)
|
|
1457
2124
|
|
|
1458
|
-
-
|
|
1459
|
-
- **Controls** (drive from a HUD or a command): `pause()`, `play()`, `toggle()`, `setSpeed(mult)` (0 pauses), `cycleSpeed()`. Read state with `ctx.time.snapshot()` / `ctx.time.calendar()` (`{ day, hour, minute, second, dayFraction, year, dayOfYear, yearFraction, season? }`), or in React with `useGameClock()` → snapshot + `controls`. Speeding to 4× or pausing affects **everything** on the tick — no per-system wiring.
|
|
1460
|
-
- **Calendar year/season** rides the same day counter, no separate clock: `year`/`dayOfYear` fall out of `day` divided by `TimeConfig.daysPerYear` (default 365), `yearFraction` is progress through the current year (0..1). Setting `TimeConfig.seasons: string[]` (e.g. `["spring","summer","fall","winter"]`) slices the year into equal named segments and populates `calendar().season`; omit `seasons` and the field is absent — a living-world sim names its own season boundaries this way instead of a hand-rolled `dayOfYear % ...` module.
|
|
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.
|
|
1461
2126
|
|
|
1462
|
-
###
|
|
2127
|
+
### Shared viewport allocation (`GameViewportProvider`, region collision)
|
|
1463
2128
|
|
|
1464
|
-
|
|
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.
|
|
1465
2130
|
|
|
1466
|
-
|
|
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"`.
|
|
1467
2135
|
|
|
1468
|
-
###
|
|
2136
|
+
### HUD priority + mobile behavior (`HudPanel`)
|
|
1469
2137
|
|
|
1470
|
-
|
|
1471
|
-
|-------|---------|
|
|
1472
|
-
| `id`, `model` | Canonical id, asset key |
|
|
1473
|
-
| `footprint` | `{ w, h, d }` placement bounds |
|
|
1474
|
-
| `snap` | `"grid"` \| `"free"` \| `"wall"` |
|
|
1475
|
-
| `solid` | Blocks movement |
|
|
1476
|
-
| `breakable` | `false` or `{ baseBreakTime, harvest, drops, dropsWhenUnmet }` |
|
|
1477
|
-
| `proximityPrompt` | Float UI + optional command invoke |
|
|
1478
|
-
| `slotInventory` | Attached container `{ slots, accepts }` created at place time (`object:<instanceId>`) |
|
|
2138
|
+
Declare intent, not just CSS. `HudPanel` accepts:
|
|
1479
2139
|
|
|
1480
|
-
|
|
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.
|
|
1481
2142
|
|
|
1482
|
-
|
|
2143
|
+
The game still owns the genre-appropriate composition; the engine owns the geometry and the failure gate.
|
|
1483
2144
|
|
|
1484
|
-
|
|
1485
|
-
|-------|---------|
|
|
1486
|
-
| `id`, `kind`, `stack`, `model` | Basics; `stack` feeds `itemTraits.stackLimit` |
|
|
1487
|
-
| `use` | Game handler name dispatched by `item.use` (`"fireGun"`, `"castBolt"`, `"drinkPotion"`) |
|
|
1488
|
-
| `weapon` | Stats the handler reads via `item.weapon.getStat` — `damage`, `heal`, `reach`, `manaCost`, `projectile.{mass,gravity,fuseTime,settleOn}`, `explosion.{radius}` … |
|
|
1489
|
-
| `trade` | `{ buy?: {coins: 80}, sell?, shops?: ["shop_town"] }` |
|
|
1490
|
-
| `requires` | Unlock ids gating purchase/use |
|
|
1491
|
-
| `placesObject` | Object id placed from hotbar |
|
|
1492
|
-
| `rarity`, `baseType` | Read by the `worldItem` rarity render binding + loot filter when this item drops to the ground (#32/#33); `baseType` defaults to the item id when absent |
|
|
2145
|
+
### Mandatory orientation (`orientation`)
|
|
1493
2146
|
|
|
1494
|
-
|
|
2147
|
+
A game declares its phone-orientation contract on `defineGame({ orientation })`:
|
|
1495
2148
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
| `movement` | `walkSpeed` (reaches spawn automatically), `poses?: ["standing","crouch","prone","running"]`, `aim?: ["hip","ads"]`, `frozen?: boolean` — a scene-instance-level movement lock (cutscenes, stuns, mount transitions); `movedWhileFrozen(entity)` (`scene/entityStore`) flags an entity whose velocity moved anyway despite `frozen: true`, catching a system that bypassed the lock |
|
|
1499
|
-
| `role` | `CatalogEntityRole` = `"player"` \| `"enemy"` \| `"hostile"` \| `"npc"` \| `"vehicle"` — catalog hostility class for targeting (`"enemy"`/`"hostile"` classify hostile in `cycleTarget`). Distinct from the scene *instance* `EntityRole` (`"player"` \| `"npc"` \| `"prop"`, in `scene/entityStore`) which drives input/camera binding — **possession** (`ctx.player.possession`) flips this instance role between `"player"`/`"npc"` on every control swap, so exactly one owned entity is ever the input/camera target |
|
|
1500
|
-
| `stats` | Stat declarations — bounded values: `{ health: { max: 120, min: 0 }, level: { max: 60, min: 1, current: 1 }, … }` — `current` optional, defaults to `max` |
|
|
1501
|
-
| `receive` | Per-effect absorption: `{ damage: { order: ["shield","health"], modifiers? }, heal: { order: ["health"] } }` — keyed by **game-defined effect ids**; presence = can receive |
|
|
1502
|
-
| `onDeath` | `{ drops: "table_id" }` or reason-aware `{ drops: [{ table, when: { reason: "player_kill" } }], command?: { name, when? } }` |
|
|
1503
|
-
| `wander`, `talkable` | AI descriptor; dialogue id sugar for a talk prompt |
|
|
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).
|
|
1504
2151
|
|
|
1505
|
-
|
|
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.
|
|
1506
2153
|
|
|
1507
|
-
|
|
2154
|
+
## 5. Change the visual grammar
|
|
1508
2155
|
|
|
1509
|
-
|
|
2156
|
+
Avoid making every element the same rounded translucent rectangle.
|
|
1510
2157
|
|
|
1511
|
-
|
|
2158
|
+
Use genre-appropriate structures:
|
|
1512
2159
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
2160
|
+
- clipped corners
|
|
2161
|
+
- irregular silhouettes
|
|
2162
|
+
- image-backed frames
|
|
2163
|
+
- mechanical plates
|
|
2164
|
+
- radial interfaces
|
|
2165
|
+
- ribbons and tabs
|
|
2166
|
+
- gauges and meters
|
|
2167
|
+
- emblems and decorative corners
|
|
2168
|
+
- notches and edge anchors
|
|
2169
|
+
- diegetic objects
|
|
2170
|
+
- world-space prompts
|
|
2171
|
+
- asymmetrical compositions
|
|
2172
|
+
- masks, textures, layered borders, and strong focal elements
|
|
1518
2173
|
|
|
1519
|
-
|
|
2174
|
+
Practical rule: no more than roughly 20% of a normal gameplay screen should resemble an ordinary web card or modal.
|
|
1520
2175
|
|
|
1521
|
-
|
|
2176
|
+
Every persistent panel must justify why it exists, remains visible, has that shape, and occupies that position.
|
|
1522
2177
|
|
|
1523
|
-
|
|
2178
|
+
## 6. Game UI primitives
|
|
1524
2179
|
|
|
1525
|
-
|
|
2180
|
+
Prefer small headless or lightly styled primitives over a giant universal design system. Useful concepts include:
|
|
1526
2181
|
|
|
1527
|
-
|
|
2182
|
+
- `HudAnchor`
|
|
2183
|
+
- `StatReadout`
|
|
2184
|
+
- `Meter`
|
|
2185
|
+
- `ObjectiveTracker`
|
|
2186
|
+
- `ActionPrompt`
|
|
2187
|
+
- `Reticle`
|
|
2188
|
+
- `MinimapFrame`
|
|
2189
|
+
- `DialoguePlate`
|
|
2190
|
+
- `Countdown`
|
|
2191
|
+
- `BossBar`
|
|
2192
|
+
- `ItemPickup`
|
|
2193
|
+
- `DamageIndicator`
|
|
2194
|
+
- `PauseScreen`
|
|
2195
|
+
- `ResultsScreen`
|
|
2196
|
+
- `VirtualControlZone`
|
|
2197
|
+
- `ScreenTransition`
|
|
1528
2198
|
|
|
1529
|
-
|
|
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.
|
|
1530
2200
|
|
|
1531
|
-
|
|
1532
|
-
ctx.scene.entity.setTarget(fromId, toId | null)
|
|
1533
|
-
ctx.scene.entity.getTarget(fromId) // → instanceId | null
|
|
1534
|
-
ctx.scene.entity.cycleTarget(fromId, { filter: "hostile" | "friendly" | "any", direction? })
|
|
1535
|
-
```
|
|
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`.
|
|
1536
2202
|
|
|
1537
|
-
|
|
2203
|
+
Do not create a primitive whose only value is wrapping a `div` with border radius.
|
|
1538
2204
|
|
|
1539
|
-
##
|
|
2205
|
+
## 7. Complete interaction states
|
|
1540
2206
|
|
|
1541
|
-
|
|
1542
|
-
ctx.item.use.register(handlers) // once in onInit; duplicate names throw
|
|
1543
|
-
ctx.item.use.can(ctx, input) // → { reason } | null
|
|
1544
|
-
ctx.item.use.use(ctx, input) // dispatches catalog `use` → your handler
|
|
1545
|
-
|
|
1546
|
-
type ItemUseInput = { from: string; itemId: string; inventoryId?: string; aim?: Aim };
|
|
1547
|
-
type ItemUseHandler<GameContext> = {
|
|
1548
|
-
can?(ctx, input): { reason: string } | null;
|
|
1549
|
-
apply(ctx, input): { state: GameContext; error?: string };
|
|
1550
|
-
};
|
|
1551
|
-
```
|
|
2207
|
+
Every interactive element needs intentional states:
|
|
1552
2208
|
|
|
1553
|
-
|
|
2209
|
+
- rest
|
|
2210
|
+
- hover where applicable
|
|
2211
|
+
- keyboard/controller focus
|
|
2212
|
+
- pressed
|
|
2213
|
+
- selected
|
|
2214
|
+
- disabled
|
|
2215
|
+
- success
|
|
2216
|
+
- failure
|
|
2217
|
+
- warning
|
|
1554
2218
|
|
|
1555
|
-
|
|
1556
|
-
|---------|--------------|
|
|
1557
|
-
| gun | spend ammo → `fireProjectile` → `settleProjectile` |
|
|
1558
|
-
| grenade | `fireProjectile` (ballistic) → settle → `effect({ at, radius })` |
|
|
1559
|
-
| melee | `queryArc` + reach from `getStat` → `effect` per hit |
|
|
1560
|
-
| MMO cast | `getTarget(from)` → `stats.delta(mana)` → `effect({ to })` |
|
|
1561
|
-
| consumable | `effect({ to: from, effect: "heal", via: { amount: -n } })` |
|
|
2219
|
+
Do not communicate all states with background-color changes alone. Use appropriate combinations of:
|
|
1562
2220
|
|
|
1563
|
-
|
|
2221
|
+
- scale compression
|
|
2222
|
+
- position shift
|
|
2223
|
+
- edge or glow response
|
|
2224
|
+
- mask movement
|
|
2225
|
+
- icon movement
|
|
2226
|
+
- text response
|
|
2227
|
+
- brief particles
|
|
2228
|
+
- sound
|
|
2229
|
+
- haptics when supported
|
|
2230
|
+
- controlled shake only when appropriate
|
|
1564
2231
|
|
|
1565
|
-
|
|
2232
|
+
Focus must look authored while remaining accessible. Menus should support keyboard/controller-style focus navigation when practical.
|
|
1566
2233
|
|
|
1567
|
-
|
|
2234
|
+
## Rendering — post-processing, lighting, shadows
|
|
1568
2235
|
|
|
1569
|
-
`@jgengine/core/
|
|
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:
|
|
1570
2237
|
|
|
1571
|
-
|
|
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.
|
|
1572
2242
|
|
|
1573
|
-
|
|
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.
|
|
1574
2244
|
|
|
1575
|
-
|
|
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.
|
|
1576
2246
|
|
|
1577
|
-
|
|
2247
|
+
## Settings menu
|
|
1578
2248
|
|
|
1579
|
-
|
|
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`):
|
|
1580
2250
|
|
|
1581
|
-
|
|
2251
|
+
- `variant: "panel" | "sheet" | "sidebar" | "fullscreen"` — the layout + skin (default `panel`; `sheet` is the mobile bottom-sheet). All four are fixed-size (no shrink-to-content jitter) and read the game's `--jg-*` theme tokens, falling back to a neutral dark skin.
|
|
2252
|
+
- `actions: SettingsActionDef[]` — game-state actions (Restart, Quit to menu, …). They become the **first "Game" tab, shown before anything else** — the home for buttons that used to float over the HUD. Each: `{ id, label, kind?: "default"|"danger", description?, run(ctx) }`; the menu closes right after `run`.
|
|
2253
|
+
- `hideBindings: string[]` — input actions to drop from the rebindable Controls list. A game-state key like `restart` belongs in `actions`, not the rebind grid — hide it here so it stops showing up as a "rebindable" control.
|
|
2254
|
+
- `surface: "quick"` — additionally mount compact on-screen volume/graphics buttons. Omit for none. `settings: false` — off entirely.
|
|
2255
|
+
- `extra: GameSettingDef[]` — append rows to any category, built-in *or a brand-new one* named by `category` (any string). Each row: `{ id, label, category, kind: "slider"|"toggle"|"select", default, min?, max?, step?, options?, onChange?(value, ctx) }`.
|
|
2256
|
+
- `categories: SettingCategoryDef[]` — declare custom category tabs, or relabel/reorder built-ins (`{ id, label, order? }`).
|
|
2257
|
+
- `hide: SettingCategory[]` — drop built-in categories.
|
|
1582
2258
|
|
|
1583
|
-
|
|
2259
|
+
**Game-state controls go in `actions`, never a floating button.** Restart/quit/new-game buttons stapled to the bottom of the HUD are the anti-pattern — declare them as `actions` (first Game tab) and place a `<SettingsTrigger>` inline. A contextual button on a win/lose *results* card is fine; a persistent game-state button pinned over live play is not.
|
|
1584
2260
|
|
|
1585
|
-
|
|
1586
|
-
lootTable({ id, rolls?, entries: [{ item? | currency?, count: n | [min,max], weight }] })
|
|
1587
|
-
ctx.game.loot.register(table) // in onInit
|
|
1588
|
-
ctx.game.loot.has(id) / roll(id, rng?) / grantToPlayer(userId, drops, source?)
|
|
1589
|
-
```
|
|
2261
|
+
**Present it any way you want.** `useSettings()` (`@jgengine/react`) returns the live controller — `{ categories, actions, variant, surface, isOpen, open, close, setOpen }` — so a game can drive its own pause-menu button, or render `categories`/`actions` (rows carry `value`/`set`/bounds, keybinds carry `rebind`/`reset`) entirely inside its own HUD. `useHasSettings()` gates a custom entry; `useSetting(id, fallback)` reads/writes one value. Set a slider's `min`/`max` explicitly — an omitted range collapses the thumb to 0/1.
|
|
1590
2262
|
|
|
1591
|
-
|
|
2263
|
+
## 8. Motion and game feel
|
|
1592
2264
|
|
|
1593
|
-
|
|
1594
|
-
Pure, renderer-free structures for card, board, and deckbuilder games — they sit **beside** the slot inventory, not in place of it. All are immutable-reducer + thin-controller pairs, mirroring the two-tier ctx/factory model: use the `create*` controller in game code, reach for the exported pure functions (`draw`, `moveCards`, `tickTimeline`, `laneAggregate`, `runPipeline`, `placeShaped`) for unit tests and headless servers.
|
|
1595
|
-
```ts
|
|
1596
|
-
// cards/cardPile — named ordered zones (deck/hand/discard/exhaust); seeded shuffle, hand limit, reshuffle-on-empty
|
|
1597
|
-
const pile = ctx.game.cards.pile("deck", { zones: ["deck","hand","discard","exhaust"], drawFrom:"deck", handZone:"hand", discardTo:"discard", handLimit:7, reshuffleFrom:"discard" });
|
|
1598
|
-
pile.reset(createCardPileState(pileConfig, { deck: ids })); // seed zone contents once, from onInit
|
|
1599
|
-
pile.shuffle("deck", seed); // seeded Fisher–Yates via pileRng — deterministic under the same seed
|
|
1600
|
-
pile.draw(5); // deck → hand, clamped to handLimit, reshuffles discard when deck runs dry
|
|
1601
|
-
pile.discard(ids); pile.exhaust(ids, "exhaust"); // Slay the Spire / Balatro lifecycle
|
|
1602
|
-
// cards/modifierPipeline — ordered { source, apply(value) → value } with an inspectable per-step trace
|
|
1603
|
-
const score = runPipeline({ chips: 10, mult: 1 }, jokers); // score.value + score.trace[i].{before,after,changed} for Balatro-style scoring readouts
|
|
1604
|
-
// board/laneBoard — N lanes, per-side power aggregate + optional per-lane LaneRule modifier (Marvel Snap / Inscryption)
|
|
1605
|
-
board.aggregate(lane, "player").total; board.outcome(lane).winner; board.lanesWon();
|
|
1606
|
-
// board/timelineBoard — N slots each on an independent cooldown, resolving in expiry order (The Bazaar auto-battlers)
|
|
1607
|
-
board.tick(dtMs); // → fires[] sorted by expiry time then slot index; multiple fires per slot per tick
|
|
1608
|
-
// inventory/shapedGrid — polyomino footprints, rotate, overlap-check, adjacency (Backpack Hero / Tetris inventory)
|
|
1609
|
-
placeShaped(grid, { id, value, footprint }, [col,row], rotation); // rotateFootprint / canPlace guard overlap + bounds
|
|
1610
|
-
gridAdjacencyQuery(grid).neighborsOf(id); // feeds synergy effects
|
|
1611
|
-
```
|
|
1612
|
-
Reuse the engine's seeded RNG (`pileRng`) for anything random — never `Math.random()` in game logic. The React drag/rotate/drop/snap gesture layer over these lives in `@jgengine/react` (see UI section).
|
|
1613
|
-
|
|
1614
|
-
`ctx.game.cards.pile(id, config?)` is the runtime-wired accessor for `createCardPile`: lazily creates the pile on first call (`config` required then) or returns the existing one for `id` on every later call, and every mutation notifies `ctx.subscribe`/bumps `ctx.version()` — so a `useEngineState`-bound hand/discard view re-renders without a game-owned store. Reach for `createCardPile` directly only for a headless test or server; game code goes through `ctx.game.cards.pile`.
|
|
1615
|
-
|
|
1616
|
-
## Puzzle primitives — cell grids and falling pieces
|
|
1617
|
-
|
|
1618
|
-
Two pure, renderer-free `@jgengine/core` primitives for cell-based puzzle games (Tetris wells, match-3 boards); tile art and the drop-cadence loop are the shell's/game's job.
|
|
1619
|
-
|
|
1620
|
-
- **`puzzle/cellGrid`** — a generic immutable `CellGrid<T>` for uniform typed-cell boards. Row 0 is the top; `y` grows downward. `createCellGrid`, `cellAt`, `withCell`/`withCells` (immutable single/batch writes), `fullRows`/`clearRows` (line-clear + compaction), `collapseColumns` (match-3 cascade gravity), `findRuns` (run detection with an optional custom matcher).
|
|
1621
|
-
- **`puzzle/fallingPiece`** — the falling-piece layer over a `CellGrid`: `ShapeTable<TShape>` maps rotation states to cell offsets; `pieceCells`/`pieceCollides`/`mergePiece` place, test, and commit a piece; `dropDistance` computes the ghost-piece landing row; `gravityInterval`/`levelForLines`/`lineScore` are the classic Tetris drop-speed/level/score curves (overridable); `createLockDelay`/`stepLockDelay` is the grounded→countdown→lock stepper (`delaySeconds: 0` locks instantly on touchdown).
|
|
1622
|
-
- **`tactics/fallingGrid`** — a generic tile-drop grid over any `TCell` payload (distinct from the `cellGrid`/`fallingPiece` row-clear pair): `createFallingGrid(config)`, `gravityIntervalMs(level, config?)` for the drop-speed curve, and a `FallingGridSnapshot`/`LockState` shape for the grounded→lock stepper.
|
|
1623
|
-
|
|
1624
|
-
## Dropped items — `worldItem` and the loot filter
|
|
1625
|
-
A `worldItem` is a scene **entity** (position + item ref + rarity), never an inventory item or object — see the three buckets. `onDeath.dropMode: "world"` (above) is the usual producer; games can also hand-place ground loot (chests, quest drops).
|
|
1626
|
-
ctx.scene.worldItem.spawn({ itemId, position, rarity?, baseType?, count?, affixTier?, source? })
|
|
1627
|
-
ctx.scene.worldItem.get(instanceId) / list() / nearestInRadius(from, radius, filter?)
|
|
1628
|
-
ctx.scene.worldItem.pickup(instanceId, userId) // grants to inventory + despawns, emits worldItem.picked_up
|
|
1629
|
-
Click-to-grab is engine-owned: setting `pointer.grabWorldItems: true` in `defineGame({...})` makes `@jgengine/shell`'s `GamePlayerShell` resolve `pointer.worldHit()` on primary click, and — when the hit entity is a `worldItem` within the `worldItem.pickupRadius` (default `DEFAULT_PICKUP_RADIUS`) configured on `defineGame({...})` of the local player — calls `pickup` directly, no game command needed. `@jgengine/react`'s `useWorldItems()` / `useNearestWorldItem(radius)` drive a HUD pickup prompt off the same store.
|
|
1630
|
-
Presentation is a two-layer render binding, both engine-owned (rendered by `@jgengine/shell`'s `WorldItems`) over **game-supplied data**:
|
|
1631
|
-
1. **Rarity baseline** — the `worldItem.rarityStyle: Record<rarity, { color?, beam?, label? }>` field of `defineGame({...})`, the game's rarity palette (Borderlands/Diablo-style beam + color coding).
|
|
1632
|
-
2. **Loot filter overlay** (#33) — the `worldItem.filter: LootFilterRule[]` field of `defineGame({...})`, built with `lootFilter([{ id, when: { rarity?, baseType?, minAffixTier?, maxAffixTier? }, hide?, color?, beam?, label? }])` from `game/lootFilter`. **First matching rule wins** (PoE/Last Epoch block semantics); a rule only overrides the fields it sets, everything else falls back to the rarity baseline. `resolveWorldItemPresentation(item, rarityStyle, rules)` composes both layers and is what the shell calls per item.
|
|
1633
|
-
## Gear systems — durability, affixes, modular items, storage tiers
|
|
1634
|
-
Four pure primitives that hang off item **instances** (not the stackable catalog id) — all catalog-first (specs are game-supplied config) and renderer-free. Item instances that carry durability/affix/modular state key off a game-assigned instance id, the same way targeting keys off entity instance ids.
|
|
1635
|
-
**Durability** (`item/durability`) — per-instance wear + repair. `DurabilitySpec` (`{ max, wearPerUse?, wearPerHit?, disableAtZero?, repair? }`) is catalog data; `createDurability(spec)` seeds a `DurabilityState`, `wear(spec, state, "use" | "hit", times?)` decrements (floors at 0), `isDisabled(spec, state)` gates use at zero, `durabilityFraction` feeds a HUD bar. Repair is quote-then-apply: `repairQuote(spec, state, { station?, to? })` returns the `{ item, count }[]` material cost (scaled by points restored) + the post-repair state (optional `qualityLossPerRepair` shrinks `max` each repair, Tarkov-style) — the game charges the materials through inventory, then commits the quote's `state`. `createDurabilityTracker()` keeps `DurabilityState` per instance id for the runtime.
|
|
1636
|
-
**Affix roller** (`item/affix`) — procgen `base × rarity → { rolled affixes, computed stats, name }`. `createAffixRoller({ pools, rarities })` over rarity-weighted `AffixPool`s. `roll(base, rarityId, rng)` draws `affixCount` distinct affixes without replacement (weighted, via the engine's `pickWeighted`), computes stats (base × `rarity.statScale`, then `op: "add"` affixes, then `op: "mul"`), and composes a name from `rarity.namePart` + prefix/suffix parts. `rollRarity(rng)` picks a weighted tier; `rollRandom(base, rng)` chains both. Pass `seededRng(seed)` for deterministic drops; any `() => number` rng works (same contract as `loot.roll`). `seededRng` lives in `random/rng` (re-exported here) alongside `seededStreams(seed)`, which derives independent named streams from one seed — `streams("worldgen")` vs `streams("history")` — so simulation draws never perturb generation (intervening in a run cannot change the map).
|
|
1637
|
-
**Modular item** (`item/modularItem`) — a whole assembled from parts in typed mount slots (guns, mechs). `ModularItemDef` has `slots: MountSlotDef[]` (`{ id, accepts, required? }`); `install(def, installed, slotId, part)` validates the slot exists, accepts the part's `category`, and is empty; `computeEffectiveStats(def, installed)` rolls part `stats` (additive) then `multipliers` over `baseStats`; `missingRequiredSlots`/`isComplete` gate a buildable whole. `createModularItem(def)` is the stateful wrapper (`install`/`uninstall`/`effectiveStats`/`partInSlot`).
|
|
1638
|
-
**Storage tiers + insurance** (`inventory/storageTier`) — the extraction-economy inventory half. Inventory containers carry a `tier: "carried" | "banked"` (`InventoryDeclaration.tier`; a Tarkov secure container is just a `banked` container on the body). `partitionOnDeath(containers)` splits a death snapshot into `{ kept, lost }` (banked survives, carried is dropped, stacks merged). `createDeliveryQueue()` is the delayed-delivery (insurance) hook: `schedule` a `ScheduledDelivery` with a game-time `deliverAt`, then `due(now)` / `claimDue(now)` drain it on the tick clock. `insureLost(lost, policy, userId, now, rng?)` filters the lost set to insured items and stamps a delayed `deliverAt` → feed straight into the queue. `resolveConsolation(policy, partition)` returns a baseline loadout id (apply via `applyLoadout`) — the death consolation grant, optionally gated on `if-carried-empty`. *(Session/round machines — extraction hold-to-leave, raid banking — consume this tier; see the objective-machine group.)*
|
|
1639
|
-
## Objective, round & session machines
|
|
1640
|
-
Content-agnostic state machines for competitive/session shapes — plant/defuse, buy/live/end rounds, downed/revive, the battle-royale ring, extraction raids, run-vs-meta persistence. All pure `core`; every timer takes a **game-time** `dt`/`now` (`ctx.time`), so pause and fast-forward apply for free. Drive them from `loop.onTick` and pipe their events into `ctx.game.feed`/`events`; render their snapshots as HUD (per the UI quality bar in [`reference/ui-react.md`](reference/ui-react.md) — the downed banner, ring warning, and extraction timer are required HUD).
|
|
1641
|
-
**Contested channel** (`session/contestedChannel`) — the interrupt-on-damage progress objective behind plant/defuse, cash-out, urn deposit, banishing, and hold-to-extract. `createContestedChannel({ duration, interruptOnDamage?, resetOnInterrupt?, favorability?, ratePerOccupant?, contested?, decayRate? })`: `start(team)` begins the channel, `tick(dt, occupants)` advances it against per-team occupancy (`Record<teamId, count>`) and emits `start`/`tick`/`contested`/`paused`/`complete` events, `damage(reason?)` interrupts (keeps or zeroes progress per `resetOnInterrupt`). `favorability[team]` scales fill rate (Deadlock deposit); `ratePerOccupant` fills faster with more owners present; `contested: "pause" | "decay"` chooses whether an opposing occupant freezes or reverses progress (The Finals contest). The owner leaving pauses it. Extraction hold-to-leave reuses this primitive verbatim.
|
|
1642
|
-
**Round state** (`session/roundState`) — the buy→live→end match machine (Valorant/CS). `createRoundState({ phases, teams, phaseOrder?, winCondition?, maxRounds?, winReward?, lossBonus? })`: `tick(dt)` runs the phase timer and auto-advances (emitting `phase.start`/`phase.end`, rolling the last phase back into the next round's first), `concludeRound(winner)` records the win on any "conclude-eligible" phase (any phase but the first/last in the cycle), settles `round.economy` (winner gets `winReward`, losers get an escalating `lossBonus` via `lossBonusFor(rule, streak)` clamped to `max`), and moves to the next phase. `onPhaseEnd(hook)` fires commerce/spawn gates on each transition; `match.end` fires at `maxRounds`. `server.mode` stays a game string — this is the timer/economy engine under it.
|
|
1643
|
-
|
|
1644
|
-
Two extras beyond the default buy/live/end cycle: `phaseOrder?: string[]` overrides the phase names/cycle entirely (a wider `Record<string, number>` `phases` shape to match) — a draft→ban→play→score cycle is the same machine with different phase names. `teams: (string | { id, role? })[]` accepts a plain id or a `{ id, role }` pair; `roleOf(team)` reads the tag back (`"attacker"`/`"defender"`, Valorant side assignment) without a parallel lookup table. `winCondition?: (snapshot: RoundSnapshot) => string | null` lets `evaluate()` (call it from `onTick` alongside `tick(dt)`) auto-conclude the round the instant a score/objective condition is met, instead of the game hand-calling `concludeRound` — return a team id to end it, `null` to keep playing; `RoundSnapshot` is `{ round, phase, timeLeft, scores, lossStreaks, roles, matchOver }`.
|
|
1645
|
-
**Role assignment** (`session/roles`) — `assignRoles(players, specs: RoleSpec[])` distributes fixed-count or proportional roles (hider/seeker, spy/operative, prop/hunter) across a player list — the allocation half of an asymmetric session mode; `RoundConfig.teams`' per-team `role` is the lighter-weight alternative when a round machine already tracks the roster.
|
|
1646
|
-
**Downed / revive** (`combat/downed`) — the 3-state alive→downed→dead chain (Apex/Helldivers). `createDownedState({ bleedoutSeconds, reviveSeconds?, reviveHealthFraction?, banner? })`: `down(id)` starts the bleedout, `tick(dt)` counts it down (→ `died`, optionally spawning a `banner`), `revive(id, dt)` accumulates an ally's hold time (→ `revived` with the health fraction the game restores), `finish(id)` executes a downed enemy, and `respawnFromBanner(id)` brings a banner-holder back at a beacon. It sits **in front of** the engine death resolution: on lethal damage call `down` instead of dying; on `died`/`bleedout` run the real `resolveDeath`. No banner ⇒ death is terminal.
|
|
1647
|
-
**Shrinking ring** (`session/ring`) — the battle-royale safe zone with out-of-bounds DoT. A catalog `RingConfig` is `{ center, phases: RingPhase[] }` where each phase is `{ startTime, shrinkDuration, fromRadius, toRadius, damagePerSecond, center? }` on the game clock. `ringSampleAt(config, t)` / `createRing(config).at(t)` returns the live `{ center, radius, damagePerSecond, shrinking }` (radius/center interpolate during each shrink window, hold between phases); `isOutside(t, pos)` / `distanceOutside(t, pos)` test a point, and `damageOutside(t, dt, positions)` returns per-entity `{ id, damage }` for everyone beyond the wall — feed those into `scene.entity.stats.delta`/`effect` each tick.
|
|
1648
|
-
**Extraction session** (`session/extraction`) — the raid-scoped "reach an extract and leave to bank what you carried" wrapper (Tarkov/DMZ/Helldivers), composed from the contested channel + `inventory/storageTier`. `createRaidSession({ extracts, insurance?, consolation? })`: `beginExtract(userId, extractId, team?)` opens a hold-to-leave channel, `tickExtract`/`damage` drive it, and on completion `resolveExtraction(userId, containers)` banks everything carried. `resolveDeath(userId, containers, now, rng?)` runs `partitionOnDeath` (banked kept, carried lost), schedules insured items through the built-in delivery queue (`claimDeliveries(now)` drains it on the clock), and yields the consolation loadout id. `playerSnapshot(userId)` feeds the extraction-timer HUD.
|
|
1649
|
-
**Persistence scopes** (`runtime/persistenceScope`) — the run-vs-meta split with explicit reset boundaries (Icarus mission wipe, Once Human season reset). `partitionScopes(state, { run })` splits a flat record into `{ meta, run }` by key; `resetRun` clears the run half while meta (talents/blueprints/account currency) survives; `clearRunFields(playerRow, runFields)` and `applyRunReset(profile, runFields, now)` do the same over `RuntimePlayerRow`/`PlayerProfileRecord`. `planScenarioReset({ gameId, serverId?, wipeChunks?, wipeServerSession?, resetPlayers?, runFields? })` normalizes a scenario/season reset that `HostPersistence.resetScenario?(reset)` applies — `@jgengine/sql` implements it (deletes the server's chunks + session, run-resets each profile in one transaction), keeping account meta intact.
|
|
1650
|
-
|
|
1651
|
-
## Trade
|
|
1652
|
-
|
|
1653
|
-
Catalog `trade` fields drive everything — no duplicate price lists.
|
|
2265
|
+
Add purposeful motion for:
|
|
1654
2266
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
2267
|
+
- screen entry and exit
|
|
2268
|
+
- confirm and cancel
|
|
2269
|
+
- warnings
|
|
2270
|
+
- score increases
|
|
2271
|
+
- objective updates
|
|
2272
|
+
- damage
|
|
2273
|
+
- victory and failure
|
|
2274
|
+
- countdowns
|
|
2275
|
+
- pause
|
|
2276
|
+
- item pickup
|
|
1662
2277
|
|
|
1663
|
-
|
|
2278
|
+
Motion should be brief, readable, interruptible when necessary, coordinated, consistent with the game’s art direction, and respectful of reduced-motion settings.
|
|
1664
2279
|
|
|
1665
|
-
|
|
1666
|
-
ctx.game.economy.balance(userId, currencyId) / grant(...) / charge(...) // charge → { reason } | null
|
|
1667
|
-
ctx.game.unlocks.has(userId, id) / grant(userId, id) / list(userId) / tree(categoryId)
|
|
1668
|
-
```
|
|
2280
|
+
Do not animate everything constantly. Motion communicates hierarchy, cause and effect, urgency, and state changes.
|
|
1669
2281
|
|
|
1670
|
-
|
|
2282
|
+
## 9. Mobile controls are genre-authored
|
|
1671
2283
|
|
|
1672
|
-
|
|
2284
|
+
Shared input mechanics may remain shared. Their visual treatment and arrangement must match the game.
|
|
1673
2285
|
|
|
1674
|
-
|
|
2286
|
+
Examples:
|
|
1675
2287
|
|
|
1676
|
-
**
|
|
2288
|
+
**Driving**
|
|
2289
|
+
Steering region or wheel, accelerator, brake, handbrake, optional camera/map control.
|
|
1677
2290
|
|
|
1678
|
-
**
|
|
2291
|
+
**Stealth**
|
|
2292
|
+
Movement zone, sneak/crouch hold, contextual interaction, temporary map/schedule control.
|
|
1679
2293
|
|
|
1680
|
-
**
|
|
2294
|
+
**Shooter**
|
|
2295
|
+
Movement zone, aim region, fire/action cluster, weapon or ability controls.
|
|
1681
2296
|
|
|
1682
|
-
**
|
|
2297
|
+
**Puzzle/arcade**
|
|
2298
|
+
Direct drag, tap, swipe, paddle region, or discrete directions. Do not add a joystick without a gameplay reason.
|
|
1683
2299
|
|
|
1684
|
-
|
|
2300
|
+
Requirements:
|
|
1685
2301
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
2302
|
+
- never cover critical HUD information
|
|
2303
|
+
- use the game’s shape and material language
|
|
2304
|
+
- fade training labels after learning
|
|
2305
|
+
- consider thumb reach
|
|
2306
|
+
- preserve accessible target sizes
|
|
2307
|
+
- visually respond to activation
|
|
2308
|
+
- support optional scaling where appropriate
|
|
1690
2309
|
|
|
1691
|
-
|
|
2310
|
+
Do not use one generic translucent controller across all games.
|
|
1692
2311
|
|
|
1693
|
-
|
|
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.
|
|
1694
2316
|
|
|
1695
|
-
|
|
1696
|
-
ctx.game.quest.register(catalog) // onInit
|
|
1697
|
-
canAccept / accept / abandon / canTurnIn / turnIn / grant / revoke
|
|
1698
|
-
progress(userId, questId, objectiveId, delta)
|
|
1699
|
-
list(userId) / has(questId)
|
|
1700
|
-
bind("entity.died") // kill objectives match objective.target === catalogId
|
|
1701
|
-
bind("inventory.added") // collect objectives match objective.item
|
|
1702
|
-
```
|
|
2317
|
+
## 10. Progressive instruction
|
|
1703
2318
|
|
|
1704
|
-
|
|
2319
|
+
Do not leave large control grids visible during gameplay.
|
|
1705
2320
|
|
|
1706
|
-
|
|
2321
|
+
Prefer:
|
|
1707
2322
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2323
|
+
- contextual prompts
|
|
2324
|
+
- brief onboarding
|
|
2325
|
+
- first-use hints
|
|
2326
|
+
- a controls screen
|
|
2327
|
+
- pause-menu reference
|
|
2328
|
+
- icons attached to actions
|
|
2329
|
+
- progressive disclosure
|
|
1715
2330
|
|
|
1716
|
-
|
|
2331
|
+
Desktop keyboard legends must not appear on touch devices. Prompts should appear near the relevant action, object, or HUD region and clear when no longer useful.
|
|
1717
2332
|
|
|
1718
|
-
|
|
2333
|
+
## 11. Reference directions for flagship games
|
|
1719
2334
|
|
|
1720
|
-
|
|
2335
|
+
### Clockwork Heist
|
|
1721
2336
|
|
|
1722
|
-
|
|
2337
|
+
Use gentleman-thief mechanical field-kit language: watch geometry, midnight enamel, aged brass, ivory paper, engraved labels, mechanical shutters, and an authored schedule timeline. The timer should feel like a clock. The schedule should be an on-demand pocket-watch or dossier panel. Touch controls should use brass/enamel construction. Restart belongs in pause, not permanently over the world.
|
|
1723
2338
|
|
|
1724
|
-
|
|
1725
|
-
ctx.game.chat.send(fromUserId, channelId, body) // → { message, recipients } | { reason }
|
|
1726
|
-
ctx.game.chat.whisper(fromUserId, toUserId, body) // stable per-pair channel "whisper:<a>:<b>"
|
|
1727
|
-
ctx.game.chat.history(channelId, { limit?, viewerUserId? }) // viewer filter drops blocked senders
|
|
1728
|
-
ctx.game.chat.register({ id, kind, radius?, historyLimit?, rateLimit? }) // custom channels
|
|
1729
|
-
ctx.game.chat.channels() / snapshot() / hydrate(data)
|
|
1730
|
-
```
|
|
2339
|
+
### Canyon Chase
|
|
1731
2340
|
|
|
1732
|
-
|
|
2341
|
+
Use desert pursuit language: battered dashboard, analog instruments, radio display, route strip, warning lamps, and road-sign typography. Target gap should read as a pursuit gauge. Border progress should resemble an odometer, route strip, or mile marker. Driving controls should feel like steering, throttle, brake, and handbrake—not generic circles.
|
|
1733
2342
|
|
|
1734
|
-
|
|
2343
|
+
### Brick Breaker
|
|
1735
2344
|
|
|
1736
|
-
|
|
2345
|
+
Use arcade-cabinet language: bezel framing, CRT/vector treatment, arcade numerical readouts, attract mode, launch feedback, and brief level overlays that clear before play. Pause and results should feel like cabinet states, not web dialogs.
|
|
1737
2346
|
|
|
1738
|
-
|
|
1739
|
-
ctx.player.cosmetics.register(defs) // onInit — Record<loadoutId, { slots: Record<slot, cosmeticId> }>
|
|
1740
|
-
ctx.player.cosmetics.apply(userId, loadoutId) // merges the preset's slots
|
|
1741
|
-
ctx.player.cosmetics.equip(userId, slot, cosmeticId | null) // set/clear one slot directly
|
|
1742
|
-
ctx.player.cosmetics.get(userId) // Record<slot, cosmeticId>
|
|
1743
|
-
```
|
|
2347
|
+
These games must remain structurally distinct, not recolors of one component set.
|
|
1744
2348
|
|
|
1745
|
-
|
|
2349
|
+
## 12. Accessibility and performance
|
|
1746
2350
|
|
|
1747
|
-
|
|
2351
|
+
Maintain:
|
|
1748
2352
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
2353
|
+
- sufficient contrast
|
|
2354
|
+
- readable text size
|
|
2355
|
+
- keyboard navigation
|
|
2356
|
+
- controller navigation where available
|
|
2357
|
+
- reduced-motion support
|
|
2358
|
+
- visible authored focus
|
|
2359
|
+
- touch target sizing
|
|
2360
|
+
- semantic labels where practical
|
|
2361
|
+
- responsive scaling
|
|
2362
|
+
- reasonable DOM and animation performance
|
|
1754
2363
|
|
|
1755
|
-
|
|
2364
|
+
Use blur, masks, textures, filters, and full-screen effects carefully, especially on mobile.
|
|
1756
2365
|
|
|
1757
|
-
##
|
|
2366
|
+
## 13. Screenshot verification is mandatory
|
|
1758
2367
|
|
|
1759
|
-
|
|
1760
|
-
ctx.scene.entity.form.register(defs) // onInit — FormDef[] = { id, movement?, abilities?, model? }
|
|
1761
|
-
ctx.scene.entity.form.shapeshift(instanceId, formId, durationSeconds?) // → null | { reason }
|
|
1762
|
-
ctx.scene.entity.form.active(instanceId) // → formId | null
|
|
1763
|
-
ctx.scene.entity.form.abilities(instanceId) // → readonly string[] | null
|
|
1764
|
-
ctx.scene.entity.form.revert(instanceId) // early revert
|
|
1765
|
-
```
|
|
2368
|
+
Capture and inspect meaningful states:
|
|
1766
2369
|
|
|
1767
|
-
|
|
2370
|
+
- desktop title screen
|
|
2371
|
+
- desktop gameplay
|
|
2372
|
+
- mobile landscape
|
|
2373
|
+
- mobile portrait where supported
|
|
2374
|
+
- pause
|
|
2375
|
+
- victory or failure
|
|
2376
|
+
- an interaction prompt
|
|
2377
|
+
- touch controls in active use
|
|
1768
2378
|
|
|
1769
|
-
|
|
2379
|
+
Inspect for:
|
|
1770
2380
|
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2381
|
+
- overlap and clipping
|
|
2382
|
+
- weak contrast
|
|
2383
|
+
- unreadable scale
|
|
2384
|
+
- excessive cards
|
|
2385
|
+
- website-like composition
|
|
2386
|
+
- broken safe areas
|
|
2387
|
+
- conflicting hierarchy
|
|
2388
|
+
- browser chrome interference
|
|
2389
|
+
- poor thumb reach
|
|
2390
|
+
- keyboard instructions on touch devices
|
|
2391
|
+
- inconsistent art direction
|
|
1779
2392
|
|
|
1780
|
-
|
|
2393
|
+
Revise after inspection. Typechecking is not visual proof.
|
|
1781
2394
|
|
|
1782
|
-
##
|
|
2395
|
+
## 14. Rejection criteria
|
|
1783
2396
|
|
|
1784
|
-
|
|
1785
|
-
ctx.game.store.set("health", 100) // any key, any value type
|
|
1786
|
-
ctx.game.store.get("health") // T | undefined
|
|
1787
|
-
ctx.game.store.has("health")
|
|
1788
|
-
ctx.game.store.delete("health")
|
|
1789
|
-
ctx.game.store.subscribe(listener) // change-signal fires on set/delete
|
|
1790
|
-
ctx.game.store.mapSnapshot() / arraySnapshot()
|
|
1791
|
-
```
|
|
2397
|
+
Require revision when any of these are true:
|
|
1792
2398
|
|
|
1793
|
-
|
|
2399
|
+
- normal site navigation remains visible during active gameplay
|
|
2400
|
+
- the player must scroll the page to use the game
|
|
2401
|
+
- the game is presented inside a normal content card
|
|
2402
|
+
- essential HUD is covered by touch controls
|
|
2403
|
+
- controls overlap each other
|
|
2404
|
+
- keyboard instructions appear on touch devices
|
|
2405
|
+
- large instruction panels remain visible during gameplay
|
|
2406
|
+
- more than three ordinary card-like panels are persistently visible
|
|
2407
|
+
- the main action looks like a standard website button
|
|
2408
|
+
- pause resembles a generic website modal
|
|
2409
|
+
- victory/failure is only text plus restart
|
|
2410
|
+
- restart is permanently visible without a gameplay reason
|
|
2411
|
+
- menu elements lack pressed, focused, selected, or disabled states
|
|
2412
|
+
- UI changes have no transition or feedback
|
|
2413
|
+
- generic virtual controls are used without genre adaptation
|
|
2414
|
+
- the theme only changes colors and fonts
|
|
2415
|
+
- unrelated UI elements have equal visual weight
|
|
2416
|
+
- mobile portrait technically fits but is not intentionally composed
|
|
2417
|
+
- important UI sits beneath safe areas
|
|
2418
|
+
- ordinary document flow determines the HUD layout
|
|
2419
|
+
- generic default styling is used because no art direction was written
|
|
1794
2420
|
|
|
1795
|
-
##
|
|
2421
|
+
## 15. Compact implementation API appendix
|
|
1796
2422
|
|
|
1797
|
-
|
|
2423
|
+
Use the main `jgengine` skill for the authoritative engine API routing. The React package exposes `GameProvider`, hooks, and headless primitives from `@jgengine/react` and its documented subpaths. Common hooks include player/game state, entities, stats, inventory, quests, prompts, clocks, markers, fog, and engine stores. The shell provides `GamePlayerShell`, input integration, devtools, and `GameUiPreview`.
|
|
2424
|
+
|
|
2425
|
+
Use these APIs to bind state; do not let API wiring dictate visual composition. Keybind labels should derive from the game’s binding table, and UI actions should dispatch through game commands rather than existing only as click handlers.
|
|
2426
|
+
|
|
2427
|
+
Headless components are not finished design. They are behavior and accessibility seams that the game must art-direct.
|
|
2428
|
+
|
|
2429
|
+
## Definition of done
|
|
2430
|
+
|
|
2431
|
+
UI work is complete only when:
|
|
2432
|
+
|
|
2433
|
+
- the game owns the viewport
|
|
2434
|
+
- site chrome is absent during active play
|
|
2435
|
+
- no document scrolling is required
|
|
2436
|
+
- HUD and control layers have clear responsibilities
|
|
2437
|
+
- mobile controls do not cover critical content
|
|
2438
|
+
- touch controls match the genre and game identity
|
|
2439
|
+
- title, pause, and results screens feel authored
|
|
2440
|
+
- interaction states and transitions are present
|
|
2441
|
+
- screenshots have been reviewed and revised
|
|
2442
|
+
- the implementation remains accessible and performant
|
|
2443
|
+
- future generated UI is explicitly prevented from falling back to generic website-card styling
|
|
2444
|
+
|
|
2445
|
+
---
|
|
2446
|
+
name: jgengine-world
|
|
2447
|
+
description: World API: movement, cameras, physics, maps, sensors, spawn placement.
|
|
2448
|
+
---
|
|
2449
|
+
|
|
2450
|
+
# jgengine-world
|
|
1798
2451
|
|
|
1799
2452
|
## Movement, pose, input
|
|
1800
2453
|
|
|
@@ -1833,7 +2486,7 @@ movement: {
|
|
|
1833
2486
|
|
|
1834
2487
|
**Vertical motion intents** — `ctx.player.motion` (`@jgengine/core/runtime/motionIntents`): `impulse(vy)` adds to the vertical velocity the shell's controller is about to integrate, `setVerticalVelocity(vy)` replaces it outright, `setY(y)` wins over physics for that frame. The shell calls `takePending()` once per frame, before integrating gravity, to drain what accumulated; this is not reactive state (jump pads, launch abilities, bounce pads).
|
|
1835
2488
|
|
|
1836
|
-
**`collision: { voxel: true }` — object lattice as solids.** When set, the shell's local player uses a voxel body whose `isSolid(x,y,z)` is rebuilt from `ctx.scene.object.list()` as exact `` `${x},${y},${z}` `` keys (integer cell queries). Integer-placed objects are walkable/blocking; fractional-coord objects decorate without colliding. Removing an object opens a real trapdoor under gravity — do not fake the fall with `setPose`. `visual.scale` does not shrink the collider (always a unit cell). The voxel body is created once; prefer `motion.setY` / `impulse` for vertical relocation — full XY teleport of the local voxel body is not supported. Solid cache rebuilds when object **count** changes. Recipe: `jgengine
|
|
2489
|
+
**`collision: { voxel: true }` — object lattice as solids.** When set, the shell's local player uses a voxel body whose `isSolid(x,y,z)` is rebuilt from `ctx.scene.object.list()` as exact `` `${x},${y},${z}` `` keys (integer cell queries). Integer-placed objects are walkable/blocking; fractional-coord objects decorate without colliding. Removing an object opens a real trapdoor under gravity — do not fake the fall with `setPose`. `visual.scale` does not shrink the collider (always a unit cell). The voxel body is created once; prefer `motion.setY` / `impulse` for vertical relocation — full XY teleport of the local voxel body is not supported. Solid cache rebuilds when object **count** changes. Recipe: `jgengine` → voxel trapdoor board.
|
|
1837
2490
|
|
|
1838
2491
|
**Respawn** — `ctx.scene.entity.spawnPoseOf(id)` reads the spawn pose, `resetToSpawn(id)` teleports back to it with zero velocity, `resetAllToSpawn(filter?)` does it for every entity matching an optional filter and returns the count (round resets, out-of-bounds recovery).
|
|
1839
2492
|
|
|
@@ -1865,7 +2518,7 @@ touch: {
|
|
|
1865
2518
|
- **`look` / `lookSensitivity`** — drag-to-look on the play surface; defaults to `true` for `first`-person camera rigs, `0.005` radians/px.
|
|
1866
2519
|
- **`touch: false`** — opt out entirely when the game's own DOM UI is already touch-native.
|
|
1867
2520
|
|
|
1868
|
-
`useDisplayProfile()` (`@jgengine/react/display`) reports `{ coarsePointer, compact, portrait }` — live media-query state, SSR-safe — for adaptive HUD layout; see the mobile/touch rules in [
|
|
2521
|
+
`useDisplayProfile()` (`@jgengine/react/display`) reports `{ coarsePointer, compact, portrait }` — live media-query state, SSR-safe — for adaptive HUD layout; see the mobile/touch rules in [`../jgengine-ui/reference.md`](https://github.com/Noisemaker111/jgengine/blob/main/.claude/skills/jgengine-ui/reference.md).
|
|
1869
2522
|
|
|
1870
2523
|
## Interaction — `proximityPrompt`
|
|
1871
2524
|
|
|
@@ -1884,10 +2537,11 @@ The **pointer is a service, not per-game glue**. Opt in with `camera` plus a `po
|
|
|
1884
2537
|
## AI — director, threat, jobs, crowds (`ai/*`)
|
|
1885
2538
|
Renderer-free AI over the same navmesh (`findPath`/`pathFollow`) gameplay already uses. Everything ticks on **game-time `dt`** (the `ctx.time` simClock delta), so it obeys pause and fast-forward for free. Manifests, patrol routes, job definitions, threat weights, and POIs are **game data** — the primitives own the loop, the catalog owns the content.
|
|
1886
2539
|
- **Spawn director** (`ai/spawnDirector`) — budgets and escalates spawns for wave shooters and difficulty directors (Brotato, Bloons TD 6, Risk of Rain 2, Helldivers 2, Deep Rock Galactic). `createSpawnDirectorState(config)` then pure `advanceSpawnDirector(config, state, dt, { alive, players? })` → `{ state, spawns: SpawnRequest[] }`. Each `WaveManifest` grants a `budget` spent on affordable weighted `SpawnEntry`s (`cost`/`weight`/`minWave`), capped by `maxAlive`; `duration` auto-advances waves (or call `advanceWave` on "wave cleared"). Budget also trickles via `budgetPerSecond`, ramps a difficulty curve with `escalationPerSecond` (grows with sim-time), scales with `playerBudgetPerSecond`, and surges on `raiseAlert(state, amount)` decaying over time (bug-breach/dropship escalation). Seeded (`seed`) so ticks are deterministic. `pickSpawnPoint(points, players, { roll, bias })` biases placement toward (or away from) players. `config.spawnPoints?: NavPoint[]` lets the director pick a point itself: each `SpawnRequest` then also carries `point` (the chosen `[x, z]`, biased by `config.spawnPointBias`) and `laneId` (the point's index into `spawnPoints`) — feed multiple named lanes/portals and read `laneId` back to route the spawned entity down its lane instead of correlating positions by hand.
|
|
1887
|
-
- **Threat table** (`ai/threat`) — MMO/extraction aggro (Escape from Tarkov, WoW-style tanking). `createThreatTable({ decayPerSecond?, max?, forgetBelow? })`: `add(source, amount)` accumulates, `decay(dt)` bleeds off per game-second and forgets emptied sources, `highest({ current?, stickiness? })` returns the top-threat source to feed `scene/targeting` — `stickiness` (e.g. 1.1) keeps the current target until another exceeds it by that factor, so aggro doesn't jitter. `ranked()` for a threat meter.
|
|
2540
|
+
- **Threat table** (`ai/threat`) — MMO/extraction aggro (Escape from Tarkov, WoW-style tanking). `createThreatTable({ decayPerSecond?, max?, forgetBelow? })`: `add(source, amount)` accumulates, `decay(dt)` bleeds off per game-second and forgets emptied sources, `highest({ current?, stickiness? })` returns the top-threat source to feed `scene/targeting` — `stickiness` (e.g. 1.1) keeps the current target until another exceeds it by that factor, so aggro doesn't jitter. `taunt(source, durationSeconds)` forces the mob onto a tank for that window (overriding `highest`) and lifts the taunter's threat to the current top so aggro stays after the window; `forcedTarget()` reports the active taunt, `decay(dt)` counts it down. `ranked()` for a threat meter.
|
|
1888
2541
|
- **Patrol** (`scene/behaviors`) — `patrol({ waypoints, speed, loop? })` is a `BehaviorDescriptor` (a route is data) that layers a fixed beat on top of `wander`; drive it with `createPathFollow`/`advancePathFollow` (lane creeps, scav patrols in Deadlock/Tarkov). Route waypoints between guard posts with `findPath`.
|
|
1889
2542
|
- **Job board** (`ai/jobBoard`) — colony/companion task assignment (Palworld stations, Schedule I employees, Sons of the Forest directives). `createJobBoard()`: `post(job)` a `JobDef` (`station`, `work` seconds, `priority`, `arriveRadius`, `repeat`), `claim(worker)` auto-pulls the highest-priority queued job or `assign(worker, jobId)` for a player order (steals it from its holder), `release` requeues. Per tick `advance(worker, dt, { distanceToStation })` runs the state machine `travelling → working → done` (path to `station(worker)` via `findPath`, occupy, run the loop), returning a `JobReport` on completion; `repeat` jobs re-run as a production loop and report each cycle.
|
|
1890
2543
|
- **Crowd flow** (`ai/crowd`) — many agents routing to their own points of interest with congestion (Two Point Museum corridors, Dave the Diver seating). `computeFlowField(grid, goals, { clearance?, congestion? })` runs Dijkstra from the goals over the walkable grid → `direction(point)`/`next(point)` steer any agent toward the nearest goal (no per-agent A*). `createCrowdField(grid)` tracks per-cell occupancy (`enter`/`leave`/`count`); pass `crowd.penalty(weight)` as the field's `congestion` to reroute flow around crowded cells each tick. `selectPoi(pois, from, { roll, occupancy?, distanceBias?, distance? })` weights a POI by appeal and proximity, skips ones at `capacity`, and accepts a `distance` override (e.g. `findPath` length) to choose over the navmesh, not line-of-sight.
|
|
2544
|
+
- **Factions & reputation** (`faction/factions`, `faction/reputation`) — allegiance-based hostility for RPGs/MMOs/strategy (WoW-style Horde-vs-Alliance, at-war reputation grinds, guards-vs-thieves). `createFactionGraph({ factions, symmetric?, unaligned? })` models a faction-vs-faction relation matrix — each `FactionDef` lists `relations` toward others (`"hostile" | "neutral" | "friendly"`) with `towardSelf`/`towardOthers` fallbacks; `relationBetween(a, b)` resolves it (mirroring undeclared directions when `symmetric`, defaulting `unaligned` for unknowns). `createFactionRoster(graph)` maps entities to factions (`assign`/`factionOf`/`members`), then `relationBetweenEntities`/`isHostile`/`hostilesOf(observer, candidates)` answer "who is my enemy" as a data lookup — feed it into `scene/targeting`'s `classify` and `ai/threat` eligibility instead of a per-game role-string check. `createReputationLedger({ tiers?, initial?, min?, max? })` is the per-actor standing ledger: `gain`/`set`/`standing` track reputation, `tier`/`relation` map it through `DEFAULT_REPUTATION_TIERS` (the classic Hated→Exalted ladder, or your own), `standings(actor)` snapshots all. `effectiveRelation({ base, ledger, actorId, factionId })` overlays a player's earned standing on the base faction relation, so a reputation grind flips a normally-hostile faction to neutral/friendly.
|
|
1891
2545
|
## Map, fog of war & ping
|
|
1892
2546
|
Minimap/world-map/fog/compass state is renderer-free core (`world/*`), the top-down terrain image bakes in the shell, and the minimap/compass/world-map are react components. Ping rides the existing party + feed — it is not a new channel.
|
|
1893
2547
|
- **Markers** (`world/markers`): `createMarkerSet()` is a reactive keyed set of `MapMarker { id, kind, position, label?, owner?, expiresAt?, meta? }` — `add`/`remove`/`get`/`list`/`query({ kind, owner, near, radius })`/`prune(now)`/`subscribe`. `kind` is a game-owned catalog string; `DEFAULT_MARKER_KINDS` (objective/enemy/loot/location/danger/ping/player/ally) supplies colors + glyphs the react map reads (override with your own `MarkerKindStyle` palette). Objective/entity/loot markers all live here.
|
|
@@ -1911,377 +2565,29 @@ Shell wiring: `@jgengine/shell/vision/RevealVision` (`RevealHighlights` — dept
|
|
|
1911
2565
|
|
|
1912
2566
|
## World features
|
|
1913
2567
|
|
|
1914
|
-
Renderer-free world surface — query primitives, environment fields + weather + realm composition, survival meters/moodles, interactive building & terraform, the optional headless physics world, vehicles/mounts/racing, and spawn placement. Full surface: **[reference
|
|
1915
|
-
|
|
1916
|
-
## Turn-based & tactics (renderer-free)
|
|
1917
|
-
|
|
1918
|
-
Pure-`core` primitives for turn-based, grid-tactics, and card games — every one is a stateful factory with matching pure math, and every stateful piece exposes `capture()`/`restore()` so it plugs straight into the snapshot store. Overlays and tile art are the shell's/game's job; these ship the logic.
|
|
1919
|
-
|
|
1920
|
-
- **`turn/turnLoop` — `createTurnLoop(config)`.** An initiative machine over an ordered participant list with optional `phases` and per-turn action-economy `pools`. `advanceTurn()` walks the order (round++ on wrap) and **resets the entering participant's pools**; `advancePhase()` steps phases then rolls into the next turn. Pools are catalog data (`{ id, max, start? }`) — a single Slay-the-Spire energy pool or BG3's Action/Bonus/Movement/Reaction set, spent independently via `spend/canSpend/gain/refill`. `setOrder`/`addParticipant`/`removeParticipant` re-roll initiative without losing the active pointer. `config.onTurnStart?(participantId)`/`onTurnEnd?(participantId)` fire on every `advanceTurn()` transition (start also fires once for the initial participant at construction) — hang status-effect ticks, "your turn" banners, or AI-turn kickoff here instead of diffing `state()` between ticks yourself. `ctx.game.turn.loop(id, config?)` is the runtime-wired accessor: lazily creates (config required the first call) or returns the existing notify-wrapped loop for `id`, so a HUD bound to `ctx.subscribe` re-renders on every turn/phase/pool change with no separate store to wire.
|
|
1921
|
-
- **`turn/commit` — `createCommitController({ mode })`**, also hosted at `turnLoop.commit`. Three commit modes: `immediate` (submit resolves now), `simultaneous` (sealed hidden submissions → `reveal()` once `allReady()`, deterministic order — Marvel Snap), and `rewind` (visible `pending()` → `rewind()` to discard or `commit()` to finalize).
|
|
1922
|
-
- **`turn/intent` — `createIntentBoard()`.** A minimal per-participant "what will you do" board, lighter than a full commit round: `declare(participantId, { kind, magnitude?, targetId?, note? })` records one intent per participant (overwriting any prior undeclared one), `peek(participantId)` reads without clearing, `all()` lists every declared `[participantId, intent]` pair (for an enemy-intent HUD row, Slay-the-Spire style), `consume(participantId)` reads and clears in one call, `clear(participantId?)` clears one or everyone. Reach for this when you need visible declared-but-not-yet-resolved intents (telegraphed enemy actions) without the full simultaneous-reveal machinery of `turn/commit`.
|
|
1923
|
-
- **`tactics/tacticalGrid` — `createTacticalGrid({ width, height, blocked?, diagonal?, world? })`.** Tile occupancy (one unit per tile), `reachable(from, budget)` flood-fill (respects walls + occupants), `path(from, to)` shortest route, and `push(id, dir, { distance, chain })` discrete knockback-to-tile — chained collisions transfer momentum through struck units (Into the Breach), or stop with a recorded `PushCollision` against `wall`/`edge`/another unit. `world: { origin: [x, z], tileSize }` (mirroring `navGrid`'s bounds+cellSize convention) turns on `worldToTile(x, z)` (world point → `Tile | null`, null outside the grid) and `tileToWorld(tile)` (a tile's world-space center) — the render/pointer-hit round trip between the tactics grid and the 3D scene; omit `world` for a grid used purely as abstract logic (both throw if called without it).
|
|
1924
|
-
- **`tactics/predictiveQuery` — `predictAreaEffect`/`predictArcEffect`/`predictTiles`.** A "would-this-effect-hit" query for pre-commit overlays and enemy-intent telegraphs. It reuses the **exact** AoE/LoS targeting behind `ctx.scene.entity.effect` (`combat/effects` `resolveAreaTargets`) so the predicted target set matches what the effect would actually drain — without committing any state change.
|
|
1925
|
-
- **`tactics/snapshot` — `createSnapshotStore()`.** Cheap, repeatable turn-undo: `register(id, slice)` any `capture()/restore()` slice (the grid, surfaces, and turn loop all qualify), then `capture()/restore()` a deep-cloned snapshot or use the `push()/pop()` undo stack. `deepClone` handles objects/arrays/Map/Set so a held snapshot is immune to later mutation.
|
|
1926
|
-
- **`tactics/surface` — `createSurfaceLayer({ kinds, reactions })`.** A stateful tile surface layer with its own `tick(dt)` (timed surfaces decay + expire) and a **combination matrix** — `reactions` is data (`{ when: [a, b], result }`), so grease+fire→fire and water+lightning→electrified are catalog entries, not hard-coded. Distinct from terrain/water; drive its tick from `onTick`'s game-time `dt`.
|
|
1927
|
-
|
|
1928
|
-
## External data — `data/dataSource` and the dev proxy
|
|
1929
|
-
|
|
1930
|
-
Renderer-free async-state primitives (`@jgengine/core/data`) for a game that reads a live external source (a leaderboard API, a session browser, remote config) — distinct from `ctx.game.store`/multiplayer, which are for the game's own authoritative state.
|
|
1931
|
-
|
|
1932
|
-
- **`createDataSource(load, options?)`** (`data/dataSource`) → `DataSource<T>` wraps one `load(signal)` async call as `{ status: "idle"|"loading"|"ready"|"error", data, error }`. `getState()` reads the current snapshot, `subscribe(listener)` fires on every change, `refresh({ force? })` re-runs `load` (de-duplicates a call already in flight unless `force`; aborts the prior call first when forced), `startPolling(intervalMs?)`/`stopPolling()` run `refresh` on an interval (`intervalMs` falls back to the one passed at construction; throws if neither is given), `dispose()` tears down polling and in-flight requests for good. Pass `options.clock` (`{ setInterval, clearInterval }`) to swap the timer source in tests.
|
|
1933
|
-
- **`fetchJson<T>(url, options?)`** (`data/fetchJson`) — `fetch` + JSON-parse in one call; throws `HttpStatusError` (`status`, `statusText`, `url`) on a non-OK response and `JsonParseError` (`url`, `cause`) on unparsable JSON, so a `DataSource`'s `error` is always one of these two typed shapes, never a bare `Error`. `options.fetchImpl` swaps the fetch implementation for tests/SSR.
|
|
1934
|
-
- **`createJsonDataSource<T>(url, options?)`** (`data/jsonDataSource`) — sugar combining the two above: a `DataSource<T>` whose `load` calls `fetchJson(url, options)`.
|
|
1935
|
-
- **Dev proxy (`data/devProxy`)** — same-origin routing for external APIs during `bun dev` so browser CORS never blocks a game's `fetchJson` call against a third-party host. `parseDevProxyTable(raw)` parses a `VITE_JGENGINE_DEV_PROXY` env value (a JSON object of `{ routeName: "https://api.example.com" }`) into a `DevProxyTable`; `proxiedUrl(target, { dev?, table?, prefix? })` rewrites a `target` URL whose prefix matches a table entry into `/proxy/<routeName>/<rest>` (default prefix `/proxy`) when `dev` is true (defaults to `import.meta.env.DEV`) — else returns `target` unchanged, so the same call hits the real host in production. `apps/dev`'s `vite.config.ts` reads the same env var and wires a matching Vite server `proxy` entry per route (`changeOrigin: true`, strips the `/proxy/<routeName>` prefix) — set `VITE_JGENGINE_DEV_PROXY` once and both sides (the URL rewrite and the actual proxy route) agree.
|
|
1936
|
-
|
|
1937
|
-
## Multiplayer and the backend seam
|
|
1938
|
-
|
|
1939
|
-
The transport/host/persistence seam — `createWsBackend`, protocol codec, browser-safe authoritative host + router, WebRTC P2P, the Node/Convex/SQL adapters, presence, and save cadence. Full surface: **[reference/multiplayer.md](reference/multiplayer.md)**.
|
|
1940
|
-
|
|
1941
|
-
## UI — `@jgengine/react`
|
|
1942
|
-
|
|
1943
|
-
The React layer — `GameProvider`, the hooks table, headless className-passthrough primitives (incl. map components), the identity/chat/voice/social/drag-layer kits, the shadcn registry install path for visual HUD components (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), the screen-layout rule, and the **UI quality bar** (required, not optional polish). Full surface: **[reference/ui-react.md](reference/ui-react.md)**.
|
|
1944
|
-
|
|
1945
|
-
## Devtools — F2 overlay and tunables
|
|
1946
|
-
|
|
1947
|
-
`@jgengine/shell`'s `GamePlayerShell` mounts an F2-toggled debug overlay (`shell/src/devtools/DevtoolsOverlay.tsx`) over every game automatically — `defineGame({ devtools: false })` is the only way to turn the toggle off (default `true`). Five tabs:
|
|
1948
|
-
|
|
1949
|
-
| Tab | Shows |
|
|
1950
|
-
|-----|-------|
|
|
1951
|
-
| Perf | fps, frame/sim ms, draw calls, triangles, entity/object counts, state notifies/s, registered probes |
|
|
1952
|
-
| Tune | every discovered tunable — checklist grouped by source file or table export name; check one to control it live |
|
|
1953
|
-
| Logs | captured `console.log`/`info`/`warn`/`error` |
|
|
1954
|
-
| Net | observed backend round-trip latency (fed by `instrumentLatency`) |
|
|
1955
|
-
| Keys | the game's `ActionCodesMap` bindings |
|
|
1956
|
-
|
|
1957
|
-
**Tunables are zero-annotation.** Write plain code under `Games/<id>/src/**` — no import, no wrapper — and it's discoverable:
|
|
1958
|
-
|
|
1959
|
-
```ts
|
|
1960
|
-
// loop.ts — top-level export const, nothing else
|
|
1961
|
-
export const GRAVITY = -22;
|
|
1962
|
-
export const SKY_COLOR = "#87ceeb";
|
|
1963
|
-
export const GOD_MODE = false;
|
|
1964
|
-
|
|
1965
|
-
// game/content.ts — an exported flat table of numbers/booleans/colors
|
|
1966
|
-
export const TUNING = { reach: 6, spawnRate: 0.4, fogColor: "#334455" };
|
|
1967
|
-
```
|
|
1968
|
-
|
|
1969
|
-
The dev runner's Vite plugin, `tunableDiscoveryPlugin` (`@jgengine/core/devtools/transformTunables`, wired in `apps/dev/vite.config.ts`), rewrites each top-level `export const <number|boolean|"#hex">` literal to `export let` and binds it into the devtools registry as the module loads (`transformTunableExports` is the pure string transform underneath; `tunableModuleTable(id)` derives the table id from the file path, skipping `main.tsx` and `*.test.*`). Table exports need no transform at all — after each game module loads, the dev app calls `devtools.discover.scanModule(moduleExports)`, which walks every export's own properties for a flat plain-object table of numbers/booleans/`"#rrggbb"` strings.
|
|
1970
|
-
|
|
1971
|
-
F2 → Tune tab lists every discovered entry as a checklist, grouped by source file (top-level constants) or by table export name (object tables). Checking an entry hands it a live slider/toggle/color picker; unchecking resets it to its initial value. Kind is inferred from the value: `number` → slider, `boolean` → toggle, a `"#rgb"`/`"#rrggbb"`/`"#rrggbbaa"` string → color.
|
|
1972
|
-
|
|
1973
|
-
**Liveness.** An edit applies live wherever the code reads the constant/table entry at use time. A value captured once at init — passed into a function call, baked into worldgen — only picks up the new value on reload. Overrides persist in `localStorage` per game (key `jg-devtools:<game name>`) and are re-applied *before* `loop.onInit` runs, so even an init-baked constant respects its override after a refresh — *if* the read happens at or after `onInit`. A read that happens earlier than that (see below) never sees the override, reload or not.
|
|
1974
|
-
|
|
1975
|
-
**Default assumption: almost every gameplay number, boolean, and color is a tunable, not a hardcoded fact.** Walk speed, jump height, gravity, damage, cooldowns, spawn rates, drop chances, radii, durations, thresholds, multipliers, colors — if it's a scalar a designer would plausibly want to nudge while playing, it belongs in a place discovery can see (a top-level `export const`, or a direct scalar field on a catalog def object like `PlayerDef`/`EnemyDef`) — never buried as a bare literal inside a deeper nested object with no named export, and never computed once and thrown away. Treat "should this be tunable" as opt-out, not opt-in.
|
|
1976
|
-
|
|
1977
|
-
**Catalog-derived content must read fields live, not bake them at import time.** A common trap: a `content.ts` (or any module implementing `GameContextContent`) that loops over a catalog array *once at module scope* and copies scalar fields into a separately cached `Map`:
|
|
1978
|
-
|
|
1979
|
-
```ts
|
|
1980
|
-
// WRONG — copies walkSpeed by value at import time, before devtools can even scan exports
|
|
1981
|
-
const entityEntries = new Map<string, GameContextEntityEntry>();
|
|
1982
|
-
for (const p of players) {
|
|
1983
|
-
entityEntries.set(p.id, { stats: p.stats, movement: { poses: p.poses, walkSpeed: p.walkSpeed } });
|
|
1984
|
-
}
|
|
1985
|
-
export const content: GameContextContent = {
|
|
1986
|
-
entityById: (id) => entityEntries.get(id) ?? null,
|
|
1987
|
-
};
|
|
1988
|
-
```
|
|
1989
|
-
|
|
1990
|
-
This runs during module import — earlier than `discoverGameTunables`/override-application in `apps/dev/src/main.tsx`, and earlier than any `loop.onInit`. The catalog object (`p`) still gets live-mutated by devtools, but nothing ever re-reads it, so the baked `walkSpeed` is permanently stale: no F2 edit and no persisted override ever reaches gameplay, reload or not.
|
|
1991
|
-
|
|
1992
|
-
```ts
|
|
1993
|
-
// RIGHT — map ids to the catalog def itself; build the entry fresh on every lookup
|
|
1994
|
-
const playersById = new Map(players.map((p) => [p.id, p]));
|
|
1995
|
-
function entityById(id: string): GameContextEntityEntry | null {
|
|
1996
|
-
const p = playersById.get(id);
|
|
1997
|
-
return p === undefined ? null : { stats: p.stats, movement: { poses: p.poses, walkSpeed: p.walkSpeed } };
|
|
1998
|
-
}
|
|
1999
|
-
export const content: GameContextContent = { entityById };
|
|
2000
|
-
```
|
|
2001
|
-
|
|
2002
|
-
Same shape, same call site — the only change is *when* `p.walkSpeed` is read: at lookup time (each spawn) instead of once at import. Objects (`stats`, `receive`) are already reference-safe to pass through either way; this only matters for scalars (numbers/booleans/strings) copied out of a catalog def.
|
|
2003
|
-
|
|
2004
|
-
**`tunable()` still exists — an optional low-level primitive, not the recommended path.** Reach for it only when you need explicit bounds, an `options` select, or a change subscription that discovery can't infer:
|
|
2005
|
-
|
|
2006
|
-
```ts
|
|
2007
|
-
import { tunable } from "@jgengine/core/devtools/devtools";
|
|
2008
|
-
|
|
2009
|
-
const gravity = tunable("physics/gravity", -22, { min: -60, max: 0 });
|
|
2010
|
-
```
|
|
2011
|
-
|
|
2012
|
-
Read `gravity.value` at use time (or `gravity.subscribe(listener)`) — never destructure once at module load. A `"group/label"` name (e.g. `"physics/gravity"`) groups the control under `group` in the Tune tab; `devtools.controls.register` is the same call underneath. Real example: `Games/voxel-mine/src/loop.ts` — `tunable("mining/reach", REACH, { min: 2, max: 16, step: 1 })`, read via a getter passed to `createEditorHandlers`.
|
|
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)**.
|
|
2013
2569
|
|
|
2014
|
-
|
|
2570
|
+
## Level design — places, not noise
|
|
2015
2571
|
|
|
2016
|
-
|
|
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:
|
|
2017
2573
|
|
|
2018
|
-
|
|
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.
|
|
2019
2581
|
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
**Sources** (CC0 — public domain, commercial use, no attribution — unless noted):
|
|
2023
|
-
|
|
2024
|
-
| Source | What you get |
|
|
2025
|
-
|--------|--------------|
|
|
2026
|
-
| [Kenney.nl](https://kenney.nl) | 40,000+ CC0 assets: characters, buildings, nature, vehicles, weapons, UI, audio — the broadest single library |
|
|
2027
|
-
| [Quaternius](https://quaternius.com) / [KayKit](https://kaylousberg.itch.io) | CC0 low-poly packs incl. **rigged + animated characters**: medieval, sci-fi, dungeons, animals, adventurers |
|
|
2028
|
-
| [Poly Haven](https://polyhaven.com) / [ambientCG](https://ambientcg.com) | CC0 PBR textures, HDRIs, materials — the floor comes from here, never a flat color |
|
|
2029
|
-
| [Poly Pizza](https://poly.pizza) | Search engine over thousands of CC0 low-poly models for one specific thing |
|
|
2030
|
-
| [Game-Icons.net](https://game-icons.net) (CC BY 3.0 — credit it) / Kenney UI packs (CC0) | 4,000+ item/ability **icon silhouettes** — the registry `game-icon` item covers common HUD glyphs first |
|
|
2031
|
-
| [itch.io CC0 3D tag](https://itch.io/game-assets/assets-cc0/tag-3d) / [OpenGameArt](https://opengameart.org) | Long tail — **check the license per asset**, CC0 filter first |
|
|
2032
|
-
| [Mixamo](https://www.mixamo.com) | Free humanoid animations (Adobe license — fine for shipped games, not CC0) |
|
|
2033
|
-
| Kenney audio / [freesound CC0 filter](https://freesound.org) | Hit sounds, UI clicks, ambience |
|
|
2034
|
-
|
|
2035
|
-
**Rules:**
|
|
2036
|
-
|
|
2037
|
-
1. **One style family per game.** Kenney + Quaternius + KayKit low-poly mix fine; low-poly models on photoreal PBR ground reads broken. Name the family in the blueprint.
|
|
2038
|
-
2. **License discipline.** CC0 needs nothing; anything else gets a line in `src/game/assets-credits.md` (source, author, license). Never ship an asset you can't name the license of.
|
|
2039
|
-
3. **Wire through the engine seams.** GLB models live in the game's `src/game/assets.ts` render catalog keyed by catalog id; billboards via `entitySprites`, real meshes via `entityModels`/`objectModels` in `defineGame({...})`; ground/skies belong to the world layer. Catalog `model` fields reference asset keys — never file paths in game logic. Source models through **`@jgengine/assets`** (`buildCatalog({ basePath })` → resolve ids/aliases → urls); `pull` packs into your app's `public/models/` (extracts Kenney's shared `Textures/` alongside the GLBs so models render textured). Network-restricted: `pull` falls back through `--mirror <baseUrl>` / `JGENGINE_ASSETS_MIRROR`, and `--offline` fails fast — see the package README for the fallback order and add/import flow.
|
|
2040
|
-
4. **Coverage follows the content budget.** Every entity family, placed object, and held item maps to a real asset *before* the catalog entry ships. If the pack lacks a model, restyle the noun to one it has — rename the fantasy, don't ship a cube.
|
|
2041
|
-
5. **Scale/pivot sanity.** `@jgengine/assets` measures each model's footprint/center/`minY` at reindex and ships them on the catalog entry (`catalog.resolve(id).dims`); with `objectModels` anchor `"center"` (the default) the shell centers the footprint on the placement point and ground-snaps the lowest vertex — so `object.place(id, cellX, y, cellZ, { rotation })` renders centered + grounded with no pivot math and no `dimensions.ts`. Anchor `"origin"` opts back into the raw GLB origin. Check the first placement of each model against its catalog `footprint`; one wrong pivot repeated 100 times is a rebuild.
|
|
2042
|
-
6. **Item and ability icons are assets too.** Every hotbar/inventory/ability slot renders a real, distinct icon — the registry `game-icon` catalog (`iconForItemId`/`iconForAction`) or a Game-Icons/Kenney silhouette — per the UI quality bar's real-icons rule.
|
|
2043
|
-
|
|
2044
|
-
## Genre cheat sheet
|
|
2045
|
-
|
|
2046
|
-
- **Voxel/crafting**: objects for blocks/machines, `voxel()`, `object.break`/`object.placeFromInventory`.
|
|
2047
|
-
- **Tycoon/lab**: objects + `slotInventory`, `plots()`, configure via prompt → command.
|
|
2048
|
-
- **Shooter**: `fireProjectile`/`settleProjectile`; grenades settle → `effect({ at, radius })`; `movement.poses`/`aim` + zoom modifier; `servers({ … })` + game-owned `server.mode`; loadout classes from commands.
|
|
2049
|
-
- **MMO/RPG**: bounded stats + `leveling()` over a game XP curve; `tabTarget` → `cycleTarget`; handlers read `getTarget`; quests bound to `entity.died`/`inventory.added`; social party + `partyShare`; `server: "persistent"`.
|
|
2050
|
-
- **All combat games**: react to `entity.died` (feed/leaderboard/score) — never poll HP.
|
|
2051
|
-
|
|
2052
|
-
## Anti-patterns
|
|
2053
|
-
|
|
2054
|
-
| Wrong | Right |
|
|
2055
|
-
|-------|-------|
|
|
2056
|
-
| Player tuning in `defineGame` | Entity catalog `movement` + stats |
|
|
2057
|
-
| `behaviors: […]` on place/spawn | Catalog entry |
|
|
2058
|
-
| Engine `weapon.fire` / `consumable.use` / `combat.*` | `item.use` + catalog `use` → game handler |
|
|
2059
|
-
| `ItemUseInput.to` for targets | `getTarget(from)` in handlers |
|
|
2060
|
-
| `effect({ to })` for gunshots | `fireProjectile` + `settleProjectile` |
|
|
2061
|
-
| Polling HP in `onTick` for kills | `entity.died` event |
|
|
2062
|
-
| `combat.lootTable` / `loot.enemy` | `onDeath` on the entity that died |
|
|
2063
|
-
| Hand-rolled `Math.random()` loot in commands | `lootTable()` + `ctx.game.loot.roll` |
|
|
2064
|
-
| Hand-rolled `xpForLevel`/`levelFromXp` | `game/progression` `curve()` + `leveling()` |
|
|
2065
|
-
| Hardcoded shop arrays | `item.trade.shops` + `tradableAt` |
|
|
2066
|
-
| Kit seeding via scattered `put`/`grant` | `applyLoadout` |
|
|
2067
|
-
| Per-user quest state hand-rolled | `game.quest.register` + binds |
|
|
2068
|
-
| `useKillFeed` / per-domain feed hooks | `useFeed({ action })` |
|
|
2069
|
-
| Raw keys in game logic | `defineGame` input actions |
|
|
2070
|
-
| Positioning inside `ui/components/` or on primitives (`CurrencyPill className="absolute …"`) | Screen wrappers in `GameUI.tsx` only |
|
|
2071
|
-
| Game UI classes without `@source` in host CSS | `@source` entries for your game dirs + `node_modules/@jgengine/{shell,react}` |
|
|
2072
|
-
| One file per catalog entry / per brand | Dense `<domain>/catalog.ts` |
|
|
2073
|
-
| Convex mutations called from game code | `commands.run` through the `GameBackend` transport |
|
|
2074
|
-
| Half a system: quest without tracker, cooldown without sweep, keybind never shown, stub "coming soon" modal | Finish the system end to end — or cut it whole (see `jgengine-newgame`) |
|
|
2075
|
-
| Game-side workaround for a missing engine primitive | File the gap at github.com/Noisemaker111/jgengine/issues (or PR the primitive) and cut or scope the dependent system honestly |
|
|
2076
|
-
| Game nouns in this skill | Engine primitives + placeholder ids only |
|
|
2077
|
-
|
|
2078
|
-
## New-game definition of done
|
|
2079
|
-
|
|
2080
|
-
This is a gate, not a suggestion — every box, in one pass (workflow: **`jgengine-newgame`** skill). "Compiles and the hooks are wired" is not done; a declared system with no UI, no feedback, or no way to exercise it is not done — finish the system or cut it whole.
|
|
2081
|
-
|
|
2082
|
-
- [ ] `game.config.ts` (`defineGame` from `@jgengine/shell/defineGame`) + `index.tsx` (barrel) + `main.tsx` (standalone host) + `loop.ts` + `game/content.ts`
|
|
2083
|
-
- [ ] Catalogs: `game/entities/<role>/catalog.ts`, `game/items/<domain>/catalog.ts`, `game/objects/catalog.ts`; loot tables beside their domain
|
|
2084
|
-
- [ ] Entity `stats` + `receive` orders aligned on the same stat ids; `role` set (drives targeting + camera)
|
|
2085
|
-
- [ ] `game/items/use-handlers.ts` registered in `onInit`; handlers read `getTarget`/`aim`, never a target input
|
|
2086
|
-
- [ ] `game/loadouts.ts` + `applyLoadout` in `onNewPlayer` (gated on `isNew`)
|
|
2087
|
-
- [ ] `game/quests/catalog.ts` + binds; if using xp/level, a game-owned curve fed to `game/progression` (`curve`/`leveling`) — **with their HUD/tracker, or cut**
|
|
2088
|
-
- [ ] `onInit`: register handlers/loadouts/loot/quests, event listeners, feed binds, leaderboard tracks; `setupWorld`
|
|
2089
|
-
- [ ] Player spawns with `id === ctx.player.userId`
|
|
2090
|
-
- [ ] `game/ui/GameUI.tsx` owns layout; components use `@jgengine/react` hooks
|
|
2091
|
-
- [ ] UI passes the **quality bar** above (contrast, scale, framing, genre fit) — not just hook wiring
|
|
2092
|
-
- [ ] Camera tuned via `camera` in `defineGame({...})` — defaults untouched means the feel was never checked
|
|
2093
|
-
- [ ] For an `environment()` world: a `<game>.world.test.ts` asserts `summarizeEnvironment(world)` (`@jgengine/core/world/environmentSummary`) is non-empty with the expected counts — the browserless scene-correctness gate
|
|
2094
|
-
- [ ] HUD screenshotted over a staged `GameUiPreview` scenario and **judged by looking at the image** against the UI quality bar in [`reference/ui-react.md`](reference/ui-react.md) — the final human glance, not the verification loop
|
|
2095
|
-
- [ ] Co-located bun tests for pure game math (curves, cooldowns, spawn logic)
|
|
2096
|
-
- [ ] Multiplayer via adapter config only; no direct backend calls
|
|
2097
|
-
|
|
2098
|
-
## Quick reference
|
|
2099
|
-
|
|
2100
|
-
```
|
|
2101
|
-
defineGame (shell) engine fields (assets, world, physics, inventories, input, server, save, time, feed, multiplayer)
|
|
2102
|
-
+ presentation fields (content, loop, GameUI, camera, environment, shadows, movement, devtools, …) in one call — smart defaults fill the rest
|
|
2103
|
-
defineGame (core) the underlying engine-only primitive: assets, world, physics, inventories, input, server, save, time, feed, multiplayer, loop
|
|
2104
|
-
PlayableGame { game, content, loop, GameUI, camera, … } — the runner contract `defineGame` (shell) returns
|
|
2105
|
-
GameContext ctx.scene / ctx.game / ctx.player / ctx.item / ctx.camera / ctx.input + subscribe/version
|
|
2106
|
-
scene.object place, remove, move, rotate, at, setVisual (per-instance ObjectVisual: scale/color/opacity override)
|
|
2107
|
-
scene.entity spawn (anchor/offset), despawn, setPose, update; stats; targeting; effects;
|
|
2108
|
-
projectiles (object-aware raycasts); spatial queries (opt-in grid broadphase)
|
|
2109
|
-
entity.stats get / set / delta — bounded stats (health, mana, xp, level) on instances
|
|
2110
|
-
progression game/progression — curve() / leveling() over bounded xp/level stats
|
|
2111
|
-
item.use catalog `use` → GameContext handler; no input.to
|
|
2112
|
-
effects drain-signed magnitudes; receive.<effect>.order; AoE = effect + at/radius/los
|
|
2113
|
-
projectiles willHit → fire → settle; ballistic via weapon.projectile
|
|
2114
|
-
death onDeath (reason-aware drops/command), entity.died, auto kill attribution + drop grant
|
|
2115
|
-
game.loot register / has / roll / grantToPlayer (lootTable() = pure factory)
|
|
2116
|
-
game.trade canBuy / canSell / buy / sell / tradableAt
|
|
2117
|
-
game.quest register, accept…turnIn, bind(entity.died | inventory.added), declarative rewards
|
|
2118
|
-
game.social friends (persisted, requests listable), party (ephemeral, invites listable), presence, emotes (nearby broadcast), worldInvites (accept → join target)
|
|
2119
|
-
game.chat send / whisper / history / register — global/party/proximity channels, rate-limited, mute via blocked set
|
|
2120
|
-
game.roster capture / release / list / setEquipped — persisted owned-creature roster
|
|
2121
|
-
game.store/cards/turn store: keyed reactive slot; cards.pile(id): lazy CardPile; turn.loop(id): lazy TurnLoop
|
|
2122
|
-
game.events/feed/leaderboard on / bind+push+recent / track+increment+getTop
|
|
2123
|
-
devtools F2 overlay (Perf/Tune/Logs/Net/Keys); zero-annotation — top-level export const number/boolean/color + exported flat tables auto-discover into Tune; tunable() is the optional low-level escape hatch; snapshotDevtools()/window.__JG_DEVTOOLS.snapshot() → DevtoolsSnapshot (+ discovered[]); probes.register(name, read) adds a Perf gauge
|
|
2124
|
-
applyLoadout all-or-nothing kit seeding per userId
|
|
2125
|
-
player.movement pose (hitboxes) + aim (zoom modifier)
|
|
2126
|
-
player.motion impulse / setVerticalVelocity / setY — vertical-motion seam into the shell's frame driver
|
|
2127
|
-
player.possession own/disown/owns/listOwned + active + possess — control-swap, rebinds shell camera
|
|
2128
|
-
player.cosmetics register + apply/equip + get — per-player appearance slots, no gameplay effect
|
|
2129
|
-
scene.entity.form register + shapeshift/revert + active/abilities — movement+ability+mesh bundle, game-time duration
|
|
2130
|
-
proximityPrompt { radius, display: {kind}, invoke } — one float-UI primitive
|
|
2131
|
-
skillCheck/qte evaluateSkillCheck (moving zone + window) / evaluateQteSequence (timed steps)
|
|
2132
|
-
captureCheck captureChance / rollCapture — hp% + catchPower → probability
|
|
2133
|
-
dialogue check DialogueChoice.check (roll vs DC + advantage/disadvantage) → onSuccess/onFailure
|
|
2134
|
-
world features biomes / voxel / plots / tilemap / flat descriptors
|
|
2135
|
-
physics/physicsWorld optional headless rigid-body sim (PhysicsWorld) — not the defineGame physics field (gravity/jumpVelocity, honored by the kinematics controller); bodies are box (halfExtents) or sphere (radius)
|
|
2136
|
-
physics/ballisticSweep createBallisticSweep(world) → arc-vs-body hit test; wire into ProjectileSystemDeps.sweepBallistic
|
|
2137
|
-
anim/easing lerp/clamp01/smoothstep/tween/timedProgress + easeIn/Out/InOut Quad/Cubic + easeOutBack/Elastic — pure 0..1 tweening math
|
|
2138
|
-
data/dataSource createDataSource/createJsonDataSource — idle/loading/ready/error async state + polling; data/fetchJson + data/devProxy for CORS-safe dev fetches
|
|
2139
|
-
audio/audioFalloff computeFalloffGain / resolveEmitterGain — pure distance→gain curve; shell plays it
|
|
2140
|
-
time/beatClock createBeatClock (BPM ticks) + createBeatInputBuffer (buffered action → next beat)
|
|
2141
|
-
ws/voiceChannel createVoiceChannelRouter — positional falloff + simultaneous non-positional channels
|
|
2142
|
-
multiplayer/identity AuthSession + sessionPlayer + resolveGuestSession — Clerk/better-auth via react structural adapters
|
|
2143
|
-
multiplayer/chatContract ChatTransport (hooks) / ChatSync (callbacks) — ws + convex bindings, local for dev
|
|
2144
|
-
multiplayer/voiceContract VoiceTransport (join/leave/publish/subscribers) + createPushToTalk — media plane host-supplied
|
|
2145
|
-
GameBackend { transport, feeds?, presence? } — Convex is one adapter (createConvexBackend)
|
|
2146
|
-
adapter kinds offline / ws / convex / socketIo / p2p / lan (+ fly({app}) ws sugar) — runtime/adapter
|
|
2147
|
-
ws/pipe TransportPipe/TransportPipeFactory — any bidirectional string channel (webSocketPipe default)
|
|
2148
|
-
ws/host, ws/hostRouter browser-safe createGameHost + createHostRouter/loopbackPipe (node re-exports both)
|
|
2149
|
-
ws/peer createPeerHost/createPeerGuest — WebRTC P2P, host tab authoritative, copy-paste signal codes
|
|
2150
|
-
ws/socketIoPipe, node/socketIoServer socketIoPipe/createSocketIoBackend + attachGameSocketIoServer
|
|
2151
|
-
@jgengine/react GameProvider + hooks + headless primitives (incl. identity/chat/voice/social kits); layout only in GameUI.tsx
|
|
2152
|
-
```
|
|
2153
|
-
|
|
2154
|
-
Engine ships verbs and primitives. Your game ships nouns.
|
|
2155
|
-
|
|
2156
|
-
# jgengine-api — UI — @jgengine/react
|
|
2157
|
-
|
|
2158
|
-
Reference module for the [`jgengine-api`](../SKILL.md) skill. Load this when you need the React UI layer. The **UI quality bar** section at the bottom is how the HUD must look and behave — required, not optional polish.
|
|
2159
|
-
|
|
2160
|
-
```tsx
|
|
2161
|
-
import { GameProvider, useSceneEntities, HealthBar } from "@jgengine/react";
|
|
2162
|
-
|
|
2163
|
-
<GameProvider context={ctx}>…</GameProvider>
|
|
2164
|
-
```
|
|
2165
|
-
|
|
2166
|
-
Import provider, hooks, and headless components from the package root `@jgengine/react` (a barrel re-export). The per-file subpaths (`@jgengine/react/provider`, `/hooks`, `/components`) resolve the same symbols if you prefer them.
|
|
2167
|
-
|
|
2168
|
-
All hooks bind through the ctx change signal (`ctx.subscribe`/`ctx.version`):
|
|
2169
|
-
|
|
2170
|
-
| Hook | Returns |
|
|
2171
|
-
|------|---------|
|
|
2172
|
-
| `useGame()` / `usePlayer()` | `{ commands, events }` / `{ userId, isNew }` |
|
|
2173
|
-
| `useSceneEntities()` / `useSceneObjects()` | live snapshots for rendering |
|
|
2174
|
-
| `useWorldItems()` / `useNearestWorldItem(radius)` | ground-loot snapshots / nearest pickup for a HUD prompt |
|
|
2175
|
-
| `useEntityStat(instanceId, statId)` | `StatValue \| null` |
|
|
2176
|
-
| `useTarget(fromId)` | locked instanceId \| null |
|
|
2177
|
-
| `useInventory(id)` / `useCurrency(id)` | slots / balance |
|
|
2178
|
-
| `useFeed({ action, limit? })` | recent entries — kills, loot, any action |
|
|
2179
|
-
| `useQuestJournal()` | active quests + objective progress |
|
|
2180
|
-
| `useFriends()` / `useParty()` / `usePresence(userId)` / `useWorldInvites()` | social panels |
|
|
2181
|
-
| `useFriendRequests()` / `usePartyInvites()` | pending inbound requests/invites for the local player |
|
|
2182
|
-
| `useWorldBrowser({ fetchSessions, filter?, limit?, refreshMs? })` | polls a host-supplied fetcher (e.g. `createWsBackend().browse`) through matchmaking's `browseSessions` |
|
|
2183
|
-
| `useSession()` / `useAuthedPlayer({ guestSeed? })` | auth session from `<GameIdentityProvider>` / the `{ userId, isNew }` player seam for `createGameContext` |
|
|
2184
|
-
| `useChat(channelId, { limit? })` | local-player-filtered recent messages from `ctx.game.chat` |
|
|
2185
|
-
| `useVoice({ transport?, channelId?, mode?, resolveRoutes? })` | mic capture + PTT + voice-channel roster over the `VoiceTransport` seam |
|
|
2186
|
-
| `useRoster(userId?)` | owned/captured roster entries for a user (defaults to the local player) |
|
|
2187
|
-
| `useLeaderboard(stat, { scope, limit? })` | `{ userId, value }[]` |
|
|
2188
|
-
| `useActivePrompt(prompts)` | nearest proximity prompt |
|
|
2189
|
-
| `useGameClock()` | clock snapshot (`now`, `paused`, `speed`, `calendar`) + `controls` (pause/play/setSpeed) |
|
|
2190
|
-
| `useLocalPlayerDead()` / `localPlayerEntity(entities, userId)` | death-screen gating; local player from a snapshot |
|
|
2191
|
-
| `useMarkers(markerSet)` / `useFog(fogField)` | live map-marker list / fog-cell snapshot (bind a core `MarkerSet`/`FogField`) |
|
|
2192
|
-
| `useGameStore()` | raw store handle — escape hatch under the typed hooks |
|
|
2193
|
-
| `useEngineState(store)` / `useEngineStore(store, selector)` / `useEngineEvent(store, event, handler)` | bind/select/subscribe against any `ReadableEngineStore<TState>` / `EventfulEngineStore<TEventMap>` — the escape hatch below `useGameStore()` for state that isn't wired into a typed hook yet |
|
|
2194
|
-
| `useHeldKeys()` | `(code: string) => boolean` — raw window keydown/keyup/blur-backed held-key predicate, no `ctx` needed; the primitive `useAxisChannel` and the shell's own movement sampling are built on |
|
|
2195
|
-
| `useAxisChannel(config: AxisChannelConfig)` | `{ channel: AxisChannel, isDown }` — wires `useHeldKeys` into a fresh `AxisChannel` (`@jgengine/core/input/axisInput`) for a per-frame `channel.sample(dt, isDown)`; a driving/twin-stick HUD or custom control scheme reads analog throttle/steer without touching `window` directly |
|
|
2196
|
-
|
|
2197
|
-
Import hooks from `@jgengine/react/hooks`, components from `@jgengine/react/components`, `GameProvider` from `@jgengine/react/provider` (the package uses deep paths like core). `useEngineState`, `ReadableEngineStore`, `useEngineStore`, `useEngineEvent` also ship from the main `@jgengine/react` entry point (defined in `@jgengine/react/engineStore`, re-exported at the package root) — no deep import required.
|
|
2198
|
-
|
|
2199
|
-
Headless components (className passthrough, no baked-in styling): `SlotGrid`, `HealthBar` (+ `fillClassName`), `CurrencyPill`, `ProximityPrompt`, `Screen`, `KeybindRow`, `DialogueBox` (+ `lineClassName`/`speakerClassName`/`choicesClassName`/`choiceClassName`/`checkClassName`, `rollCheck`-gated choices), `SkillCheckBar` (+ `trackClassName`/`zoneClassName`/`markerClassName`), `QteTrack` (+ `stepClassName`/`activeClassName`/`doneClassName`), `CaptureOdds` (+ `fillClassName`), `ToastStack`, `DeathScreen`, `LevelUpFlash`. Map components (bind a core `MarkerSet`/`FogField`, `kindStyles` palette overridable): `Minimap` (framed circular player-centered map — fog + markers + facing arrow, optional baked terrain `background`+`mapBounds`), `Compass` (facing strip with cardinals + marker pips), `WorldMap` (full-bounds top-down overlay). Not yet implemented: `useServer`, `useDialogue`.
|
|
2200
|
-
**Identity (`@jgengine/react/identity`)** — `<GameIdentityProvider source={…}>` + `useSession()`. Sources: `clerkIdentity({ isLoaded, isSignedIn, user })` maps Clerk's `useUser()` shape, `betterAuthIdentity({ data, isPending })` maps better-auth's `useSession()` shape (both pure structural mappers — no SDK imports, one line at the call site), `guestIdentity(seed?)` for local/dev. Gate UI with `<RequireSession fallback loading>`; `<UserBadge>` / `<SignOutButton>` are headless like everything else. `useAuthedPlayer({ guestSeed? })` returns the `{ userId, isNew }` to hand `createGameContext` — feed the player seam from the session instead of hand-picking a userId.
|
|
2201
|
-
**Chat (`@jgengine/react/chat`)** — headless `<ChatPanel>` (tabs + log + input composition with internal active-channel state), or compose `<ChannelTabs active onSelect>`, `<ChatLog channelId>` (auto-scrolls, `renderMessage` override), `<ChatInput channelId onSent onRejected>` yourself. All drive `ctx.game.chat` through `useChat`. `chatTransportFromSync(sync)` lifts a callback-style `ChatSync` (e.g. `createWsBackend(...).chatSyncFor(serverId)`) into the hook-shaped `ChatTransport` for remote chat.
|
|
2202
|
-
**Voice (`@jgengine/react/voice`)** — `useVoice()` once per channel: `getUserMedia` mic capture (`requestMic()`, tracks gated by transmission), push-to-talk via `createPushToTalk` (hold/toggle/openMic + mute), roster from `VoiceTransport.subscribers`, and per-speaker `gainFor(userId)` when you pass `resolveRoutes: () => router.resolveRoutes(myUserId)` from `@jgengine/ws/voiceChannel`. Hand the returned state to the headless `<PushToTalkButton voice>`, `<MicToggle voice>`, `<SpeakingIndicator voice userId>`, `<VoiceRoster voice>`.
|
|
2203
|
-
**Social (`@jgengine/react/social`)** — the headless social kit over `ctx.game.social`: friends (`<FriendsList>`, `<FriendRow>`, `<PresenceDot>`, `<AddFriendButton toUserId>`, `<FriendRequestsList>` with accept/decline), party (`<PartyFrame>`, `<PartyMemberRow>`, `<PartyInviteToast>`, `<LeavePartyButton>`), worlds (`<WorldBrowser listings onJoin>`, `<JoinByCode onJoin>` — normalizes codes, `<QuickMatchButton listings filter?>`, `<InviteToWorldButton toUserId target>`, `<WorldInviteToast onAccepted>` — hands you the `{ serverId, joinCode? }` join target), and `<EmoteWheel emotes>` over `emotes.play`. All className-passthrough with `data-*` hooks and `renderX` overrides; the `social-hub` demo in `apps/dev` (`?game=social-hub`) composes the whole kit.
|
|
2204
|
-
**Drag/rotate/drop/snap gesture layer** (`@jgengine/react/dragLayer`) — a 2-D UI-space gesture layer over the card/shaped-grid primitives, distinct from 3-D world drag. `useDragLayer<T>({ onDrop })` owns pointer-follow drag state (begin/rotate/setTarget/end); pair it with the headless, className-passthrough `DraggableCard` (right-click rotates), `DropZone` (reports the snapped `cellFromPoint` cell + active state), and `DragGhost` (a pointer-anchored preview). Drop resolution and overlap validation stay the game's job via `canPlace`/`placeShaped` from `inventory/shapedGrid` — Balatro hand→play drags, Backpack Hero grid placement, Slay-the-Spire card-onto-enemy targeting.
|
|
2205
|
-
|
|
2206
|
-
**Layout rule:** all **screen** positioning (`absolute`, `inset-*`, grid zones, flex regions) lives on wrappers inside `ui/GameUI.tsx` only. `ui/components/` files are content + hooks only — internal `relative`/`absolute` for bar overlays or slot badges inside a component is fine; never anchor a component to the viewport from a child file. Pass `className` to primitives for **visual** styling (colors, borders, size), not screen placement.
|
|
2207
|
-
|
|
2208
|
-
**Tailwind sources:** add `@source` entries in your CSS for your game source dirs plus `node_modules/@jgengine/shell` and `node_modules/@jgengine/react`. Without them, classes used in dynamically imported game code are **not generated** — layout wrappers in `GameUI.tsx` silently fail and every HUD cluster stacks in one corner.
|
|
2209
|
-
|
|
2210
|
-
## Visual HUD via the shadcn registry
|
|
2211
|
-
|
|
2212
|
-
Styled HUD components are **copy-in code**, not npm exports — install them from the JGengine registry with the shadcn CLI:
|
|
2213
|
-
|
|
2214
|
-
```sh
|
|
2215
|
-
npx shadcn@latest add https://jgengine.com/r/entity-vital-bar.json
|
|
2216
|
-
```
|
|
2217
|
-
|
|
2218
|
-
The component lands in your game's `components/ui/`, themes through `--jg-*` CSS variables, and reads engine data through `@jgengine/react/hooks`. Health bar hookup, end to end:
|
|
2219
|
-
|
|
2220
|
-
```tsx
|
|
2221
|
-
import { usePlayer } from "@jgengine/react/hooks";
|
|
2222
|
-
import { EntityVitalBar } from "@/components/ui/entity-vital-bar";
|
|
2223
|
-
|
|
2224
|
-
const { userId } = usePlayer();
|
|
2225
|
-
return <EntityVitalBar instanceId={userId} statId="health" />;
|
|
2226
|
-
```
|
|
2227
|
-
|
|
2228
|
-
The full HUD catalog ships as registry items — vitals, slots, feedback, meters, panels, screens, reticles, plus the `game-icon` glyph catalog (`iconForItemId`/`iconForAction`), with engine-bound `Entity`/`Ability`/`Journal`/`Feed`/`Wallet` variants wired to the hooks. Where a registry item exists, prefer it — and never hand-roll a gray-box version of a component the registry already ships.
|
|
2229
|
-
|
|
2230
|
-
### UI quality bar (required — not optional polish)
|
|
2231
|
-
|
|
2232
|
-
Headless primitives mean **you** ship the visual design. Functional wiring alone is not shippable UI. Judge against staged screenshots, never your mental model of the code.
|
|
2233
|
-
|
|
2234
|
-
**See what you ship.** `@jgengine/shell`'s `GameUiPreview` renders your `GameUI` over a staged `GameContext` (ticks run, hostile targeted, first ability fired) with no gameplay or backend — mount it on a dev route, screenshot it, and judge the image before calling any HUD work done; pass a custom `scenario` to stage richer states (open modals, low health, active quest). Type-green says nothing about whether the HUD renders.
|
|
2235
|
-
|
|
2236
|
-
| Requirement | Minimum |
|
|
2237
|
-
|-------------|---------|
|
|
2238
|
-
| **Contrast** | HUD text and borders readable on the game's scene background — never bare `text-stone-400` on near-black without a panel |
|
|
2239
|
-
| **Scale** | Primary HUD (unit frames, hotbar slots, menu buttons) ≥ 48px touch targets; body text ≥ `text-sm` (12px); key labels never below 11px |
|
|
2240
|
-
| **Distinct construction** | Unit frame, hotbar, currency, quests, and toasts must not share one card style — same `rounded border bg-stone-900/80 p-3` on everything reads identical and cheap |
|
|
2241
|
-
| **Real icons** | Every item/ability/hotbar slot shows a real, distinct silhouette or sprite (registry `game-icon`, asset-pack sprite) — never a gray box, first letter, emoji, or one generic shape reused everywhere |
|
|
2242
|
-
| **Hotbar / slots** | Icon per ability; keybind badge on the slot corner; hover/active state; empty slots visually distinct |
|
|
2243
|
-
| **Unit frames** | Name + level + labeled bars with numeric values; health/mana/resource colors genre-appropriate |
|
|
2244
|
-
| **Layout** | No overlapping anchors; reserve space for frames that appear conditionally (target, quest log) |
|
|
2245
|
-
| **Panels** | Modal/slide panels: title, close control, section headers, consistent chrome with the HUD |
|
|
2246
|
-
| **Feedback** | Errors, cooldowns, and empty actions surface to the player (toast, dim, shake) — not `console.warn` only. Error text is ephemeral floating combat text, never a bordered toast card |
|
|
2247
|
-
|
|
2248
|
-
**Genre fit:** MMO/RPG → ornate dark panels, gold accents, portrait + bars, action bar with icons. Shooter → crosshair + ammo + ability cooldowns. Tycoon → resource pills + build menus. Match the game's fantasy; do not ship debug-gray placeholders.
|
|
2249
|
-
|
|
2250
|
-
**Panel vs frameless.** Modal/panel chrome (backdrop + bordered window) is for on-demand windows only: backpack/bags, combat log/chat feed, social window. Everything persistent stays frameless — typography, bars, icons, shadows, no enclosing card: player/target/party frames, action bar, quest tracker, currency, voice cluster, floating combat text, world VFX. Backpack is the only bag UI (no hotbar inside it); character sheet holds equipment + stats; the abilities page lists catalog abilities with costs/cooldowns/keybinds, not a hotbar duplicate.
|
|
2251
|
-
|
|
2252
|
-
**Keybinds are badges on their control.** Every toggle and hotbar slot shows its binding on itself (slot corner, toggle button) — never a persistent "WASD to move / E to interact" legend pinned to the screen; if a control needs explaining, badge it or use a proximity prompt that fades. Labels derive from the game's `keybinds.ts` via `actionLabel(keybinds, action)` — hardcoded key strings drift. Register actions in `defineGame.input`, wire through `game.commands` (never UI click handlers only), and read the binding table once before shipping: **one key, one action**. Panel toggles follow the `ui.openBackpack`-style command pattern with state in `ctx.game.store` (`useGameStore`), not a hand-rolled module store.
|
|
2253
|
-
|
|
2254
|
-
**Action bar slot states — all four, visually:** ready (full color), cooldown (dim + sweep + numeric timer), no-resource (red tint/desaturate, cost checked before press), just-cast (brief bright ring flash ~200ms). Cooldown data lives in game code; UI reads it.
|
|
2255
|
-
|
|
2256
|
-
**Combat feedback:** bolts/bullets are `fireProjectile` + delayed `settleProjectile` so they visibly travel — never `effect({ to })`; instant heals flash the unit-frame bar; out-of-range/oom is floating text that fades, no bordered toast.
|
|
2257
|
-
|
|
2258
|
-
**Mobile / touch:** HUD fits a 390px portrait viewport with no horizontal overflow (`min()`/viewport units once compact); `useDisplayProfile().compact` (`@jgengine/react/display`) collapses side panels into a slim top bar; keep the bottom ~180px clear for the engine's touch dock on `coarsePointer`; never render key badges/legends on touch; non-interactive wrappers stay `pointer-events-none` (only real controls opt in with `pointer-events-auto`); ≥48px touch targets always.
|
|
2259
|
-
|
|
2260
|
-
**Social HUD:** build from the headless kits (`@jgengine/react/social`, `/chat`, `/voice`, `/identity`), never hand-rolled lists. Chat is a corner-anchored panel (channel tabs, log, input; sender names tinted apart from bodies); invite toasts are ephemeral with accept/decline that expire with the invite; presence is a dot (`data-online`), not a word; push-to-talk badges its keybind and visibly transmits (`data-transmitting`), speaking players glow on their party row; emote wheel appears on hold-key and fades. Every social button drives an engine verb (`social.friends.request`, `party.accept`, `worldInvites.accept` → join) — one that only mutates local UI state is half a system.
|
|
2261
|
-
|
|
2262
|
-
**Camera feel is part of the HUD pass.** The shell's orbit camera (left-drag orbit, scroll zoom, tap = primary ability, camera-relative WASD) tunes per game via the `camera` field of `defineGame({...})` (`minDistance`, `maxDistance`, `targetHeight`, `rotateSpeed`, `zoomSpeed`, `dampingFactor`, smoothing) — defaults untouched means the feel was never checked; never hardcode camera position in `onTick` while a rig is active.
|
|
2263
|
-
|
|
2264
|
-
**Shared chrome:** extract repeated panel/slot styles into `ui/<theme>.ts` or `ui/components/<Frame>.tsx` — do not copy-paste three classes per file.
|
|
2265
|
-
|
|
2266
|
-
**Self-check before calling UI done** (against actual staged screenshots):
|
|
2267
|
-
|
|
2268
|
-
- [ ] Screenshot at 1080p: can you read every label without squinting?
|
|
2269
|
-
- [ ] Could you mistake the unit frame and hotbar for the same component? If yes, redo.
|
|
2270
|
-
- [ ] Every toggle shows its key on its own control — and no persistent controls legend anywhere?
|
|
2271
|
-
- [ ] Every item/ability slot shows a real, distinct icon — no gray boxes, letters, or emoji?
|
|
2272
|
-
- [ ] Do bolts visibly travel before damage lands?
|
|
2273
|
-
- [ ] Only on-demand windows (backpack, log, social) have panel borders?
|
|
2274
|
-
- [ ] Every declared system has its UI end state — quest → tracker, cooldown → sweep + timer, error → floating text? A wired hook with no visual is half a system: finish it or cut it (see `jgengine-newgame`).
|
|
2275
|
-
- [ ] Does the staged shot show the HUD *working* (target locked, cooldown mid-sweep, tracker populated), not resting-empty?
|
|
2276
|
-
- [ ] Would a player think this is intentional art direction? If it looks like a debug build, it ships nothing.
|
|
2582
|
+
## Turn-based & tactics (renderer-free)
|
|
2277
2583
|
|
|
2278
|
-
# jgengine
|
|
2584
|
+
# jgengine domain API — World features
|
|
2279
2585
|
|
|
2280
|
-
Reference module for the [`jgengine-
|
|
2586
|
+
Reference module for the [`jgengine-world` API](SKILL.md) skill. Load this when you need the renderer-free world surface.
|
|
2281
2587
|
|
|
2282
2588
|
## World features
|
|
2283
2589
|
|
|
2284
|
-
Descriptors from `@jgengine/core/world/features`
|
|
2590
|
+
Descriptors from `@jgengine/core/world/features` — config data the runner/world layer interprets:
|
|
2285
2591
|
|
|
2286
2592
|
| Feature | Use |
|
|
2287
2593
|
|---------|-----|
|
|
@@ -2290,93 +2596,166 @@ Descriptors from `@jgengine/core/world/features` — config data the runner/worl
|
|
|
2290
2596
|
| `plots(config)` | Shared city + instanced interiors |
|
|
2291
2597
|
| `tilemap({ map })` | 2D/2.5D levels |
|
|
2292
2598
|
| `flat()` | Plain arena |
|
|
2293
|
-
| `environment({ terrain, sky, weather, vegetation, water, structures, pads })` | Composable outdoor scene
|
|
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 |
|
|
2600
|
+
|
|
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.
|
|
2294
2602
|
|
|
2295
|
-
`
|
|
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).
|
|
2296
2604
|
|
|
2297
|
-
|
|
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`.
|
|
2298
2606
|
|
|
2299
|
-
`parentSpace` positions are local to that space
|
|
2607
|
+
`parentSpace` positions are local to that space — convert at seams only.
|
|
2300
2608
|
|
|
2301
2609
|
### Query primitives (renderer-free, for gameplay)
|
|
2302
2610
|
|
|
2303
|
-
Pure `@jgengine/core` functions so gameplay reads the same world the shell renders
|
|
2611
|
+
Pure `@jgengine/core` functions so gameplay reads the same world the shell renders — no three.js needed:
|
|
2304
2612
|
|
|
2305
2613
|
| Primitive | Answers |
|
|
2306
2614
|
|-----------|---------|
|
|
2307
|
-
| `resolveTerrainField(terrain(...))` / `noiseField(cfg)`
|
|
2308
|
-
| `snapToGround(field, position, offset?)`
|
|
2309
|
-
| `snapEntityToGround(entities, id, field, offset?)`
|
|
2310
|
-
| `windField(cfg)`
|
|
2311
|
-
| `waterSurface(cfg)` / `waterSurfaceFromDescriptor(ocean(...))`
|
|
2312
|
-
| `scatter(cfg)`
|
|
2313
|
-
| `createRegionField({ regions })`
|
|
2314
|
-
| `scatterItems(field, area, layersFor)`
|
|
2315
|
-
| `buildingIndex(district)`
|
|
2615
|
+
| `resolveTerrainField(terrain(...))` / `noiseField(cfg)` → `TerrainField` | `sampleHeight(x,z)`, `sampleNormal(x,z)`, `waterLevel` — ground-snap, collision, camera. `resolveGroundStep` slope-limits movement |
|
|
2616
|
+
| `snapToGround(field, position, offset?)` → `[x,y,z]` | Replaces a position's `y` with the field's sampled ground height (+ `offset`) at its `x`/`z` — the pure version of a spawn/placement ground-snap |
|
|
2617
|
+
| `snapEntityToGround(entities, id, field, offset?)` → `boolean` | Ground-snaps an already-spawned entity in place via `entities.setPose` — false when `id` is unknown; the imperative sibling of `snapToGround` for "drop this entity onto the terrain right now" (mount dismount, teleport, respawn) |
|
|
2618
|
+
| `windField(cfg)` → `WindField` | `at(t)`, `atPoint(x,z,t)`, `strengthAt` — one wind source for weather sway, grass, sailing, fire spread |
|
|
2619
|
+
| `waterSurface(cfg)` / `waterSurfaceFromDescriptor(ocean(...))` → `WaterSurface` | `height(x,z,t)`, `normal`, `displace` — buoyancy, floating, shoreline (CPU Gerstner matching the ocean shader) |
|
|
2620
|
+
| `scatter(cfg)` → `ScatterPoint[]` | Seeded, overlap-aware point distribution — vegetation, props, lots, spawn points (`minDistance`, `avoid` rects) |
|
|
2621
|
+
| `createRegionField({ regions })` → `RegionField` | `sampleRegion(x,z)` blends content-agnostic biomes by nearest selector — height + `tint`/`water`/`fog`/`speedMultiplier` + opaque `data`. Extends `TerrainField`, so it ground-snaps too |
|
|
2622
|
+
| `scatterItems(field, area, layersFor)` → `ScatterInstance[]` | Region-driven content scatter — density per region, grounded, above-water/slope-aware. `pickWeighted` for weighted rolls. (vs `scatter`'s pure geometric points) |
|
|
2623
|
+
| `buildingIndex(district)` → `BuildingIndex` | `at`/`within`/`nearest`/`isInside`/`blockers` over a generated district — placement avoidance, pathfinding |
|
|
2316
2624
|
|
|
2317
|
-
**Voxel field (`world/voxelField`).** `createVoxelField<T extends string>({ chunkSize? })` (default 16) is a logical block lattice for voxel games and instanced renderers
|
|
2625
|
+
**Voxel field (`world/voxelField`).** `createVoxelField<T extends string>({ chunkSize? })` (default 16) is a logical block lattice for voxel games and instanced renderers — distinct from the `voxel()` `WorldFeature` descriptor above (that's the runner-level world kind; this is the block data structure a voxel game's gameplay reads and writes). `set`/`remove`/`get`/`has`/`fill`/`clear`/`count`/`cells`/`bounds` are the block CRUD (`set` returns `false` only when the identical type is already there — a no-op write). `neighbors(x, y, z)` returns the 6-adjacent occupied cells; `exposedFaces(x, y, z)` returns the `VoxelFace`s (`"px"|"nx"|"py"|"ny"|"pz"|"nz"`) not touching another voxel — feed that straight into greedy-meshing/face-culling. `raycast(origin, direction, maxDistance)` runs a 3D DDA and returns `{ x, y, z, type, face, adjacent, distance }`, where `adjacent` is the empty cell just in front of the hit — the placement target for block-place tools. Renderers dirty-track via `chunkOf(x, y, z)` + `chunkVersion(chunk)` and `subscribe(listener)`, so an instanced mesh only rebuilds the chunks that changed. For a non-`environment()` voxel world, assert on `field.summary()` (`{ blocks, types, bounds }`) the same way an `environment()` world asserts on `summarizeEnvironment` (see `jgengine-verify`).
|
|
2318
2626
|
|
|
2319
|
-
**Destructible terrain (`world/carve`).** Two runtime-editable primitives for dig/carve worlds. `VoxelVolume` is a dense grid of material ids (0 = empty)
|
|
2627
|
+
**Destructible terrain (`world/carve`).** Two runtime-editable primitives for dig/carve worlds. `VoxelVolume` is a dense grid of material ids (0 = empty) — `carve({ center, radius, toolStrength })` clears a sphere of solid cells the tool is strong enough to break and returns the count removed (feed a loot roll), `deposit({ center, radius, material })` fills one (Deep Rock tunnels, Astroneer terrain add); `solidAtWorld` reads it back for collision. `CarvableField` (via `carvableTerrain(base)`) wraps any `TerrainField` and writes craters/mounds into its height — `carve({ x, z, radius, depth })`/`deposit({ x, z, radius, height })` — so ground-snap, collision, and the shell mesh all read the deformed surface (Helldivers 2 explosion craters). Cell strengths come from a `VoxelMaterial` table (DATA). Renders through `@jgengine/shell/terrain/CarvedTerrain`.
|
|
2320
2628
|
|
|
2321
2629
|
Renderers for these descriptors live in `@jgengine/shell` (`shell/terrain`, `shell/water`, `shell/weather`, `shell/structures`).
|
|
2322
2630
|
|
|
2323
2631
|
### Environment fields, weather hooks & realm composition
|
|
2324
|
-
Renderer-free survival/environment primitives that extend the world query layer
|
|
2325
|
-
- **Environment field** (`world/envField`): `createEnvironmentField({ dayLength, baseTemperature, nightDrop, altitudeLapse, terrain, rain, occluders, heatSources, ambientFloor, temperatureAt })`
|
|
2326
|
-
- **Weather
|
|
2327
|
-
- **Fire spread** (`world/weather`): `createFireGrid({ cols, rows, cellSize, origin, fuelAt, spreadRate, burnRate, wind, windBias })`
|
|
2328
|
-
- **Realm composition** (`world/realm`): `composeRealm(base, cards)` assembles a played instance at runtime from a deck of modifier **cards** (Nightingale realm cards)
|
|
2632
|
+
Renderer-free survival/environment primitives that extend the world query layer — meters, spawn gating, and damage-in-sunlight read the same world the shell renders, all ticking on game-time `dt`.
|
|
2633
|
+
- **Environment field** (`world/envField`): `createEnvironmentField({ dayLength, baseTemperature, nightDrop, altitudeLapse, terrain, rain, occluders, heatSources, ambientFloor, temperatureAt })` → `EnvironmentField`. Sample **temperature**, **wetness**, **lightExposure** (direct sun/sky), and **ambientLight** (spawn gating) at any `(x, z, time)` — `sample(x, z, time, y?)` returns all four plus `sheltered`. Occluders (roofs/canopy) shade sun and shelter from rain; heat sources (campfires) warm nearby positions; `sunElevation(time)` drives the day cycle. Sun damages a vampire, cold forces campfires, low ambient light spawns mobs — the field answers "am I in sun vs. shade / cold vs. warm / dark vs. lit". Pure and instantaneous; stateful build-up belongs to a decay meter reading the field.
|
|
2634
|
+
- **Weather → gameplay** (`world/weather`): `resolveWeather(state, table)` turns a `WeatherState { kind, intensity }` into concrete `ResolvedWeather` (`grip`, `visibility`, `structureDamage`, `chill`, `ignition`, `spread`) via a game-owned `WeatherModifierTable` — multipliers interpolate from neutral by intensity, rate effects scale linearly. Read `grip`/`visibility` in movement and AI, `structureDamage` on a building tick.
|
|
2635
|
+
- **Fire spread** (`world/weather`): `createFireGrid({ cols, rows, cellSize, origin, fuelAt, spreadRate, burnRate, wind, windBias })` → `FireGrid` is a **coarse cellular** propagation (not a fluid solver): `ignite(x, z)` / `igniteCell(col, row)`, then `step(dt, { spread, wetnessAt })` transfers heat to neighbours biased by wind, consumes fuel (`unburnt → burning → burnt`), and honours firebreaks (zero-fuel cells) and rain/wetness suppression. `resolveWeather(...).spread` feeds the step; `@jgengine/shell/weather` `FireSpreadLayer` renders the burning/scorched cells.
|
|
2636
|
+
- **Realm composition** (`world/realm`): `composeRealm(base, cards)` assembles a played instance at runtime from a deck of modifier **cards** (Nightingale realm cards) — a `major` card is the biome base, `minor` cards layer environment param overrides, a `WeatherState`, and spawn-table edits (`set`/`add`/`scale`/`remove`). The result recomposes both the environment (into a sampleable field via `composed.environmentField(extra?)`) and the `spawnTable`, and depends on the weather hooks above to turn its `weather` into gameplay modifiers.
|
|
2329
2637
|
### Survival meters, moodles & multi-region health
|
|
2330
|
-
The `survival/` domain
|
|
2331
|
-
- **Decay meters** (`survival/decayMeter`): `createDecayMeterSet([{ id, max, min?, start?, rate, thresholds }])`
|
|
2332
|
-
- **Moodles** (`survival/moodle`): the shared status stack, distinct from raw bars. `stackMoodles(...groups)` folds meter, ailment, and buff `Moodle[]` into one worst-first display (same-id stacks add, worst severity wins). `createMoodleStack()` holds timed buffs (`add({ id, label, duration })`
|
|
2333
|
-
- **Multi-region health** (`survival/regionHealth`): `createMultiRegionHealth({ regions, ailments })`
|
|
2638
|
+
The `survival/` domain — decay-over-time meters and per-part health, both feeding one stacking **moodle** status display distinct from numeric bars.
|
|
2639
|
+
- **Decay meters** (`survival/decayMeter`): `createDecayMeterSet([{ id, max, min?, start?, rate, thresholds }])` → `DecayMeterSet`. Each named meter (hunger, thirst, oxygen, sanity, warmth, stamina) drains/recovers on `tick(dt)` at `rate`, refills from consumables/actions via `refill(id, amount)`, and raises threshold moodles (`below`/`above`). `setRateModifier(id, mult)` lets the environment drive them — read an env field, then speed warmth loss when cold or oxygen loss in a toxic biome.
|
|
2640
|
+
- **Moodles** (`survival/moodle`): the shared status stack, distinct from raw bars. `stackMoodles(...groups)` folds meter, ailment, and buff `Moodle[]` into one worst-first display (same-id stacks add, worst severity wins). `createMoodleStack()` holds timed buffs (`add({ id, label, duration })` — Valheim's concurrent food buffs) and expires them on `tick(dt)`.
|
|
2641
|
+
- **Multi-region health** (`survival/regionHealth`): `createMultiRegionHealth({ regions, ailments })` → `MultiRegionHealth` gives per-part pools (head/thorax/arms/legs, Tarkov/DayZ style) — `damage(regionId, amount)` scales by `vulnerability` and kills when a `vital` part empties; a stacking **ailment queue** (`applyAilment`, `tick(dt)` drains like bleed) carries per-injury treatment (`treat(itemId)` clears wounds via bandage/tourniquet/splint). `ailmentMoodles()` shares the moodle display with the meters (#78 + #90).
|
|
2334
2642
|
### Interactive building & terraform (renderer-free tools)
|
|
2335
2643
|
Turn data-only placement into the build tooling of Valheim/Enshrouded/The Sims/Fortnite/Dinkum. All pure `@jgengine/core/world`; the shell renders the ghost/tint/brush (`shell/structures/PlacementGhost`, `shell/terrain/EditableGround`, `shell/terrain/TerraformBrushCursor`) driven by `pointer.worldHit()`.
|
|
2336
2644
|
| Primitive | Answers |
|
|
2337
2645
|
|-----------|---------|
|
|
2338
|
-
| `createPlacementController({ footprint, rules, snapMode, grid })` | Owns the ghost: `hover(hit)`
|
|
2339
|
-
| `snapToNearest(registry, placed, movingDef, cursor, { snapDistance })` | Typed connector sockets
|
|
2340
|
-
| `solveSupport(pieces, links, { maxDistance })`
|
|
2341
|
-
| `createWallDrawTool({ snap, closeTolerance })` | Drag wall points
|
|
2342
|
-
| `createPlacedStructureStore()` | Save/load a built layout: `add`/`move`/`rotate`/`remove`/`select`, `snapshot()
|
|
2343
|
-
| `createEditableTerrain({ bounds, base, cellSize })`
|
|
2646
|
+
| `createPlacementController({ footprint, rules, snapMode, grid })` | Owns the ghost: `hover(hit)` → `PlacementPreview` (`valid` tint wraps `validatePlacement`), `rotate()`, `setSnapMode`/`cycleSnapMode` (`"grid"`/`"free"`/`"surface"`), `commit()` → `PlacementCommit` (`rotationY` via `quarterTurnsToRotationY`). Feed it `pointer.worldHit()`. |
|
|
2647
|
+
| `snapToNearest(registry, placed, movingDef, cursor, { snapDistance })` | Typed connector sockets — snaps a piece's socket onto the nearest **compatible** placed socket (`socketsCompatible` = both sides `accept` the other type). `worldSockets`/`socketWorldPosition` expand a piece's sockets to world space. |
|
|
2648
|
+
| `solveSupport(pieces, links, { maxDistance })` → `SupportResult` | Walks the connector graph to any `grounded` piece: `supported` stays, `unsupported` collapses, `distance` (hops-to-ground) drives the white→red decay tint. `toDebrisBodies(pieces, unsupported)` → `AddBodyOptions[]` for the `PhysicsWorld` debris sink. |
|
|
2649
|
+
| `createWallDrawTool({ snap, closeTolerance })` | Drag wall points → auto-encloses when the path returns to the start (`isEnclosed`), `footprint()` derives the room `EnclosedFootprint`, `roof()` auto-fits a hip/gable/flat `RoofPlan`. `createSurfacePaint()` stores per-tile floor/wall surfaces. |
|
|
2650
|
+
| `createPlacedStructureStore()` | Save/load a built layout: `add`/`move`/`rotate`/`remove`/`select`, `snapshot()`↔`load()` round-trip (survives reload), `subscribe` for the renderer. |
|
|
2651
|
+
| `createEditableTerrain({ bounds, base, cellSize })` → `EditableTerrain` | A `TerrainField` you can **write back to**: `apply(edit: TerraformEdit)` raises/lowers/flattens/paints under a cursor and re-samples `sampleHeight`; `surfaceAt`, `snapshot`/`restore`, `reset`. `createTerraformBrush(field)` is the cursor tool (`raise`/`lower`/`flatten`/`paint`, radius/strength). This write-back grid is the shared terrain-edit pattern. |
|
|
2344
2652
|
| `createPlotPermissions({ plotId, ownerId, guildId? })` + `createContributionPool(goal)` | Per-plot/guild edit authority (`canEdit`/`canView`, `grant`/`revoke` `BuildRole`, guild inheritance) for co-op building, plus a pooled-resource contribution model (`contribute` caps at the goal, reports overflow, `isComplete`, per-contributor totals). |
|
|
2345
2653
|
|
|
2346
2654
|
### Physics world (optional, headless)
|
|
2347
2655
|
|
|
2348
|
-
`physics/physicsWorld` `PhysicsWorld` is a standalone fixed-capacity rigid-body sim (SoA buffers, spatial-hash broadphase, sleeping)
|
|
2656
|
+
`physics/physicsWorld` `PhysicsWorld` is a standalone fixed-capacity rigid-body sim (SoA buffers, spatial-hash broadphase, sleeping) — **not** the `defineGame` `physics: { gravity, jumpVelocity }` field, which the built-in walk controller reads directly every frame (see "Controller kinematics" above; both values are real and honored, not dead config). Reach for `PhysicsWorld` when a game needs many colliding dynamic bodies (piles, debris, stress scenes): `new PhysicsWorld({ capacity, bounds, … })`, `addBody({ position, mass?, ...shape })`, then `step(dt)` per tick → `PhysicsStats`. Core owns the sim; `@jgengine/shell/world/InstancedBodies` renders its bodies. Most games never need it — the character controller covers ordinary movement. The broadphase grid (`nx*ny*nz` cells from `bounds`/`cellSize`) throws at construction if it would exceed a sane cell cap — shrink `bounds` or raise `cellSize` (same guard on `physics/spatialGrid`'s `SpatialGrid`).
|
|
2349
2657
|
|
|
2350
|
-
**Body shape: box or sphere.** `AddBodyOptions` is a discriminated union
|
|
2658
|
+
**Body shape: box or sphere.** `AddBodyOptions` is a discriminated union — `{ shape?: "box", halfExtents }` (box is the default, `shape` omittable) or `{ shape: "sphere", radius }` (the radius fills all three half-extent columns, so broadphase/bounds see the sphere's enclosing AABB). `world.shape[i]` (`SHAPE_BOX` / `SHAPE_SPHERE`) reports a live body's shape for a consumer walking the raw SoA arrays. Sphere-sphere and sphere-box pairs resolve with a proper radial normal (not the axis-aligned box/box path) — balls, projectile bodies, and rolling debris collide correctly against both boxes and each other.
|
|
2351
2659
|
|
|
2352
|
-
**Collision shapes are box/sphere/voxel only.** Every collider in the engine
|
|
2660
|
+
**Collision shapes are box/sphere/voxel only.** Every collider in the engine — `PhysicsWorld` bodies (box `halfExtents` or sphere `radius`), object/entity picking (`scene/objectQuery` raycasts against unit boxes), and `world/voxelField`/`world/carve` blocks — is an axis-aligned box, a sphere, or a voxel cell. Arbitrary authored level-mesh collision (a sculpted GLB as a collider) is not supported. The seams for custom collision are `movement.beforeCommit` (steer or replace the walk controller's resolved step) and object raycasts (query the scene yourself and react) — not a mesh collider.
|
|
2353
2661
|
|
|
2354
|
-
**Ballistic collision sweep (`physics/ballisticSweep`).** `createBallisticSweep(world, { step?, radius? })`
|
|
2662
|
+
**Ballistic collision sweep (`physics/ballisticSweep`).** `createBallisticSweep(world, { step?, radius? })` → `BallisticSweep`, a `(origin, velocity, gravity, maxTime) => BallisticSweepHit | null` function that marches the closed-form arc (constant gravity, straight lateral) through a `PhysicsWorld` and reports the first sample inside any live body's AABB (sleeping bodies included), refined by one bisection step; `null` means the whole arc is clear. `step` (default 1/60) is the march interval in seconds, `radius` (default 0) inflates every body's AABB before the point test — pass the projectile's own radius. Wire it into `combat/projectiles` via `ProjectileSystemDeps.sweepBallistic`: when set, a ballistic shot settles at the swept impact point instead of the closed-form landing; omitted or `null` falls back to that closed-form arc.
|
|
2355
2663
|
|
|
2356
|
-
**Removing and moving bodies.** `removeBody(id)` tombstones a body
|
|
2664
|
+
**Removing and moving bodies.** `removeBody(id)` tombstones a body — it drops out of integration/broadphase and its slot is queued for the next `addBody` — without moving or invalidating any other body's `id` (ids are raw SoA slots, stored as-is in joints and game state, so nothing ever gets swapped). It conservatively wakes any sleeping body whose AABB touched the removed one's (no persistent contact set to consult, so this errs toward waking too much, never too little). `setVelocity(id, x, y, z)` and `setPosition(id, x, y, z)` write a body's velocity/position directly (instead of poking the public `velX`/`posX` SoA arrays) and wake it if asleep; `teleport(id, x, y, z)` is `setPosition` plus a hard velocity reset (respawn/teleporter, vs. sliding). `isAlive(id)`/`highWater` (one past the highest slot ever handed out) let a consumer that iterates the raw SoA arrays skip tombstoned holes correctly instead of assuming `count` is a dense `0..count` range.
|
|
2357
2665
|
|
|
2358
|
-
**Joints & constraints.** `hingeJoint`/`fixedJoint`/`distanceJoint`/`springJoint(opts)` connect two bodies, or a body to a fixed world point (omit `bodyB`). The sim is translational (no angular DOF), so `hinge`/`fixed` pin the shared anchor (the `axis` is retained metadata), `distance` holds a fixed separation, and `spring` drives toward `restLength` with `stiffness`/`damping` (suspension, follow-point carry). `removeJoint(id)`, `setJointAnchor(id, x, y, z)` (move anchor B
|
|
2666
|
+
**Joints & constraints.** `hingeJoint`/`fixedJoint`/`distanceJoint`/`springJoint(opts)` connect two bodies, or a body to a fixed world point (omit `bodyB`). The sim is translational (no angular DOF), so `hinge`/`fixed` pin the shared anchor (the `axis` is retained metadata), `distance` holds a fixed separation, and `spring` drives toward `restLength` with `stiffness`/`damping` (suspension, follow-point carry). `removeJoint(id)`, `setJointAnchor(id, x, y, z)` (move anchor B — a world anchor's follow point, or body B's local offset), `setJointAnchorA(id, x, y, z)` (move anchor A's local offset — e.g. re-rotating a suspension mount each frame as the chassis turns), `setJointRest`, and `readJointSegments(out)` for `@jgengine/shell/world/InstancedJoints` (debug line render). This is the foundation under vehicles, ragdolls, grapples, and carry.
|
|
2359
2667
|
|
|
2360
|
-
**Collision
|
|
2668
|
+
**Collision → gameplay events.** `world.onCollision(listener, minApproachSpeed?)` delivers every impacting contact — `CollisionEvent { a, b, nx, ny, nz, approachSpeed, impulse }` — to game code during `step` (the object is reused; read/copy it, never retain). This is the seam crash-damage and destruction read; pass `null` to detach.
|
|
2361
2669
|
|
|
2362
|
-
**Actors on top of the sim:** `physics/ragdoll` (`createRagdoll(world, { bones, links, balance? })`
|
|
2670
|
+
**Actors on top of the sim:** `physics/ragdoll` (`createRagdoll(world, { bones, links, balance? })` — jointed bones, floppy or active-ragdoll via a balance motor), `physics/carryable` (`Carryable` — grab a body to a follow point, shared multi-owner carry, `carrySpeedMultiplier` encumbrance, drop/throw; the raycast pick is the caller's job, core owns the constraint), `physics/forceVolume` (`ForceVolume` — impulse/velocity/accelerate trigger region, `once` for boost pads; `PlatformCarry` — carry bodies standing on a moving platform by its per-`step` delta). Separately, `physics/spatialGrid` `SpatialGrid` is a broad-phase grid over the x/z plane, **distinct** from the rigid-body sim, for cheap same-tick proximity across hundreds–thousands of simple movers — `rebuild(count, xs, zs)` then `queryCircle` (swarm enemies hitting a player/AoE) or `forEachPair` (mutual separation).
|
|
2363
2671
|
|
|
2364
|
-
**
|
|
2365
|
-
|
|
2672
|
+
**Swarm-scale presentation.** `@jgengine/shell/world/SpriteBatch` renders up to `capacity` sprite-sheet quads as one `InstancedMesh` (per-instance atlas `frame`, `plane: "xy" | "xz"` or `billboard`, `pixelated` nearest-filtering) — the batched path for bullet-heaven swarms, item streams, and tile-grid presentation instead of one `<sprite>` per entity. Pair it with `world/lod`'s `createLodScheduler` (see the concept table) to band render detail by distance and throttle far entities' update work.
|
|
2673
|
+
|
|
2674
|
+
**Traversal (`physics/traversal`).** `Grapple` fires a rope from a body to a fixed world point on the joint API — `fire(x,y,z)` attaches a `distance` (rigid) or `elastic` (spring) joint, `reel(dt)`/`payOut(dt)` shorten/lengthen the rope to pull the traveller in, `moveAnchor` re-points it (ziplines, grapple-to-moving-target). Grapple/zipline/swing (Sekiro, Deep Rock, Just Cause) are all the same primitive; the raycast that finds the anchor is the caller's. `Glide` is a reduced-gravity, forward-thrust wingsuit/glider over a body — call `apply(dt, steerX, steerZ)` each frame before `step` to feed back most of gravity (`gravityScale`), thrust along the steer vector, and clamp descent; stop calling it to fall normally, no attach/detach state.
|
|
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).
|
|
2366
2676
|
### Vehicles, mounts, crash damage & racing
|
|
2367
|
-
Five primitives layer a driving/racing game over the physics sim and `world/water`. All are **data-first** (spec the chassis/wheels/grip curve, damage thresholds, and checkpoint layout as catalog data) and pure `@jgengine/core`; renderers live in the game/shell. Each `update(dt,
|
|
2368
|
-
- **Analog input
|
|
2369
|
-
- **`physics/vehicleBody`.** `createVehicleBody(world, config)` is an arcade car: a chassis box body with per-wheel suspension held by G3's `springJoint` against the sampled `groundHeight`, drive/brake along the heading, and a `GripCurve` (`sampleGripCurve`) that bleeds lateral velocity for cornering
|
|
2370
|
-
- **`physics/buoyancy`.** `createBuoyantBody(world, { body, water,
|
|
2371
|
-
- **`scene/mount`.** `createMountController()` transfers control to a driven entity: `register({ id, kit, seats })`, `mount(riderId, mountId, seatId?)`, `dismount`. Read `cameraTarget(riderId)` to point the follow camera and `driveTarget(riderId)` to route that rider's `AxisInput` at the mount
|
|
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)`.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
2372
2682
|
- **`scene/stationClaim`.** `createStationClaim(controller?)` layers **facet stations** on `scene/mount` for a vehicle several players crew at once: `register({ id, kit, stations })` where each `Station` tags a seat with a `facet` (`"steer"`/`"sails"`/`"cannon"`). `claim(playerId, vehicleId, facetOrStationId)`, `release`, `controllerOf(vehicleId, facet)` (who mans it), `facetOf(playerId)`, `openFacets`, `crew`. Only a `control` station operates the hull (`driver`/`driveTarget`); the rest ride but command their own facet. Sea of Thieves helm + sails + cannons.
|
|
2373
2683
|
- **`physics/damageZones`.** `createDamageModel({ zones, disableAt })` maps accumulated contact impulse (from `onCollision`) to **coarse discrete stages** (not soft-body): `absorb(zoneId, impulse)` / `routeCollision(event, resolveZone)` bump a zone's stage (caller swaps the visual/collider), an optional `detachStage` ejects a part as debris once, and crossing `disableAt` flips a whole-vehicle `disabled` state. Wreckfest crumple/derby.
|
|
2374
|
-
- **`game/race`.** `raceTrack({ checkpoints, laps })` is an ordered ring of AABB checkpoint volumes (the final one is the finish line); `createRaceState({ track, win })`
|
|
2684
|
+
- **`game/race`.** `raceTrack({ checkpoints, laps })` is an ordered ring of AABB checkpoint volumes (the final one is the finish line); `createRaceState({ track, win })` — driven each tick by `update(now, positions)` on game time — emits `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished`, keeps split times, resolves a pluggable `RaceWinCondition` (`firstPastPost`, `topK` round-cut, `everyoneFinishes`, `lastStanding` derby), and `resetToCheckpoint(id)` hands back a respawn pose. `removeRacer(id)` drops a racer mid-race and renumbers the remaining standings; `reset()` clears all racer progress/finish state back to construction time so the same instance replays without rebuilding it. Trackmania, Mario Kart, Fall Guys.
|
|
2375
2685
|
|
|
2376
2686
|
### Spawn placement
|
|
2377
2687
|
|
|
2378
|
-
`spawn(catalogId, { id?, position | anchor, offset?, parentSpace?, group? })`
|
|
2688
|
+
`spawn(catalogId, { id?, position | anchor, offset?, parentSpace?, group? })` — anchor `{ kind: "entity" | "zone", id }` with offset `{ radius, pattern }` or `{ xyz }`. Catalog supplies movement/model; no behaviors on spawn.
|
|
2689
|
+
|
|
2690
|
+
**Named spawn/respawn points** (`game/spawnPoints`) — `createSpawnPoints()`: `record(id, { x, y, z, rotationY? })` names a point (level bounds, team spawns, checkpoints), `get(id)`/`list()` read them back, `respawn(entities, entityId, spawnId)` teleports an existing entity to a recorded point via `setPose` in one call — the id-keyed alternative to threading raw coordinates through respawn logic by hand.
|
|
2691
|
+
|
|
2692
|
+
**Level sequence** (`game/levelSequence`) — `createLevelSequence({ levels: [{ id, config }], retriesPerLevel? })` is a pure, deterministic campaign machine: `start()` enters level 0, `clear()` marks the current level cleared, `advance()` moves to the next (or `"complete"` after the last), `fail()` consumes an attempt and returns `"retry"` while `retriesPerLevel` remain else `"failed"`, `retry()` restarts after a retry-eligible failure. `current()` → `{ id, index, config, attempt } | null`; `progress()` → `{ index, total, cleared }`; `reset()` rewinds to idle. Mirrors the reducer style of `game/race` and `ai/spawnDirector` — a level-select/roguelike-run campaign shell without hand-rolling the state machine per game.
|
|
2693
|
+
|
|
2694
|
+
# jgengine-api — Cartridge
|
|
2695
|
+
|
|
2696
|
+
Reference module for the [`jgengine`](./SKILL.md) skill. Load this when building a game as a **cartridge** — one declarative config instead of hand-wired loop/state/UI files. A cartridge game is ~75% smaller than the classic shape and nearly all of it is data; reach for `defineGame` directly only when the game's simulation doesn't fit the cartridge archetypes even through the escape hatches.
|
|
2697
|
+
|
|
2698
|
+
## Why
|
|
2699
|
+
|
|
2700
|
+
Across the classic games, 78–91% of each game's LOC was generic wiring restated per game: a run-state store with subscribe/notify, chase-and-contact enemy loops, auto-fire weapon routines, magnet pickups, draft plumbing, phase-gated HUD trees, and tests for all of it. The cartridge layer owns every one of those; the game ships only its **data** (catalogs, tuning, layout) and any genuinely bespoke rules through typed escape hatches.
|
|
2701
|
+
|
|
2702
|
+
## Shape
|
|
2703
|
+
|
|
2704
|
+
`game.config.ts` is the whole game — `cartridge({...})` from `@jgengine/shell/cartridge` compiles the config into a `PlayableGame` (validating it first — a bad reference throws at import with a list of problems). `loop.ts` and `world.ts` disappear; the spec carries the world feature and the loop is engine-owned. `main.tsx` is 5 lines via `mountGame` (`@/components/ui/mount`, registry). Sprites/SVG art stay in `src/game/assets.ts`; the game keeps one test file under `src/game/`.
|
|
2705
|
+
|
|
2706
|
+
```ts
|
|
2707
|
+
import { cartridge, type CartridgeConfig } from "@jgengine/shell/cartridge";
|
|
2708
|
+
import { standardCartridgePanels } from "@/components/ui/cartridge-panels";
|
|
2709
|
+
|
|
2710
|
+
export const config: CartridgeConfig = {
|
|
2711
|
+
name, seed, panels: standardCartridgePanels, assets, entitySprites,
|
|
2712
|
+
flow: { start: "gate", countdownSeconds: 3, restart: true }, // press-to-start → 3-2-1 → playing; "restart" command
|
|
2713
|
+
player: { kind, health, walkSpeed }, // compiled into content + spawned per player
|
|
2714
|
+
enemies: { id: { label, health, walkSpeed, xp, contact: { damage, intervalSeconds }, behavior: "chase" | "none" } },
|
|
2715
|
+
combat: { contactRadius },
|
|
2716
|
+
spawning: { director: SpawnDirectorConfig, placement: { kind: "ring", radius } },
|
|
2717
|
+
weapons: { id: { kind: "projectile" | "orbit" | "pulse" | "custom", damage, cooldownMs, maxLevel, ... } },
|
|
2718
|
+
progression: { xp: Curve, maxLevel, draft: { choices, upgrades } },
|
|
2719
|
+
fields: { magnetRadius, damageMultiplier }, // named run-scalars upgrades mutate
|
|
2720
|
+
xpGems: { collectRadius, pullSpeed, rarityThresholds, defaultRarity },
|
|
2721
|
+
rules: { win: { kind: "survive", seconds }, lose: { kind: "playerDeath" } | { kind: "custom", check }, killLeaderboardStat },
|
|
2722
|
+
world, physics, camera, worldItem, theme, hud, screens, // screens: start/win/lose
|
|
2723
|
+
};
|
|
2724
|
+
export const game = cartridge(config);
|
|
2725
|
+
```
|
|
2726
|
+
|
|
2727
|
+
The run is a phase machine every game gets for free: `start → countdown → playing → won | lost`. `flow.start: "gate"` renders `screens.start` (TitleScreen binding) until begin; `flow.countdownSeconds` renders the big-number countdown; `flow.restart: true` registers a `restart` command that resets the run in place, clears cartridge entities, and restores player stats — no hand-rolled session store, phase union, or reset function. `run.playingSeconds` is the run clock (drives the survive rule and the timer panel), so pauses and pre-game phases never skew timing.
|
|
2728
|
+
|
|
2729
|
+
## Core surface (`@jgengine/core/cartridge/`)
|
|
2730
|
+
|
|
2731
|
+
- `@jgengine/core/cartridge/spec` — the `CartridgeSpec` types, `Leveled` values (`number | { base, perLevel, min?, max? } | { table }` resolved by `leveled(value, level)`), `WASD_KEYBINDS` (the default when `input` is omitted).
|
|
2732
|
+
- `@jgengine/core/cartridge/runtime` — `createCartridge(spec)` → `{ content, loop, run(ctx), weaponKit(ctx), chooseUpgrade(ctx, offerId) }`. Owns the run store (outcome/kills/fields/weapon levels, subscribe/notify), spawn director advance + ring placement, chase + contact-damage enemies (terrain-grounded), the three weapon archetypes with auto-targeting, magnet xp-gem pickups feeding `leveling()`, pause-and-draft level-ups, kill → gem drop + leaderboard, and win/lose rules.
|
|
2733
|
+
- `@jgengine/core/cartridge/validate` — `validateCartridge(spec)` → problem list: spawn waves must reference declared enemies, upgrades must reference declared weapons/fields, stacks can't exceed weapon `maxLevel`, tuning must be positive, rarity thresholds descending. The game's test asserts it returns `[]`.
|
|
2734
|
+
|
|
2735
|
+
Weapon archetypes: `projectile` (auto-target nearest in `range`, travel-time bolt fx), `orbit` (leveled blade count/radius sweeping `hitRadius`), `pulse` (radial AoE with linear falloff and an expanding-ring fx). Upgrade effects: `weaponLevel` (+1 level per stack), `statBonus` (+max and +current on pick), `fieldAdd` / `fieldMultiply` (recomputed from stacks). Weapon damage is multiplied by the `damageMultiplier` field when declared.
|
|
2736
|
+
|
|
2737
|
+
## Presentation (shell + registry)
|
|
2379
2738
|
|
|
2380
|
-
|
|
2739
|
+
`hud.panels` is a schema — items `vital | xp | timer | score | abilityBar | component`, anchored via the movable HUD layout; `screens.win/lose` render the results/death screens; the upgrade-draft modal appears whenever a draft is pending. The shell renders the frame and injects concrete components through the `panels` seam — `standardCartridgePanels` (registry `cartridge-panels`) binds the jgengine UI kit; swap any binding for a custom component without touching the engine. Weapon fx render engine-side from the run store's bolt/pulse feeds (`fxColor`/`fxEmissive` per weapon). `theme` is the `--jg-*` CSS var block — hand-pick it or derive from a few seed colors with `deriveJgTheme` (registry `jg-theme`).
|
|
2740
|
+
|
|
2741
|
+
## Escape hatches — bespoke logic without leaving the cartridge
|
|
2742
|
+
|
|
2743
|
+
- `weapons.<id> = { kind: "custom", fire(ctx, run, args) }` — cooldown/level/damage handled for you; the fire shape is yours.
|
|
2744
|
+
- `spawning.placement = { kind: "custom", position(ctx, run) }`.
|
|
2745
|
+
- `rules.win = { kind: "custom", check(ctx, run) }`.
|
|
2746
|
+
- `progression.draft.upgrades[].effect = { kind: "custom", apply(ctx, run, stacks) }`.
|
|
2747
|
+
- `systems: [(ctx, run, dt) => ...]` — extra per-tick simulation after the built-ins, only while playing.
|
|
2748
|
+
- `hud` items `{ kind: "component", Component }` — arbitrary React panels alongside the schema ones.
|
|
2749
|
+
|
|
2750
|
+
A game that is *mostly* bespoke simulation (custom physics, session machines) should keep `defineGame` and write its systems — the cartridge is for games whose shape the archetypes already cover.
|
|
2751
|
+
|
|
2752
|
+
## Testing
|
|
2753
|
+
|
|
2754
|
+
Cartridge behaviors are engine-tested once (`packages/core/src/cartridge/runtime.test.ts`); the game test is one call plus any game-specific assertions:
|
|
2755
|
+
|
|
2756
|
+
```ts
|
|
2757
|
+
import { bootCartridge, cartridgeSmokeTest, tickCartridge } from "@jgengine/core/cartridge/testkit";
|
|
2758
|
+
cartridgeSmokeTest(config); // validate + world summary + headless run/spawn/kill/gem smoke
|
|
2759
|
+
```
|
|
2381
2760
|
|
|
2382
|
-
|
|
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.
|