diff --git a/.claude/skills/jgengine-assets/api.md b/.claude/skills/jgengine-assets/api.md index 554036611..6c194d8f3 100644 --- a/.claude/skills/jgengine-assets/api.md +++ b/.claude/skills/jgengine-assets/api.md @@ -111,6 +111,10 @@ - `ExtractedSpriteFile` (interface): interface ExtractedSpriteFile — One SVG/PNG file pulled out of a sprite/icon-pack archive by `extractSpriteFiles`. - `ExtractedTexture` (interface): interface ExtractedTexture — ⚠ undocumented - `FetchLike` (type): type FetchLike = typeof fetch — ⚠ undocumented +- `MAX_ARCHIVE_COMPRESSION_RATIO` (const): const MAX_ARCHIVE_COMPRESSION_RATIO: 100 — Max allowed originalSize/size ratio for a single archive entry — past this it's treated as a zip bomb. +- `MAX_ARCHIVE_DOWNLOAD_BYTES` (const): const MAX_ARCHIVE_DOWNLOAD_BYTES: number — Max size of a downloaded (still-compressed) archive, in bytes. Provider zips run tens of MB; this leaves headroom without buffering an unbounded response. +- `MAX_ARCHIVE_ENTRY_COUNT` (const): const MAX_ARCHIVE_ENTRY_COUNT: 20000 — Max number of entries this module will extract out of one archive. +- `MAX_ARCHIVE_UNCOMPRESSED_BYTES` (const): const MAX_ARCHIVE_UNCOMPRESSED_BYTES: number — Max total uncompressed size this module will inflate out of one archive, in bytes. ## @jgengine/assets/find diff --git a/.claude/skills/jgengine-combat/api.md b/.claude/skills/jgengine-combat/api.md index 8ecb24383..6639ab816 100644 --- a/.claude/skills/jgengine-combat/api.md +++ b/.claude/skills/jgengine-combat/api.md @@ -93,6 +93,10 @@ - `deathReasonFromEffect` (function): function deathReasonFromEffect(ctx: EffectDeathContext): DeathReason — ⚠ undocumented - `normalizeOnDeath` (function): function normalizeOnDeath(spec: OnDeathSpec | null | undefined): NormalizedOnDeath — ⚠ undocumented +## @jgengine/core/combat/deathReason + +- `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; via?: { item?: string } } | { kind: "environment"; source: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event. + ## @jgengine/core/combat/defensiveWindow - `DefenseKind` (type): type DefenseKind = "parry" | "block" | "dodge" — ⚠ undocumented diff --git a/.claude/skills/jgengine-editor/api.md b/.claude/skills/jgengine-editor/api.md index ea84fe15a..0d7d41bd6 100644 --- a/.claude/skills/jgengine-editor/api.md +++ b/.claude/skills/jgengine-editor/api.md @@ -58,7 +58,7 @@ - `DEFAULT_PAINT_SETTINGS` (const): const DEFAULT_PAINT_SETTINGS: PaintSettings — The terrain tool's default paint controls. - `DEFAULT_SCULPT_SETTINGS` (const): const DEFAULT_SCULPT_SETTINGS: SculptSettings — The terrain tool's default brush controls. - `EDITOR_MCP_TOOLS` (const): const EDITOR_MCP_TOOLS: readonly EditorMcpTool[] — Full set of MCP tools an agent can call to drive the live scene editor. -- `EditorApp` (function): function EditorApp({ gameId, playable, layers, save }: EditorAppProps): React.JSX.Element — Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. +- `EditorApp` (function): function EditorApp({ gameId, playable, layers, save, modeChip }: EditorAppProps): React.JSX.Element — Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. - `EditorAppProps` (interface): interface EditorAppProps — Props for mounting the scene editor over a playable game. - `EditorAssetEntry` (interface): interface EditorAssetEntry — A searchable, placeable asset shown in the editor's asset browser panel. - `EditorAssetInfo` (interface): interface EditorAssetInfo — A placeable asset entry offered in the editor's asset browser. @@ -66,7 +66,7 @@ - `EditorBridgeResponse` (type): type EditorBridgeResponse = { ok: boolean; result?: unknown; error?: string; } — Result envelope returned by every editor host RPC call. - `EditorBridgeServer` (interface): interface EditorBridgeServer — A running editor bridge server: its bound port, URL, and a stop handle. - `EditorBridgeServerOptions` (interface): interface EditorBridgeServerOptions — Options for starting the editor's HTTP bridge server: host api, port, hostname. -- `EditorCameraDriver` (function): function EditorCameraDriver({ api }: { api: EditorHostApi }): null — Smoothly pans the orbit camera to the editor host's focus target when it changes. +- `EditorCameraDriver` (const): const EditorCameraDriver: React.MemoExoticComponent<({ api }: { api: EditorHostApi; }) => null> — Smoothly pans the orbit camera to the editor host's focus target when it changes. - `EditorChrome` (function): function EditorChrome({ gameId, session, api, assets, ui, baselineJson, save, }: { gameId: string; session: EditorSession; api: EditorHostApi; assets: readonly EditorAssetEntry[]; ui: EditorUiStore; baselineJson?: string; save?: (json: string) => Promise<{ ok: boolean; path?: string; error?: string … — The full editor UI shell — toolbar, left panels (outliner/prefabs/sets/layers), viewport overlays, the selector-subscribed {@link InspectorPanel}, and the asset browser — wired to the session, UI store, and host RPC. Mounted by `EditorApp`; not a game-author entry point. - `EditorHostApi` (interface): interface EditorHostApi — The live editor's global control surface — session, visibility, camera focus, assets, mode, RPC. - `EditorLayerOverlays` (function): function EditorLayerOverlays({ document, visibility, selection, onSelect, activePathPoint, groundHeightAt, }: { document: EditorDocument; visibility: EditorKindVisibility; selection: readonly string[]; onSelect: (id: string) => void; activePathPoint?: { pathId: string; index: number } | null; ground… — Renders every visible marker, volume, path, and note from a document as in-scene 3D gizmos. @@ -80,10 +80,10 @@ - `GizmoMode` (type): type GizmoMode = "translate" | "rotate" | "scale" — Which transform gizmo is active for the current selection. - `PaintSettings` (interface): interface PaintSettings — Live terrain material-paint controls driven by the terrain tool panel. - `PathDraftPreview` (function): function PathDraftPreview({ points }: { points: readonly EditorVec3[] }): React.JSX.Element — Live preview of an in-progress path drawing: placed points and the connecting line. -- `PerfProbe` (function): function PerfProbe({ api }: { api: EditorHostApi }): null — In-canvas frame counter: publishes fps/draw-call samples to the editor host every 500ms. +- `PerfProbe` (const): const PerfProbe: React.MemoExoticComponent<({ api }: { api: EditorHostApi; }) => null> — In-canvas frame counter: publishes fps/draw-call samples to the editor host every 500ms. - `PlacementTool` (type): type PlacementTool = | { tool: "marker"; kind: string } | { tool: "volume"; kind: string; shape: EditorVolumeShape } | { tool: "note" } | { tool: "path"; kind: string } — The active creation tool — what a viewport click places next. - `SculptSettings` (interface): interface SculptSettings — Live terrain-brush controls driven by the terrain tool panel. -- `SelectionGizmo` (function): function SelectionGizmo({ session, ui, groundSnap, }: { session: EditorSession; ui: EditorUiStore; groundSnap?: (x: number, z: number) => number; }): React.JSX.Element | null — Drag-to-transform gizmo bound to the current selection, dispatching editor commands on release. Translating with a multi-selection moves every selected object by the drag delta; scaling a volume resizes its true shape (radius, height, or box half-extents); a selected path vertex moves just that point. Snapping follows the UI store: terrain height, grid quantization, or free movement. +- `SelectionGizmo` (const): const SelectionGizmo: React.MemoExoticComponent<({ session, ui, groundSnap, }: { session: EditorSession; ui: EditorUiStore; groundSnap?: ((x: number, z: number) => number) | undefined; }) => React.JSX.Element | null> — Drag-to-transform gizmo bound to the current selection, dispatching editor commands on release. Translating with a multi-selection moves every selected object by the drag delta; scaling a volume resizes its true shape (radius, height, or box half-extents); a selected path vertex moves just that point. Snapping follows the UI store: terrain height, grid quantization, or free movement. - `SnapMode` (type): type SnapMode = "ground" | "grid" | "off" — How gizmo drags land: stick to terrain height, quantize to a grid, or free. - `StandaloneAsset` (interface): interface StandaloneAsset — One user-supplied model the standalone editor can place: a stable id and a resolvable URL. - `StandaloneEditor` (function): function StandaloneEditor({ sceneId = "standalone", scene, assets, world, save, hidePickers = false, }: StandaloneEditorProps): React.JSX.Element — The scene editor, mounted over a blank gameless world instead of a game — the same `EditorApp` every jgengine game ships, usable standalone on the user's own project (CLI `jgengine editor`, desktop app, or any React host). Ships a slim strip to open a world file and pull in an asset folder; both are also settable up front through props. @@ -94,7 +94,7 @@ - `TerrainBrushKind` (type): type TerrainBrushKind = "raise" | "lower" | "smooth" | "flatten" | "noise" | "ramp" — A heightfield sculpt brush the terrain tool can apply. - `TerrainMaterial` (interface): interface TerrainMaterial — A paintable terrain material layer — a surface id plus the color it renders as. - `TerrainMode` (type): type TerrainMode = "sculpt" | "paint" — The terrain tool's active sub-mode: reshape the heightfield, or paint material layers onto it. -- `ViewportSelect` (function): function ViewportSelect({ api, ui }: { api: EditorHostApi; ui: EditorUiStore }): null — Canvas click-to-select and click-to-place. Document objects pick by screen proximity (registration always matches what you see) with click-cycling through stacked candidates and shift/ctrl additive selection; everything else picks by occlusion-ordered raycast against the tagged scene graph. When a placement tool is armed, clicks author new markers, volumes, notes, or path points at the ground hit instead of selecting. +- `ViewportSelect` (const): const ViewportSelect: React.MemoExoticComponent<({ api, ui }: { api: EditorHostApi; ui: EditorUiStore; }) => null> — Canvas click-to-select and click-to-place. Document objects pick by screen proximity (registration always matches what you see) with click-cycling through stacked candidates and shift/ctrl additive selection; everything else picks by occlusion-ordered raycast against the tagged scene graph. When a placement tool is armed, clicks author new markers, volumes, notes, or path points at the ground hit instead of selecting. - `VirtualWindow` (interface): interface VirtualWindow — The visible slice of a fixed-row-height list: which rows to mount and the spacer geometry. - `assetsFromCatalog` (function): function assetsFromCatalog(ids: readonly string[], resolve?: (id: string) => { url?: string } | null): EditorAssetEntry[] — Turns a game's asset catalog ids into editor asset entries for the browser panel. - `blankWorld` (function): function blankWorld(seed = "standalone"): EnvironmentWorldFeature — The default flat-ground world the standalone editor opens on when the host supplies none. @@ -124,13 +124,13 @@ ## @jgengine/editor/EditorApp -- `EditorApp` (function): function EditorApp({ gameId, playable, layers, save }: EditorAppProps): React.JSX.Element — Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. +- `EditorApp` (function): function EditorApp({ gameId, playable, layers, save, modeChip }: EditorAppProps): React.JSX.Element — Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. - `EditorAppProps` (interface): interface EditorAppProps — Props for mounting the scene editor over a playable game. - `EditorSaveFn` (type): type EditorSaveFn = (json: string) => Promise<{ ok: boolean; path?: string; error?: string }> — Persists an exported document JSON; resolves with where it landed or why it failed. ## @jgengine/editor/EditorCameraDriver -- `EditorCameraDriver` (function): function EditorCameraDriver({ api }: { api: EditorHostApi }): null — Smoothly pans the orbit camera to the editor host's focus target when it changes. +- `EditorCameraDriver` (const): const EditorCameraDriver: React.MemoExoticComponent<({ api }: { api: EditorHostApi; }) => null> — Smoothly pans the orbit camera to the editor host's focus target when it changes. ## @jgengine/editor/EditorChrome @@ -138,7 +138,7 @@ ## @jgengine/editor/PerfProbe -- `PerfProbe` (function): function PerfProbe({ api }: { api: EditorHostApi }): null — In-canvas frame counter: publishes fps/draw-call samples to the editor host every 500ms. +- `PerfProbe` (const): const PerfProbe: React.MemoExoticComponent<({ api }: { api: EditorHostApi; }) => null> — In-canvas frame counter: publishes fps/draw-call samples to the editor host every 500ms. ## @jgengine/editor/SchemaInspector @@ -147,8 +147,8 @@ ## @jgengine/editor/SelectionGizmo - `GizmoMode` (type): type GizmoMode = "translate" | "rotate" | "scale" — Which transform gizmo is active for the current selection. -- `SelectionGizmo` (function): function SelectionGizmo({ session, ui, groundSnap, }: { session: EditorSession; ui: EditorUiStore; groundSnap?: (x: number, z: number) => number; }): React.JSX.Element | null — Drag-to-transform gizmo bound to the current selection, dispatching editor commands on release. Translating with a multi-selection moves every selected object by the drag delta; scaling a volume resizes its true shape (radius, height, or box half-extents); a selected path vertex moves just that point. Snapping follows the UI store: terrain height, grid quantization, or free movement. -- `ViewportSelect` (function): function ViewportSelect({ api, ui }: { api: EditorHostApi; ui: EditorUiStore }): null — Canvas click-to-select and click-to-place. Document objects pick by screen proximity (registration always matches what you see) with click-cycling through stacked candidates and shift/ctrl additive selection; everything else picks by occlusion-ordered raycast against the tagged scene graph. When a placement tool is armed, clicks author new markers, volumes, notes, or path points at the ground hit instead of selecting. +- `SelectionGizmo` (const): const SelectionGizmo: React.MemoExoticComponent<({ session, ui, groundSnap, }: { session: EditorSession; ui: EditorUiStore; groundSnap?: ((x: number, z: number) => number) | undefined; }) => React.JSX.Element | null> — Drag-to-transform gizmo bound to the current selection, dispatching editor commands on release. Translating with a multi-selection moves every selected object by the drag delta; scaling a volume resizes its true shape (radius, height, or box half-extents); a selected path vertex moves just that point. Snapping follows the UI store: terrain height, grid quantization, or free movement. +- `ViewportSelect` (const): const ViewportSelect: React.MemoExoticComponent<({ api, ui }: { api: EditorHostApi; ui: EditorUiStore; }) => null> — Canvas click-to-select and click-to-place. Document objects pick by screen proximity (registration always matches what you see) with click-cycling through stacked candidates and shift/ctrl additive selection; everything else picks by occlusion-ordered raycast against the tagged scene graph. When a placement tool is armed, clicks author new markers, volumes, notes, or path points at the ground hit instead of selecting. ## @jgengine/editor/StandaloneEditor @@ -160,6 +160,21 @@ - `createBlankPlayable` (function): function createBlankPlayable(options: BlankPlayableOptions = {}): PlayableGame — Builds a minimal gameless `PlayableGame` — a flat world plus an asset catalog — for the editor to mount over. - `downloadSaver` (function): function downloadSaver(filename = "editor.scene.json"): EditorSaveFn — A save fn that hands the scene JSON back to the browser as a downloaded file — the exit path when no dev server is listening. +## @jgengine/editor/TerrainPanel + +- `TerrainPanel` (function): function TerrainPanel({ session, ui }: { session: EditorSession; ui: EditorUiStore }): React.JSX.Element — The terrain-tool panel: create/clear the heightfield and drive the sculpt/paint controls. + +## @jgengine/editor/chromeFields + +- `NumberField` (function): function NumberField({ label, value, onCommit, step = 1, }: { label: string; value: number; onCommit: (value: number) => void; step?: number; }): React.JSX.Element — ⚠ undocumented +- `SliderRow` (function): function SliderRow({ label, value, min, max, step, onChange, format, }: { label: string; value: number; min: number; max: number; step: number; onChange: (value: number) => void; format?: (value: number) => string; }): React.JSX.Element — ⚠ undocumented + +## @jgengine/editor/chromeStyles + +- `BTN` (const): const BTN: "rounded-md bg-white/[0.04] px-2 py-1 text-neutral-300 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/10 hover:text-neutral-100" — ⚠ undocumented +- `INPUT` (const): const INPUT: "rounded-md border border-white/10 bg-black/40 px-2 py-1 outline-none transition-colors placeholder:text-neutral-600 focus:border-cyan-400/60 focus:bg-black/60" — ⚠ undocumented +- `MICRO` (const): const MICRO: "text-[9px] font-semibold uppercase tracking-[0.14em] text-neutral-500" — ⚠ undocumented + ## @jgengine/editor/mcp/bridgeServer - `EditorBridgeServer` (interface): interface EditorBridgeServer — A running editor bridge server: its bound port, URL, and a stop handle. @@ -171,7 +186,15 @@ ## @jgengine/editor/mcp/loadGameLayers -- `loadGameLayers` (function): function loadGameLayers(gameId: string): Promise — Node-only: resolve a game's `editorLayers` export straight from Games//src. +- `LoadGameLayersResult` (type): type LoadGameLayersResult = | { ok: true; document: EditorDocument } | { ok: false; errors: EditorDocumentDiagnostic[] } — Result of {@link loadGameLayers}: a validated document, or every diagnostic collected while decoding it. +- `decodeGameLayers` (function): function decodeGameLayers(resolved: unknown): LoadGameLayersResult — Validates an already-resolved `editorLayers` export value (post module-load, post factory-call) against the editor document schema — the exact check {@link loadGameLayers} applies at the untrusted-input boundary between game-authored code and a live editor session. +- `loadGameLayers` (function): function loadGameLayers(gameId: string): Promise — Node-only: resolves a game's `editorLayers` export straight from Games//src and validates its shape before it reaches a live editor session — the untrusted-input boundary between game-authored code and the engine. + +## @jgengine/editor/mcp/rpcRequest + +- `DecodeRpcRequestResult` (type): type DecodeRpcRequestResult = | { ok: true; request: EditorBridgeRequest } | { ok: false; errors: RpcRequestDiagnostic[] } — Result of {@link decodeEditorBridgeRequest}: a request whose `method` is a real one, or the diagnostic that rejected it. +- `RpcRequestDiagnostic` (interface): interface RpcRequestDiagnostic — One field-level failure surfaced while decoding an untrusted RPC request. +- `decodeEditorBridgeRequest` (function): function decodeEditorBridgeRequest(raw: unknown): DecodeRpcRequestResult — Validates an untrusted JSON-decoded RPC payload (from `--rpc` or the HTTP bridge) before it reaches `EditorHostApi.handle`: confirms it is a plain object carrying a known `method` name. Per-method field shape is still enforced by `handle`'s own dispatch, but a garbled or unknown-method payload is rejected here with a path-specific diagnostic instead of flowing through on a blind cast. ## @jgengine/editor/mcp/stdioServer diff --git a/.claude/skills/jgengine-gameplay/api.md b/.claude/skills/jgengine-gameplay/api.md index 200203ef9..f00ee3216 100644 --- a/.claude/skills/jgengine-gameplay/api.md +++ b/.claude/skills/jgengine-gameplay/api.md @@ -230,7 +230,7 @@ ## @jgengine/core/game/connectedPlayers -- `ConnectedPlayer` (interface): interface ConnectedPlayer — A player currently joined to a hosted world — the unit a shared-world loop iterates instead of `ctx.player`. +- `ConnectedPlayer` (interface): interface ConnectedPlayer — A player currently joined to a hosted world — the unit a shared-world loop iterates instead of `ctx.player`. Frozen by the registry (see {@link ConnectedPlayers.get}); fields are `readonly` so a caller can't edit its own copy and assume the change stuck. - `ConnectedPlayers` (interface): interface ConnectedPlayers — The set of players connected to one hosted world. A single-player game uses `ctx.player`; a shared-world loop reads `ctx.game.players` so `onTick` can advance every connected hero, not just the one local player. The host (`HostedGameRunner`) drives `join`/`leave`/`setInput`; game code reads `list`/`ids`/`has`/`count`/`input`. - `createConnectedPlayers` (function): function createConnectedPlayers(): ConnectedPlayers — Build an empty {@link ConnectedPlayers} registry — the host joins/leaves players; the game loop reads them. @@ -281,7 +281,7 @@ - `CombatTelegraphEvent` (interface): interface CombatTelegraphEvent — ⚠ undocumented - `CombatVfxEvent` (interface): interface CombatVfxEvent — A transient sprite-particle effect the shell renders once and expires — one burst of `kind`, tinted `color`, anchored at `from` (and `to` for travel/beam effects). - `CosmeticsChangedEvent` (interface): interface CosmeticsChangedEvent — ⚠ undocumented -- `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; via?: { item?: string } } | { kind: "environment"; source: string } | { kind: "self"; source: string } — ⚠ undocumented +- `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; via?: { item?: string } } | { kind: "environment"; source: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event. - `EmotePlayedEvent` (interface): interface EmotePlayedEvent — ⚠ undocumented - `EntityAnimationEvent` (interface): interface EntityAnimationEvent — Request that an entity's rig play a one-shot animation clip bound to `event` in its `animation.oneShots` (e.g. an "attack" swing); the shell resolves the clip and plays it once over the locomotion state. - `EntityDiedEvent` (interface): interface EntityDiedEvent — ⚠ undocumented @@ -928,9 +928,12 @@ ## @jgengine/core/random/rng +- `RandomSeed` (type): type RandomSeed = number & { readonly __randomSeed: unique symbol } — Opaque, serializable PRNG cursor for state machines that must persist their own random stream (a spawn director, a heat/pursuit meter) instead of holding a closure — the state round-trips through save/load and multiplayer sync, so it can't carry a function. Never read or do arithmetic on the raw value directly; thread it through {@link stepRandomSeed} only. - `hashString` (function): function hashString(text: string): number — Deterministic 32-bit FNV-1a hash of a string → unsigned int. Same text, same number, on every platform — the stable seed behind per-id jitter, spread offsets, and content-addressed variation. +- `randomSeedFrom` (function): function randomSeedFrom(seed: number): RandomSeed — Wraps an already-integer seed (e.g. a `config.seed`) as a {@link RandomSeed} — no hashing. - `seededRng` (function): function seededRng(seed: string | number): () => number — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence. - `seededStreams` (function): function seededStreams(seed: string | number): (stream: string) => () => number — Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name. +- `stepRandomSeed` (function): function stepRandomSeed(seed: RandomSeed): readonly [value: number, next: RandomSeed] — One step of the {@link seededRng} recurrence in pure (seed in, seed out) form — the same mulberry32-style generator, shared by every state machine that persists its own PRNG cursor instead of closing over a generator. ## @jgengine/core/random/seedLink diff --git a/.claude/skills/jgengine-gameplay/capabilities.md b/.claude/skills/jgengine-gameplay/capabilities.md index ed211ee8a..4b019de84 100644 --- a/.claude/skills/jgengine-gameplay/capabilities.md +++ b/.claude/skills/jgengine-gameplay/capabilities.md @@ -16,10 +16,6 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createCosmetics` (function) · `import { createCosmetics } from "@jgengine/core/game/cosmetics"` -## currency-format — format a currency amount with its symbol for display - -- `formatCurrencyAmount` (function) · `import { formatCurrencyAmount } from "@jgengine/core/economy/currency"` - ## dialogue-bridge — open/close the talkable→DialogueBox flow with no per-game store or command glue - `createGameDialogue` (function) · `import { createGameDialogue } from "@jgengine/core/game/dialogue"` @@ -40,19 +36,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createSaveStore` (function) · `import { createSaveStore } from "@jgengine/core/game/saveStore"` -## inventory-grid — a bag of stackable items with add, remove, and move - -- `createEmptyInventory` (function) · `import { createEmptyInventory } from "@jgengine/core/inventory/inventoryModel"` - ## item-instance-registry — a runtime store for procedurally generated item instances - `createItemInstanceRegistry` (function) · `import { createItemInstanceRegistry } from "@jgengine/core/item/itemInstanceRegistry"` - `proceduralLootEntry` (function) · `import { proceduralLootEntry } from "@jgengine/core/item/itemInstanceRegistry"` -## lane-board — a lane-based card-battler board with per-lane outcomes - -- `createLaneBoard` (function) · `import { createLaneBoard } from "@jgengine/core/board/laneBoard"` - ## lap-splits — per-lap durations from a cumulative split book - `lapDurations` (function) · `import { lapDurations } from "@jgengine/core/game/race"` @@ -92,10 +80,6 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createLootRegistry` (function) · `import { createLootRegistry } from "@jgengine/core/game/lootTable"` - `lootTable` (function) · `import { lootTable } from "@jgengine/core/game/lootTable"` -## match-rounds — run buy/action/end round phases with per-round economy - -- `createRoundState` (function) · `import { createRoundState } from "@jgengine/core/session/roundState"` - ## modular-item — attach parts into item mount slots to compute combined stats - `slotAccepts` (function) · `import { slotAccepts } from "@jgengine/core/item/modularItem"` @@ -133,22 +117,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createRaceState` (function) · `import { createRaceState } from "@jgengine/core/game/race"` -## role-assign — assign hidden or team roles to players by ratio - -- `assignRoles` (function) · `import { assignRoles } from "@jgengine/core/session/roles"` - ## run-modifiers — a roguelike run built from stacking drafted modifier picks - `createRunDraft` (function) · `import { createRunDraft } from "@jgengine/core/game/runDraft"` -## shared-wallet — shared/group currency pools tracking per-member contributions - -- `createWalletBook` (function) · `import { createWalletBook } from "@jgengine/core/economy/sharedWallet"` - -## shop-trade — buy and sell goods against player currency balances - -- `createTradeSystem` (function) · `import { createTradeSystem } from "@jgengine/core/game/trade"` - ## social-emotes — emotes and social interactions between nearby players - `createSocial` (function) · `import { createSocial } from "@jgengine/core/game/social"` @@ -157,18 +129,6 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createSpawnPoints` (function) · `import { createSpawnPoints } from "@jgengine/core/game/spawnPoints"` -## tech-tree — research nodes with prerequisites that unlock recipes - -- `availableTech` (function) · `import { availableTech } from "@jgengine/core/economy/techTree"` - -## tetris-inventory — a spatial grid inventory holding shaped multi-cell items - -- `createShapedGrid` (function) · `import { createShapedGrid } from "@jgengine/core/inventory/shapedGrid"` - -## timeline-board — a step-sequencer timeline board of timed slots - -- `createTimelineBoard` (function) · `import { createTimelineBoard } from "@jgengine/core/board/timelineBoard"` - ## toast-feed — queue of transient self-expiring on-screen messages (toasts, announcer, kill-feed) - `appendToast` (function) · `import { appendToast } from "@jgengine/core/game/toasts"` @@ -193,7 +153,3 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p ## weighted-pick — pick one item from a set with an injected random source - `pickUniform` (function) · `import { pickUniform } from "@jgengine/core/random/pick"` - -## world-drops — spawn pickup-able items in the world, including death drops - -- `createWorldItemStore` (function) · `import { createWorldItemStore } from "@jgengine/core/game/worldItem"` diff --git a/.claude/skills/jgengine-multiplayer/api.md b/.claude/skills/jgengine-multiplayer/api.md index 4b407c638..6dcd3b592 100644 --- a/.claude/skills/jgengine-multiplayer/api.md +++ b/.claude/skills/jgengine-multiplayer/api.md @@ -203,28 +203,37 @@ ## @jgengine/node -- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; }) => Promise; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise; joinByCode: (args: { userId: strin… — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence. +- `DEFAULT_HEARTBEAT_INTERVAL_MS` (const): const DEFAULT_HEARTBEAT_INTERVAL_MS: 30000 — Default ping/pong interval; a socket that misses one round-trip is terminated. +- `DEFAULT_MAX_CONNECTIONS` (const): const DEFAULT_MAX_CONNECTIONS: 10000 — Default max concurrent sockets this server accepts before rejecting new ones. +- `DEFAULT_MAX_PAYLOAD_BYTES` (const): const DEFAULT_MAX_PAYLOAD_BYTES: 1048576 — Default per-message payload cap (bytes) — `ws` closes the socket with 1009 past this. +- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; }) => Promise; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise; joinByCode: (args: … — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence. - `GameHostOptions` (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; } — Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes. - `GameSocketIoServer` (type): type GameSocketIoServer = { rewind: (args: { serverId: string; atMs: number }) => RewoundPosition[]; close: () => void; } — ⚠ undocumented - `GameSocketIoServerOptions` (type): type GameSocketIoServerOptions = HostRouterOptions & { io: SocketIoLikeServer } — ⚠ undocumented - `GameWsServer` (type): type GameWsServer = { wss: WebSocketServer; port: () => number; rewind: (args: { serverId: string; atMs: number }) => RewoundPosition[]; close: () => Promise; } — ⚠ undocumented -- `GameWsServerOptions` (type): type GameWsServerOptions = HostRouterOptions & { server?: HttpServer; port?: number; path?: string; } — ⚠ undocumented +- `GameWsServerOptions` (type): type GameWsServerOptions = HostRouterOptions & { server?: HttpServer; port?: number; path?: string; /** Per-message payload cap in bytes. Defaults to {@link DEFAULT_MAX_PAYLOAD_BYTES}. */ maxPayloadBytes?: number; /** Max concurrent sockets accepted; connections beyond this are closed immediately. D… — ⚠ undocumented - `HostChangeEvent` (type): type HostChangeEvent = { type: "server"; serverId: string; } | { type: "player"; serverId: string; userId: string; } | { type: "feed"; serverId: string; action: string; } — A change notification emitted by a `GameHost` for a server, player, or feed mutation. - `HostedGameDefinition` (interface): interface HostedGameDefinition — A game the world server can host — its authoritative {@link GameDefinition} and the content lookup a `GameContext` reads. +- `InstallShutdownHookOptions` (interface): interface InstallShutdownHookOptions — Config for {@link installShutdownHook}. - `NodeHandler` (type): type NodeHandler = (req: IncomingMessage, res: ServerResponse) => void — ⚠ undocumented - `RewoundPosition` (type): type RewoundPosition = { userId: string; x: number; y: number; z: number; } — A player's interpolated position sampled from history at a past timestamp. +- `ShutdownHook` (interface): interface ShutdownHook — A live signal listener installed by {@link installShutdownHook}; call `remove()` to uninstall it (tests, embedders opting out). - `SocketIoLikeServer` (type): type SocketIoLikeServer = { on: (event: "connection", listener: (socket: SocketIoLikeServerSocket) => void) => unknown; } — ⚠ undocumented - `SocketIoLikeServerSocket` (type): type SocketIoLikeServerSocket = { on: (event: string, listener: (payload: string) => void) => unknown; send: (data: string) => unknown; disconnect: (close?: boolean) => unknown; } — ⚠ undocumented - `WebHandler` (type): type WebHandler = (request: Request) => Promise — ⚠ undocumented - `WorldGameServer` (interface): interface WorldGameServer — A runnable ws host for GameContext worlds: {@link createWorldGameHost} + {@link createGameWsServer} + a tick loop, with a manual `tick(dt)` seam so a fake clock can drive it in tests. - `WorldGameServerOptions` (interface): interface WorldGameServerOptions extends Omit — Config for {@link createWorldGameServer}: how to resolve a game by id, the tick cadence, and the underlying ws-server/router options (minus `host`, which the server builds). +- `WorldPersistence` (interface): interface WorldPersistence — The persistence plug-point for {@link createWorldGameServer}: resolves one {@link HostedWorldStore} per hosted world, called once when the world host session is created. Structural — a SQL, file, or Convex-backed store all conform without `node` importing a concrete driver; only a `store()` factory is required. Mirrors `HostPersistence` (the reducer host's persistence seam). +- `WorldPersistenceKey` (interface): interface WorldPersistenceKey — Per-world key a {@link WorldPersistence} resolves a {@link HostedWorldStore} for. - `attachGameSocketIoServer` (function): function attachGameSocketIoServer(options: GameSocketIoServerOptions): GameSocketIoServer — ⚠ undocumented - `clearFilePersistence` (function): function clearFilePersistence(dir: string): Promise — ⚠ undocumented - `createGameHost` (function): function createGameHost(options: GameHostOptions): GameHost — Creates a `GameHost` that runs game servers over the given persistence and runtimes. - `createGameWsServer` (function): function createGameWsServer(options: GameWsServerOptions): GameWsServer — ⚠ undocumented - `createWorldGameServer` (function): function createWorldGameServer(options: WorldGameServerOptions): WorldGameServer — Build a {@link WorldGameServer} — one process hosting authoritative GameContext worlds over ws, ready for two-client play once {@link WorldGameServer.start} runs. - `filePersistence` (function): function filePersistence(dir: string, now: () => number = Date.now): HostPersistence — ⚠ undocumented +- `installShutdownHook` (function): function installShutdownHook(shutdown: () => Promise | void, options: InstallShutdownHookOptions = {}): ShutdownHook — Wires `SIGINT`/`SIGTERM` (or a custom signal list) to a clean-shutdown callback — e.g. `() => worldServer.close()` or `() => Promise.all([wsServer.close(), host.stop()])`. Bounded by `timeoutMs` so a stuck flush can't hang the process forever; idempotent — a second signal delivered mid-shutdown reuses the same in-flight run instead of flushing twice. Returns a {@link ShutdownHook} whose `remove()` uninstalls the listeners, for tests and embedders that want their own handling. - `memoryPersistence` (function): function memoryPersistence(now?: () => number): HostPersistence — Creates an in-memory `HostPersistence` implementation, useful for tests and ephemeral hosts. +- `memoryWorldPersistence` (function): function memoryWorldPersistence(): WorldPersistence — Default {@link WorldPersistence}: an isolated in-memory {@link HostedWorldStore} per `gameId`/`serverId`, lost on process exit. - `toNodeHandler` (function): function toNodeHandler(handler: WebHandler): NodeHandler — ⚠ undocumented - `toWebRequest` (function): function toWebRequest(req: IncomingMessage): Promise — ⚠ undocumented @@ -244,7 +253,7 @@ ## @jgengine/node/host -- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; }) => Promise; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise; joinByCode: (args: { userId: strin… — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence. +- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; }) => Promise; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise; joinByCode: (args: … — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence. - `GameHostOptions` (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; } — Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes. - `HostChangeEvent` (type): type HostChangeEvent = { type: "server"; serverId: string; } | { type: "player"; serverId: string; userId: string; } | { type: "feed"; serverId: string; action: string; } — A change notification emitted by a `GameHost` for a server, player, or feed mutation. - `createGameHost` (function): function createGameHost(options: GameHostOptions): GameHost — Creates a `GameHost` that runs game servers over the given persistence and runtimes. @@ -252,9 +261,18 @@ ## @jgengine/node/persistence +- `WorldPersistence` (interface): interface WorldPersistence — The persistence plug-point for {@link createWorldGameServer}: resolves one {@link HostedWorldStore} per hosted world, called once when the world host session is created. Structural — a SQL, file, or Convex-backed store all conform without `node` importing a concrete driver; only a `store()` factory is required. Mirrors `HostPersistence` (the reducer host's persistence seam). +- `WorldPersistenceKey` (interface): interface WorldPersistenceKey — Per-world key a {@link WorldPersistence} resolves a {@link HostedWorldStore} for. - `clearFilePersistence` (function): function clearFilePersistence(dir: string): Promise — ⚠ undocumented - `filePersistence` (function): function filePersistence(dir: string, now: () => number = Date.now): HostPersistence — ⚠ undocumented - `memoryPersistence` (function): function memoryPersistence(now?: () => number): HostPersistence — Creates an in-memory `HostPersistence` implementation, useful for tests and ephemeral hosts. +- `memoryWorldPersistence` (function): function memoryWorldPersistence(): WorldPersistence — Default {@link WorldPersistence}: an isolated in-memory {@link HostedWorldStore} per `gameId`/`serverId`, lost on process exit. + +## @jgengine/node/shutdown + +- `InstallShutdownHookOptions` (interface): interface InstallShutdownHookOptions — Config for {@link installShutdownHook}. +- `ShutdownHook` (interface): interface ShutdownHook — A live signal listener installed by {@link installShutdownHook}; call `remove()` to uninstall it (tests, embedders opting out). +- `installShutdownHook` (function): function installShutdownHook(shutdown: () => Promise | void, options: InstallShutdownHookOptions = {}): ShutdownHook — Wires `SIGINT`/`SIGTERM` (or a custom signal list) to a clean-shutdown callback — e.g. `() => worldServer.close()` or `() => Promise.all([wsServer.close(), host.stop()])`. Bounded by `timeoutMs` so a stuck flush can't hang the process forever; idempotent — a second signal delivered mid-shutdown reuses the same in-flight run instead of flushing twice. Returns a {@link ShutdownHook} whose `remove()` uninstalls the listeners, for tests and embedders that want their own handling. ## @jgengine/node/socketIoServer @@ -286,8 +304,11 @@ ## @jgengine/node/wsServer +- `DEFAULT_HEARTBEAT_INTERVAL_MS` (const): const DEFAULT_HEARTBEAT_INTERVAL_MS: 30000 — Default ping/pong interval; a socket that misses one round-trip is terminated. +- `DEFAULT_MAX_CONNECTIONS` (const): const DEFAULT_MAX_CONNECTIONS: 10000 — Default max concurrent sockets this server accepts before rejecting new ones. +- `DEFAULT_MAX_PAYLOAD_BYTES` (const): const DEFAULT_MAX_PAYLOAD_BYTES: 1048576 — Default per-message payload cap (bytes) — `ws` closes the socket with 1009 past this. - `GameWsServer` (type): type GameWsServer = { wss: WebSocketServer; port: () => number; rewind: (args: { serverId: string; atMs: number }) => RewoundPosition[]; close: () => Promise; } — ⚠ undocumented -- `GameWsServerOptions` (type): type GameWsServerOptions = HostRouterOptions & { server?: HttpServer; port?: number; path?: string; } — ⚠ undocumented +- `GameWsServerOptions` (type): type GameWsServerOptions = HostRouterOptions & { server?: HttpServer; port?: number; path?: string; /** Per-message payload cap in bytes. Defaults to {@link DEFAULT_MAX_PAYLOAD_BYTES}. */ maxPayloadBytes?: number; /** Max concurrent sockets accepted; connections beyond this are closed immediately. D… — ⚠ undocumented - `RewoundPosition` (type): type RewoundPosition = { userId: string; x: number; y: number; z: number; } — A player's interpolated position sampled from history at a past timestamp. - `createGameWsServer` (function): function createGameWsServer(options: GameWsServerOptions): GameWsServer — ⚠ undocumented @@ -311,17 +332,36 @@ ## @jgengine/ws +- `CommandAuthorize` (type): type CommandAuthorize = (args: { userId: string; op: HostCommandOp; serverId?: string; command?: string; }) => boolean | Promise — Per-command authorization hook: return `false` to reject. Defaults to allow-all when omitted. +- `CommandCatalog` (type): type CommandCatalog = Record — Declared `runCommand` names and their input validators. When set, any `runCommand` name absent from this catalog is rejected as unknown. +- `CommandCatalogEntry` (type): type CommandCatalogEntry = { validate?: (input: unknown) => CommandRejection | null; } — A declared `runCommand` name's input validator, run before the command reaches the game host. +- `CommandGateArgs` (type): type CommandGateArgs = { connection: object; userId: string; op: HostCommandOp; atMs: number; serverId?: string; command?: string; input?: unknown; } — One op attempt to run through the middleware pipeline: which connection, which op, and (for `runCommand`) the command name/input. +- `CommandGateDecision` (type): type CommandGateDecision = { allow: true } | { allow: false; reason: string } — The pipeline's verdict for one {@link CommandGateArgs}: allowed, or rejected with a client-facing reason. +- `CommandLimits` (type): type CommandLimits = Partial> — Per-op rate limits. An op with no entry (or an undefined `limits`) is unlimited. +- `CommandMiddleware` (type): type CommandMiddleware = { check: (args: CommandGateArgs) => Promise; } — A composable rate-limit → validate → authorize pipeline the host router runs before dispatching pose/runCommand/join/browse/voice ops. Every stage defaults to a no-op, so an unconfigured router behaves exactly as before. +- `CommandMiddlewareOptions` (type): type CommandMiddlewareOptions = { limits?: CommandLimits; authorize?: CommandAuthorize; validate?: CommandCatalog; } — Config for {@link createCommandMiddleware}: the same `limits`/`authorize`/`validate` fields accepted by `HostRouterOptions`. +- `CommandRateLimit` (type): type CommandRateLimit = { count: number; perMs: number } — A sliding-window rate limit: at most `count` calls per `perMs` window. +- `CommandRateLimiter` (type): type CommandRateLimiter = { allow: (connection: object, op: HostCommandOp, atMs: number) => boolean; } — A composable per-connection/per-op sliding-window rate limiter. +- `DEFAULT_COMMAND_LIMITS` (const): const DEFAULT_COMMAND_LIMITS: CommandLimits — Recommended per-op limits a host can opt into via `limits: DEFAULT_COMMAND_LIMITS`. Rate limiting is off unless `limits` is set. - `DEFAULT_POSE_RULES` (const): const DEFAULT_POSE_RULES: PoseSyncRules — ⚠ undocumented -- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; }) => Promise; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise; joinByCode: (args: { userId: strin… — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence. +- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; }) => Promise; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise; joinByCode: (args: … — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence. - `GameHostOptions` (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; } — Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes. - `HostChangeEvent` (type): type HostChangeEvent = | { type: "server"; serverId: string } | { type: "player"; serverId: string; userId: string } | { type: "feed"; serverId: string; action: string } — A change notification emitted by a `GameHost` for a server, player, or feed mutation. +- `HostCommandOp` (type): type HostCommandOp = "pose" | "runCommand" | "join" | "browse" | "voice" — A host-side op the command middleware pipeline can gate: pose sync, `runCommand`, join/joinByCode, browse, or voice join/leave/publish. - `HostRouter` (type): type HostRouter = { connect: (transport: HostRouterTransport) => HostRouterConnection; rewind: (args: { serverId: string; atMs: number }) => RewoundPosition[]; close: () => void; } — ⚠ undocumented - `HostRouterAuthenticate` (type): type HostRouterAuthenticate = (args: { userId: string; token?: string; }) => Promise | string | null — ⚠ undocumented - `HostRouterConnection` (type): type HostRouterConnection = { handleRaw: (raw: unknown) => void; close: () => void; } — ⚠ undocumented -- `HostRouterOptions` (type): type HostRouterOptions = { host: GameHost; authenticate?: HostRouterAuthenticate; allowAnonymous?: boolean; singleSession?: boolean; poseRules?: PoseSyncRules; positionHistoryMs?: number; chatRateLimit?: ChatRateLimit; chatHistoryLimit?: number; chatMaxBodyLength?: number; now?: () => number; } — ⚠ undocumented +- `HostRouterOptions` (type): type HostRouterOptions = { host: GameHost; authenticate?: HostRouterAuthenticate; allowAnonymous?: boolean; singleSession?: boolean; poseRules?: PoseSyncRules; positionHistoryMs?: number; chatRateLimit?: ChatRateLimit; chatHistoryLimit?: number; chatMaxBodyLength?: number; /** Per-op rate limits for… — ⚠ undocumented - `HostRouterTransport` (type): type HostRouterTransport = { send: (data: string) => void; close: () => void; } — ⚠ undocumented - `HttpReads` (type): type HttpReads = { getTop: (args: { stat: string; scope: LeaderboardScope; serverId?: string; limit?: number; }) => Promise; getLeaderboardProfile: (userId: string) => Promise>; getPlayerProfile: (userId: string) => Promise; li… — ⚠ undocumented - `HttpReadsOptions` (type): type HttpReadsOptions = { baseUrl: string; gameId: string; fetchImpl?: typeof fetch; } — ⚠ undocumented +- `MAX_APPEARANCE_ENTRIES` (const): const MAX_APPEARANCE_ENTRIES: 32 — Max number of keys in a pose `appearance` tag map. +- `MAX_APPEARANCE_VALUE_LENGTH` (const): const MAX_APPEARANCE_VALUE_LENGTH: 256 — Max length of a single `appearance` tag string value, in UTF-16 code units. +- `MAX_COMMAND_LENGTH` (const): const MAX_COMMAND_LENGTH: 4096 — Max length of a `runCommand` command name, in UTF-16 code units. +- `MAX_FEED_ACTION_LENGTH` (const): const MAX_FEED_ACTION_LENGTH: 256 — Max length of a `pushFeed` action name, in UTF-16 code units. +- `MAX_FEED_ENTRY_BYTES` (const): const MAX_FEED_ENTRY_BYTES: 65536 — Max serialized size of a `pushFeed` entry payload, in bytes. +- `MAX_QUEUED_MESSAGES` (const): const MAX_QUEUED_MESSAGES: 64 — Cap on frames queued behind a connection's in-flight message; beyond this a flood gets rejected instead of piling up unbounded promises. +- `OP_LEDGER_LIMIT` (const): const OP_LEDGER_LIMIT: 64 — Max recently-applied `runCommand` op IDs retained per (serverId, userId), oldest evicted first. - `PeerGuest` (type): type PeerGuest = { backend: WsBackend; offer: () => Promise; connect: (answerCode: string) => Promise; close: () => void; } — ⚠ undocumented - `PeerGuestOptions` (type): type PeerGuestOptions = { userId: string; token?: string; rtc?: PeerRtcOptions; } — ⚠ undocumented - `PeerHost` (type): type PeerHost = { backend: WsBackend; host: GameHost; router: HostRouter; accept: (offerCode: string) => Promise; close: () => void; } — ⚠ undocumented @@ -355,7 +395,7 @@ - `WsChannel` (type): type WsChannel = "server" | "player" | "feed" | "presence" | "chat" | "voice" — ⚠ undocumented - `WsChatMessage` (type): type WsChatMessage = { id: string; channelId: string; fromUserId: string; body: string; at: number; } — ⚠ undocumented - `WsChatSync` (type): type WsChatSync = { subscribe: ( serverId: string, channelId: string, onChange: (messages: WsChatMessage[]) => void, ) => () => void; send: (serverId: string, channelId: string, body: string) => Promise; } — ⚠ undocumented -- `WsClientMessage` (type): type WsClientMessage = | { v: 1; t: "hello"; id: number; userId: string; token?: string } | { v: 1; t: "join"; id: number; gameId: string; serverId?: string; attributes?: SessionAttributes } | { v: 1; t: "joinByCode"; id: number; gameId: string; code: string } | { v: 1; t: "browse"; id: number; game… — ⚠ undocumented +- `WsClientMessage` (type): type WsClientMessage = | { v: 1; t: "hello"; id: number; userId: string; token?: string } | { v: 1; t: "join"; id: number; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; } | { v: 1; t: "joinByCode"; id: number; gameId: string; code: string } | { v: 1; t: "browse"; … — ⚠ undocumented - `WsDecodeFailure` (type): type WsDecodeFailure = { reason: string; id?: number; } — ⚠ undocumented - `WsJoinByCodeResult` (type): type WsJoinByCodeResult = JoinServerResult | null — ⚠ undocumented - `WsJoinResult` (type): type WsJoinResult = JoinServerResult — ⚠ undocumented @@ -370,6 +410,8 @@ - `announcePeerHost` (function): function announcePeerHost(host: PeerHost, signaling: PeerSignaling): () => void — ⚠ undocumented - `broadcastChannelSignaling` (function): function broadcastChannelSignaling(room: string): PeerSignaling — ⚠ undocumented - `computeVoiceGain` (function): function computeVoiceGain(def: VoiceChannelDef, distance: number | null): number — ⚠ undocumented +- `createCommandMiddleware` (function): function createCommandMiddleware(options: CommandMiddlewareOptions): CommandMiddleware — Builds the composed command middleware pipeline from game-intent config: `limits`, `validate`, `authorize`. +- `createCommandRateLimiter` (function): function createCommandRateLimiter(limits: CommandLimits): CommandRateLimiter — Creates a sliding-window rate limiter keyed by connection identity and op; ops absent from `limits` are always allowed. - `createGameHost` (function): function createGameHost(options: GameHostOptions): GameHost — Creates a `GameHost` that runs game servers over the given persistence and runtimes. - `createHostRouter` (function): function createHostRouter(options: HostRouterOptions): HostRouter — ⚠ undocumented - `createHttpReads` (function): function createHttpReads(options: HttpReadsOptions): HttpReads — ⚠ undocumented @@ -386,8 +428,27 @@ - `loopbackPipe` (function): function loopbackPipe(router: HostRouter): TransportPipeFactory — ⚠ undocumented - `memoryPersistence` (function): function memoryPersistence(now: () => number = Date.now): HostPersistence — Creates an in-memory `HostPersistence` implementation, useful for tests and ephemeral hosts. - `socketIoPipe` (function): function socketIoPipe(socket: SocketIoLikeSocket): TransportPipeFactory — ⚠ undocumented +- `validateCommandInput` (function): function validateCommandInput(catalog: CommandCatalog | undefined, command: string, input: unknown): CommandRejection | null — Validates a `runCommand` input against a declared catalog. `undefined` catalog means "no declarations" — everything passes through unchanged. - `webSocketPipe` (function): function webSocketPipe(url: string, webSocketFactory: (url: string) => WebSocket = (target) => new WebSocket(target)): TransportPipeFactory — ⚠ undocumented +## @jgengine/ws/commandMiddleware + +- `CommandAuthorize` (type): type CommandAuthorize = (args: { userId: string; op: HostCommandOp; serverId?: string; command?: string; }) => boolean | Promise — Per-command authorization hook: return `false` to reject. Defaults to allow-all when omitted. +- `CommandCatalog` (type): type CommandCatalog = Record — Declared `runCommand` names and their input validators. When set, any `runCommand` name absent from this catalog is rejected as unknown. +- `CommandCatalogEntry` (type): type CommandCatalogEntry = { validate?: (input: unknown) => CommandRejection | null; } — A declared `runCommand` name's input validator, run before the command reaches the game host. +- `CommandGateArgs` (type): type CommandGateArgs = { connection: object; userId: string; op: HostCommandOp; atMs: number; serverId?: string; command?: string; input?: unknown; } — One op attempt to run through the middleware pipeline: which connection, which op, and (for `runCommand`) the command name/input. +- `CommandGateDecision` (type): type CommandGateDecision = { allow: true } | { allow: false; reason: string } — The pipeline's verdict for one {@link CommandGateArgs}: allowed, or rejected with a client-facing reason. +- `CommandLimits` (type): type CommandLimits = Partial> — Per-op rate limits. An op with no entry (or an undefined `limits`) is unlimited. +- `CommandMiddleware` (type): type CommandMiddleware = { check: (args: CommandGateArgs) => Promise; } — A composable rate-limit → validate → authorize pipeline the host router runs before dispatching pose/runCommand/join/browse/voice ops. Every stage defaults to a no-op, so an unconfigured router behaves exactly as before. +- `CommandMiddlewareOptions` (type): type CommandMiddlewareOptions = { limits?: CommandLimits; authorize?: CommandAuthorize; validate?: CommandCatalog; } — Config for {@link createCommandMiddleware}: the same `limits`/`authorize`/`validate` fields accepted by `HostRouterOptions`. +- `CommandRateLimit` (type): type CommandRateLimit = { count: number; perMs: number } — A sliding-window rate limit: at most `count` calls per `perMs` window. +- `CommandRateLimiter` (type): type CommandRateLimiter = { allow: (connection: object, op: HostCommandOp, atMs: number) => boolean; } — A composable per-connection/per-op sliding-window rate limiter. +- `DEFAULT_COMMAND_LIMITS` (const): const DEFAULT_COMMAND_LIMITS: CommandLimits — Recommended per-op limits a host can opt into via `limits: DEFAULT_COMMAND_LIMITS`. Rate limiting is off unless `limits` is set. +- `HostCommandOp` (type): type HostCommandOp = "pose" | "runCommand" | "join" | "browse" | "voice" — A host-side op the command middleware pipeline can gate: pose sync, `runCommand`, join/joinByCode, browse, or voice join/leave/publish. +- `createCommandMiddleware` (function): function createCommandMiddleware(options: CommandMiddlewareOptions): CommandMiddleware — Builds the composed command middleware pipeline from game-intent config: `limits`, `validate`, `authorize`. +- `createCommandRateLimiter` (function): function createCommandRateLimiter(limits: CommandLimits): CommandRateLimiter — Creates a sliding-window rate limiter keyed by connection identity and op; ops absent from `limits` are always allowed. +- `validateCommandInput` (function): function validateCommandInput(catalog: CommandCatalog | undefined, command: string, input: unknown): CommandRejection | null — Validates a `runCommand` input against a declared catalog. `undefined` catalog means "no declarations" — everything passes through unchanged. + ## @jgengine/ws/createWsBackend - `WsBackend` (type): type WsBackend = GameBackend & { pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise; browse: (args: { gameId: string; filter?: MatchFilter; limit?: number }) => Promise; joinByCode: (args: { gameId: string; code: string }) => Promise Promise; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise; joinByCode: (args: { userId: strin… — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence. +- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; }) => Promise; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise; joinByCode: (args: … — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence. - `GameHostOptions` (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; } — Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes. - `HostChangeEvent` (type): type HostChangeEvent = | { type: "server"; serverId: string } | { type: "player"; serverId: string; userId: string } | { type: "feed"; serverId: string; action: string } — A change notification emitted by a `GameHost` for a server, player, or feed mutation. +- `OP_LEDGER_LIMIT` (const): const OP_LEDGER_LIMIT: 64 — Max recently-applied `runCommand` op IDs retained per (serverId, userId), oldest evicted first. - `createGameHost` (function): function createGameHost(options: GameHostOptions): GameHost — Creates a `GameHost` that runs game servers over the given persistence and runtimes. - `memoryPersistence` (function): function memoryPersistence(now: () => number = Date.now): HostPersistence — Creates an in-memory `HostPersistence` implementation, useful for tests and ephemeral hosts. @@ -411,8 +473,9 @@ - `HostRouter` (type): type HostRouter = { connect: (transport: HostRouterTransport) => HostRouterConnection; rewind: (args: { serverId: string; atMs: number }) => RewoundPosition[]; close: () => void; } — ⚠ undocumented - `HostRouterAuthenticate` (type): type HostRouterAuthenticate = (args: { userId: string; token?: string; }) => Promise | string | null — ⚠ undocumented - `HostRouterConnection` (type): type HostRouterConnection = { handleRaw: (raw: unknown) => void; close: () => void; } — ⚠ undocumented -- `HostRouterOptions` (type): type HostRouterOptions = { host: GameHost; authenticate?: HostRouterAuthenticate; allowAnonymous?: boolean; singleSession?: boolean; poseRules?: PoseSyncRules; positionHistoryMs?: number; chatRateLimit?: ChatRateLimit; chatHistoryLimit?: number; chatMaxBodyLength?: number; now?: () => number; } — ⚠ undocumented +- `HostRouterOptions` (type): type HostRouterOptions = { host: GameHost; authenticate?: HostRouterAuthenticate; allowAnonymous?: boolean; singleSession?: boolean; poseRules?: PoseSyncRules; positionHistoryMs?: number; chatRateLimit?: ChatRateLimit; chatHistoryLimit?: number; chatMaxBodyLength?: number; /** Per-op rate limits for… — ⚠ undocumented - `HostRouterTransport` (type): type HostRouterTransport = { send: (data: string) => void; close: () => void; } — ⚠ undocumented +- `MAX_QUEUED_MESSAGES` (const): const MAX_QUEUED_MESSAGES: 64 — Cap on frames queued behind a connection's in-flight message; beyond this a flood gets rejected instead of piling up unbounded promises. - `RewoundPosition` (type): type RewoundPosition = { userId: string; x: number; y: number; z: number; } — A player's interpolated position sampled from history at a past timestamp. - `createHostRouter` (function): function createHostRouter(options: HostRouterOptions): HostRouter — ⚠ undocumented - `loopbackPipe` (function): function loopbackPipe(router: HostRouter): TransportPipeFactory — ⚠ undocumented @@ -449,12 +512,17 @@ ## @jgengine/ws/protocol +- `MAX_APPEARANCE_ENTRIES` (const): const MAX_APPEARANCE_ENTRIES: 32 — Max number of keys in a pose `appearance` tag map. +- `MAX_APPEARANCE_VALUE_LENGTH` (const): const MAX_APPEARANCE_VALUE_LENGTH: 256 — Max length of a single `appearance` tag string value, in UTF-16 code units. +- `MAX_COMMAND_LENGTH` (const): const MAX_COMMAND_LENGTH: 4096 — Max length of a `runCommand` command name, in UTF-16 code units. +- `MAX_FEED_ACTION_LENGTH` (const): const MAX_FEED_ACTION_LENGTH: 256 — Max length of a `pushFeed` action name, in UTF-16 code units. +- `MAX_FEED_ENTRY_BYTES` (const): const MAX_FEED_ENTRY_BYTES: 65536 — Max serialized size of a `pushFeed` entry payload, in bytes. - `WS_PROTOCOL_VERSION` (const): const WS_PROTOCOL_VERSION: 1 — ⚠ undocumented - `WsAppearance` (type): type WsAppearance = Record — Client-set cosmetic/state tags carried alongside a pose (skin, mount, emote, ...). Primitive values only. - `WsBrowseResult` (type): type WsBrowseResult = SessionListing[] — ⚠ undocumented - `WsChannel` (type): type WsChannel = "server" | "player" | "feed" | "presence" | "chat" | "voice" — ⚠ undocumented - `WsChatMessage` (type): type WsChatMessage = { id: string; channelId: string; fromUserId: string; body: string; at: number; } — ⚠ undocumented -- `WsClientMessage` (type): type WsClientMessage = | { v: 1; t: "hello"; id: number; userId: string; token?: string } | { v: 1; t: "join"; id: number; gameId: string; serverId?: string; attributes?: SessionAttributes } | { v: 1; t: "joinByCode"; id: number; gameId: string; code: string } | { v: 1; t: "browse"; id: number; game… — ⚠ undocumented +- `WsClientMessage` (type): type WsClientMessage = | { v: 1; t: "hello"; id: number; userId: string; token?: string } | { v: 1; t: "join"; id: number; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; } | { v: 1; t: "joinByCode"; id: number; gameId: string; code: string } | { v: 1; t: "browse"; … — ⚠ undocumented - `WsDecodeFailure` (type): type WsDecodeFailure = { reason: string; id?: number; } — ⚠ undocumented - `WsJoinByCodeResult` (type): type WsJoinByCodeResult = JoinServerResult | null — ⚠ undocumented - `WsJoinResult` (type): type WsJoinResult = JoinServerResult — ⚠ undocumented diff --git a/.claude/skills/jgengine-ui/api.md b/.claude/skills/jgengine-ui/api.md index 63747a1ba..72c8e390b 100644 --- a/.claude/skills/jgengine-ui/api.md +++ b/.claude/skills/jgengine-ui/api.md @@ -643,6 +643,10 @@ - `GameHost` (function): function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer }: GameHostProps): React.JSX.Element — ⚠ undocumented - `GameHostProps` (interface): interface GameHostProps — ⚠ undocumented +## @jgengine/shell/GamePhaseStamp + +- `GamePhaseStamp` (function): function GamePhaseStamp(): null — ⚠ undocumented + ## @jgengine/shell/GamePlayer - `GamePlayer` (function): function GamePlayer({ gameId, registry, fallbackGameId, loading = null, multiplayer = null }: GamePlayerProps): React.JSX.Element — ⚠ undocumented @@ -651,9 +655,6 @@ ## @jgengine/shell/GamePlayerShell - `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,… — ⚠ undocumented -- `applyMotionImpulses` (function): function applyMotionImpulses(currentVelocity: number, batch: MotionIntentBatch | null): number — Fold a batch's vertical impulses into a controller's velocity, then apply an outright `setVerticalVelocity` override — the vertical counterpart of {@link applyHorizontalImpulses}. -- `hasEnvironmentTerrain` (function): function hasEnvironmentTerrain(world: WorldFeature | undefined): boolean — Whether a world declares real terrain (base heightfield or islands) rather than a flat plane — gates terrain-floor sampling in the movement controllers. -- `nearbyObstacles` (function): function nearbyObstacles(objects: readonly { position: readonly [number, number, number]; }[], center: readonly [number, number, number], radius?: number): CollisionObstacle[] — Placed objects within `radius` (XZ) of `center`, as {@link CollisionObstacle}s to pre-filter for {@link resolveObstacleStep}. - `resolvePhysicsTuning` (function): function resolvePhysicsTuning(physics: PhysicsConfig | undefined): MovementTuningOverrides | undefined — Maps a game's declared `physics` onto the movement controllers' tuning. `PhysicsConfig.gravity` is a signed world acceleration (negative points down), but the controllers integrate `velocityY -= gravityAcceleration * dt` and expect a positive downward magnitude — so gravity is negated here to keep down-pointing gravity pulling down. ## @jgengine/shell/GameUiPreview @@ -835,10 +836,34 @@ - `GameConfig` (type): type GameConfig = EngineFields & PresentationFields — ⚠ undocumented - `defineGame` (function): function defineGame(config: GameConfig): PlayableGame — ⚠ undocumented +## @jgengine/shell/devtools/ColPanel + +- `ColPanel` (function): function ColPanel(): React.JSX.Element — ⚠ undocumented + ## @jgengine/shell/devtools/CollisionDebugWorld - `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. +## @jgengine/shell/devtools/KeysPanel + +- `KeysPanel` (function): function KeysPanel({ input }: { input: ActionCodesMap | undefined }): React.JSX.Element — ⚠ undocumented + +## @jgengine/shell/devtools/LogsPanel + +- `LogsPanel` (function): function LogsPanel(): React.JSX.Element — ⚠ undocumented + +## @jgengine/shell/devtools/NetPanel + +- `NetPanel` (function): function NetPanel({ multiplayer }: { multiplayer: ShellMultiplayer | null }): React.JSX.Element — ⚠ undocumented + +## @jgengine/shell/devtools/PerfPanel + +- `PerfPanel` (function): function PerfPanel({ ctx }: { ctx: GameContext }): React.JSX.Element — ⚠ undocumented + +## @jgengine/shell/devtools/TunePanel + +- `TunePanel` (function): function TunePanel({ gameName }: { gameName: string }): React.JSX.Element — ⚠ undocumented + ## @jgengine/shell/devtools/agentBridge - `AgentBridgeRequest` (type): type AgentBridgeRequest = { method: string } & Record — One RPC call into the agent bridge: a verb name plus its verb-specific fields. @@ -871,6 +896,36 @@ - `HITBOX_WIRE_COLOR` (const): const HITBOX_WIRE_COLOR: "#f472b6" — ⚠ undocumented - `PROJECTILE_PATH_COLOR` (const): const PROJECTILE_PATH_COLOR: "#fde68a" — ⚠ undocumented +## @jgengine/shell/devtools/devtoolsOverrides + +- `readStoredOverrides` (function): function readStoredOverrides(gameName: string): DevtoolsOverrides | null — ⚠ undocumented + +## @jgengine/shell/devtools/panelAtoms + +- `SectionLabel` (function): function SectionLabel({ children }: { children: string }): React.JSX.Element — ⚠ undocumented +- `StatRow` (function): function StatRow({ name, value, alert }: { name: string; value: string; alert?: boolean }): React.JSX.Element — ⚠ undocumented +- `ms` (function): function ms(value: number): string — ⚠ undocumented + +## @jgengine/shell/devtools/perfDiagnose + +- `diagnose` (function): function diagnose(frame: NonNullable>, longs: readonly LongFrameEvent[]): string | null — ⚠ undocumented + +## @jgengine/shell/diagnostics/RuntimeDiagnostics + +- `DiagnosticOverlay` (function): function DiagnosticOverlay({ diagnostics, gameName }: { diagnostics: RuntimeDiagnostic[]; gameName: string }): React.JSX.Element | null — ⚠ undocumented +- `GameUiErrorBoundary` (class): class GameUiErrorBoundary extends Component< { children: ReactNode; onRuntimeError: (error: unknown, phase: string, componentStack?: string) => void }, { failed: boolean } > — ⚠ undocumented +- `RuntimeDiagnostic` (interface): interface RuntimeDiagnostic — ⚠ undocumented +- `logRuntimeError` (function): function logRuntimeError(error: unknown, phase: string, componentStack?: string): Omit — ⚠ undocumented + +## @jgengine/shell/drivers/FrameDriver + +- `FrameDriver` (function): function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }: { ctx: GameContext; playable: PlayableGame; tracker: ActionStateTracker; y… — ⚠ undocumented +- `POSTER_SETTLE_SECONDS` (const): const POSTER_SETTLE_SECONDS: 1.6 — ⚠ undocumented + +## @jgengine/shell/drivers/HudOnlyDriver + +- `HudOnlyDriver` (function): function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, }: { ctx: GameContext; playable: PlayableGame; tracker: ActionStateTracker; pointerAxisRef: { current: PointerAxisState | null }; gateRef: { current: boolean }; onRuntimeError: (error: unknown, phase: s… — ⚠ undocumented + ## @jgengine/shell/environment - `DaylightCycleConfig` (interface): interface DaylightCycleConfig — ⚠ undocumented @@ -1022,6 +1077,17 @@ - `RenderObject` (type): type RenderObject = (object: SceneObject) => ReactNode — ⚠ undocumented - `resolveGameLoader` (function): function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise) | undefined — ⚠ undocumented +## @jgengine/shell/render/SceneLighting + +- `BackdropFog` (function): function BackdropFog({ fog }: { fog: BackdropConfig["fog"] }): React.JSX.Element | null — ⚠ undocumented +- `ConfiguredLighting` (function): function ConfiguredLighting({ lighting }: { lighting: LightingConfig }): React.JSX.Element — ⚠ undocumented + +## @jgengine/shell/render/SceneModels + +- `EntityModel` (function): function EntityModel({ model, instanceId }: { model: ModelConfig; instanceId?: string }): React.JSX.Element — ⚠ undocumented +- `EntitySprite` (function): function EntitySprite({ sprite }: { sprite: EntitySpriteConfig }): React.JSX.Element — ⚠ undocumented +- `IsolatedEntityModel` (function): function IsolatedEntityModel({ model, instanceId, fallback, }: { model: ModelConfig; instanceId?: string; fallback?: ReactNode; }): React.JSX.Element — ⚠ undocumented + ## @jgengine/shell/render/modelRender - `MaterialCache` (interface): interface MaterialCache — ⚠ undocumented @@ -1118,9 +1184,13 @@ - `SettingsControllerInput` (interface): interface SettingsControllerInput — ⚠ undocumented - `SettingsKeybindRow` (interface): interface SettingsKeybindRow — One rebindable action row rendered in the controls settings category. - `SettingsRow` (interface): interface SettingsRow — One editable setting rendered in a settings menu category. -- `bindingLabel` (function): function bindingLabel(code: string): string — Short display label for a raw key/button code (e.g. `"KeyW"` → `"W"`). - `useSettingsCategories` (function): function useSettingsCategories(config: SettingsControllerInput): SettingsCategoryView[] — ⚠ undocumented +## @jgengine/shell/shellConstants + +- `EMPTY_RESERVED` (const): const EMPTY_RESERVED: ReadonlySet — No action names are reserved when no camera rig is active (hud/none presentation): games may bind `turnLeft`/`interact`/etc. as their own. +- `NO_ACTIONS` (const): const NO_ACTIONS: string[] — Empty action list — published while the orientation gate is up to suppress all held input without touching the tracker. + ## @jgengine/shell/structures - `BuildingBlock` (function): function BuildingBlock({ part, palette }: BuildingBlockProps): React.JSX.Element — ⚠ undocumented @@ -1198,14 +1268,6 @@ - `TerrainNormal` (type): type TerrainNormal = readonly [number, number, number] — A surface normal vector at a terrain sample point. - `TerrainSeed` (type): type TerrainSeed = number | string — ⚠ undocumented - `TerrainVertexColorOptions` (interface): interface TerrainVertexColorOptions — ⚠ undocumented -- `arenaField` (function): function arenaField(config?: ArenaFieldConfig): TerrainField — Builds a `TerrainField` with a flat spawn plateau, rolling hills, and a basin, for combat arenas. -- `flatField` (function): function flatField(): TerrainField — A flat, zero-height `TerrainField` for arenas with no elevation. -- `fractalNoise` (function): function fractalNoise(x: number, z: number, config: FractalNoiseConfig): number — Layers `valueNoise` octaves per `config` into a single normalized noise sample. -- `noiseField` (function): function noiseField(config?: NoiseFieldConfig): TerrainField — Builds a `TerrainField` whose height is fractal noise shaped by `config`. -- `resolveGroundStep` (function): function resolveGroundStep(field: TerrainField, x: number, z: number, stepX: number, stepZ: number, maxSlope?: number): { stepX: number; stepZ: number; } — Zeroes out a movement step's x or z component where it would climb steeper than `maxSlope`. -- `resolveTerrainField` (function): function resolveTerrainField(descriptor?: TerrainEnvironmentDescriptor): TerrainField — Resolves a `TerrainEnvironmentDescriptor` into a concrete `TerrainField`, applying flatten masks. -- `valueNoise` (function): function valueNoise(x: number, z: number, seed: number): number — Smoothly interpolated 2D value noise in `[-1, 1]` for the given seed. -- `withNormal` (function): function withNormal(sampleHeight: (x: number, z: number) => number): TerrainField["sampleNormal"] — Derives a `TerrainField.sampleNormal` from a height sampler via finite-difference gradients. ## @jgengine/shell/terrain @@ -1239,14 +1301,6 @@ - `TerrainNormal` (type): type TerrainNormal = readonly [number, number, number] — A surface normal vector at a terrain sample point. - `TerrainSeed` (type): type TerrainSeed = number | string — ⚠ undocumented - `TerrainVertexColorOptions` (interface): interface TerrainVertexColorOptions — ⚠ undocumented -- `arenaField` (function): function arenaField(config?: ArenaFieldConfig): TerrainField — Builds a `TerrainField` with a flat spawn plateau, rolling hills, and a basin, for combat arenas. -- `flatField` (function): function flatField(): TerrainField — A flat, zero-height `TerrainField` for arenas with no elevation. -- `fractalNoise` (function): function fractalNoise(x: number, z: number, config: FractalNoiseConfig): number — Layers `valueNoise` octaves per `config` into a single normalized noise sample. -- `noiseField` (function): function noiseField(config?: NoiseFieldConfig): TerrainField — Builds a `TerrainField` whose height is fractal noise shaped by `config`. -- `resolveGroundStep` (function): function resolveGroundStep(field: TerrainField, x: number, z: number, stepX: number, stepZ: number, maxSlope?: number): { stepX: number; stepZ: number; } — Zeroes out a movement step's x or z component where it would climb steeper than `maxSlope`. -- `resolveTerrainField` (function): function resolveTerrainField(descriptor?: TerrainEnvironmentDescriptor): TerrainField — Resolves a `TerrainEnvironmentDescriptor` into a concrete `TerrainField`, applying flatten masks. -- `valueNoise` (function): function valueNoise(x: number, z: number, seed: number): number — Smoothly interpolated 2D value noise in `[-1, 1]` for the given seed. -- `withNormal` (function): function withNormal(sampleHeight: (x: number, z: number) => number): TerrainField["sampleNormal"] — Derives a `TerrainField.sampleNormal` from a height sampler via finite-difference gradients. ## @jgengine/shell/terrain/CarvedTerrain @@ -1326,6 +1380,10 @@ - `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. - `touchDockClearance` (function): function touchDockClearance(scheme: TouchScheme | null, scale = 1): number — Vertical space (px, excluding device safe areas) that *bottom-docked* clusters occupy above the bottom edge. The shell publishes it as `--jg-hud-dock-clearance` so `HudCanvas` regions never collide with touch controls. Side rails and top clusters reserve their own rectangles through the layout registry instead of this scalar. +## @jgengine/shell/useShellMultiplayerSync + +- `useShellMultiplayerSync` (function): function useShellMultiplayerSync(ctx: GameContext | null, multiplayer: ShellMultiplayer | null, playable: PlayableGame, serverIdRef: { current: string | null }, setRemotePlayers: Dispatch>): void — Joins the multiplayer server for the live context and wires presence, feed relay, and chat sync until teardown. + ## @jgengine/shell/visibility/CullingProvider - `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. @@ -1542,6 +1600,11 @@ - `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`. +## @jgengine/shell/world/WorldScene + +- `RemotePlayers` (function): function RemotePlayers({ rows }: { rows: PresencePoseRow[] }): React.JSX.Element — ⚠ undocumented +- `WorldView` (function): function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }: { entitySprites: Record | undefined; entityModels: Record | undefined; objectModels… — ⚠ undocumented + ## @jgengine/shell/world/entityPose - `PoseSource` (interface): interface PoseSource — ⚠ undocumented diff --git a/.claude/skills/jgengine-ui/capabilities.md b/.claude/skills/jgengine-ui/capabilities.md index 525ff733a..0ee6784ee 100644 --- a/.claude/skills/jgengine-ui/capabilities.md +++ b/.claude/skills/jgengine-ui/capabilities.md @@ -4,10 +4,6 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*. -## camera-shake — calibrated trauma² camera kick with zero tuning - -- `traumaShake` (function) · `import { traumaShake } from "@jgengine/shell/camera"` - ## clock-format — format a signed time gap like a race split (+/- m:ss.ff) - `formatDelta` (function) · `import { formatDelta } from "@jgengine/core/format/duration"` diff --git a/.claude/skills/jgengine-world/SKILL.md b/.claude/skills/jgengine-world/SKILL.md index a285d2778..623255747 100644 --- a/.claude/skills/jgengine-world/SKILL.md +++ b/.claude/skills/jgengine-world/SKILL.md @@ -52,7 +52,7 @@ movement: { | `PhysicsWorld` (`physics/physicsWorld`) | Debris, piles, joints, vehicles-lite, structure collapse — headless SoA sim. | | `ctx.scene.entity.bind(key).sync(bodies, dt)` (`scene/bodyBind`) | When a sim body **and** a scene entity must share pose — do not hand-write per-body `setPose` every tick. | -ADR: `packages/core/src/physics/README.md`. Never feed the walk controller into `PhysicsWorld` “for realism” without a bind plan. +Decision record: `packages/core/src/physics/README.md`. Never feed the walk controller into `PhysicsWorld` “for realism” without a bind plan. **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). diff --git a/.claude/skills/jgengine-world/api.md b/.claude/skills/jgengine-world/api.md index c0fbe87df..a0e883f5f 100644 --- a/.claude/skills/jgengine-world/api.md +++ b/.claude/skills/jgengine-world/api.md @@ -69,6 +69,7 @@ - `SpawnDirectorConfig` (interface): interface SpawnDirectorConfig — ⚠ undocumented - `SpawnDirectorState` (interface): interface SpawnDirectorState — ⚠ undocumented - `SpawnEntry` (interface): interface SpawnEntry — ⚠ undocumented +- `SpawnPointBiasStrength` (type): type SpawnPointBiasStrength = "subtle" | "moderate" | "strong" — How strongly `distanceBias` weights candidates by distance from `avoid` — a named intent, not a weighting exponent. - `SpawnPointDistanceBias` (type): type SpawnPointDistanceBias = "near" | "far" | "none" — Preference for picking a spawn point relative to `avoid` positions: closer, farther, or unweighted. - `SpawnPointSelectionOptions` (interface): interface SpawnPointSelectionOptions — Semantic options for selecting a spawn point without exposing weighting internals. - `SpawnRequest` (interface): interface SpawnRequest — ⚠ undocumented @@ -289,6 +290,7 @@ - `PoseAllowedStates` (interface): interface PoseAllowedStates — ⚠ undocumented - `PoseHitbox` (interface): interface PoseHitbox — ⚠ undocumented - `PoseRejection` (interface): interface PoseRejection — ⚠ undocumented +- `PoseSnapshot` (interface): interface PoseSnapshot — ⚠ undocumented - `PoseState` (interface): interface PoseState — ⚠ undocumented - `createPoseState` (function): function createPoseState(resolveAllowed: (instanceId: string) => PoseAllowedStates | null | undefined): PoseState — Stance/pose transitions — stand, crouch, prone — that change the hitbox and movement. @@ -453,6 +455,7 @@ - `JointOptions` (interface): interface JointOptions — ⚠ undocumented - `MAX_BROADPHASE_CELLS` (const): const MAX_BROADPHASE_CELLS: 1000000 — Cap on `nx*ny*nz` broadphase cells — guards a huge-bounds/tiny-cellSize config from hanging `step()`. - `PhysicsBounds` (interface): interface PhysicsBounds — ⚠ undocumented +- `PhysicsPrecision` (type): type PhysicsPrecision = "low" | "standard" | "high" — Simulation fidelity intent: `low` (cheap — many bodies, loose stacks, forgiving sleep), `standard` (default), `high` (tight stacks, accurate joints, worth the extra solver work). Sets the solver knobs below to a matched preset; an individual knob left explicit always wins over the preset. - `PhysicsStats` (interface): interface PhysicsStats — ⚠ undocumented - `PhysicsWorld` (class): class PhysicsWorld — ⚠ undocumented - `PhysicsWorldConfig` (interface): interface PhysicsWorldConfig — ⚠ undocumented @@ -691,6 +694,7 @@ - `PossessionDeps` (interface): interface PossessionDeps — ⚠ undocumented - `PossessionEntities` (interface): interface PossessionEntities — ⚠ undocumented - `PossessionEvents` (interface): interface PossessionEvents — ⚠ undocumented +- `PossessionSnapshot` (interface): interface PossessionSnapshot — ⚠ undocumented - `PossessionSwappedEvent` (interface): interface PossessionSwappedEvent — ⚠ undocumented - `createPossession` (function): function createPossession(deps: PossessionDeps): Possession — ⚠ undocumented @@ -1412,18 +1416,26 @@ - `ScatterRegion` (interface): interface ScatterRegion — A resolvable scatter region: a closed polygon footprint plus its fill rules. - `ScatterRegionRules` (interface): interface ScatterRegionRules — How a scatter region fills its polygon: density, spacing, variation, and masking rules. - `ScatterTerrain` (interface): interface ScatterTerrain — Ground sampler a scatter resolve reads height/normal from (the sculpt terrain or the game's ground). -- `clearanceZonesFrom` (function): function clearanceZonesFrom(doc: EditorDocument, options: ClearanceOptions = {}): AvoidZone[] — Point-pad clearance **discs** from a document's markers/volumes — the terrain-flatten set (spawns, plots, POIs get a level pad). A marker/volume contributes a disc when it carries `meta.clearance` or its kind is in `kinds`. Paths are *not* included (they render draped, never flattened — see {@link clearanceMasksFrom} for their foliage corridor). Pass `ids`/`kinds` to scope it. +- `clearanceZonesFrom` (function): function clearanceZonesFrom(doc: SceneDocumentLike, options: ClearanceOptions = {}): AvoidZone[] — Point-pad clearance **discs** from a document's markers/volumes — the terrain-flatten set (spawns, plots, POIs get a level pad). A marker/volume contributes a disc when it carries `meta.clearance` or its kind is in `kinds`. Paths are *not* included (they render draped, never flattened — see {@link clearanceMasksFrom} for their foliage corridor). Pass `ids`/`kinds` to scope it. - `distanceToPolygonEdge` (function): function distanceToPolygonEdge(point: Vec2, polygon: readonly Vec2[]): number — Shortest distance from a point to a polygon's boundary. -- `isScatterPath` (function): function isScatterPath(path: EditorPath): boolean — True when an editor path is a foliage/scatter region. +- `isScatterPath` (function): function isScatterPath(path: ScenePathLike): boolean — True when an editor path is a foliage/scatter region. - `pointInPolygon` (function): function pointInPolygon(point: Vec2, polygon: readonly Vec2[]): boolean — Ray-casting point-in-polygon test on the XZ plane. - `polygonArea` (function): function polygonArea(polygon: readonly Vec2[]): number — Shoelace area of a polygon (always non-negative), in square meters. - `polygonBounds` (function): function polygonBounds(polygon: readonly Vec2[]): Aabb | null — Axis-aligned bounds of a polygon, or null if it has no points. - `readScatterPalette` (function): function readScatterPalette(meta: Record | undefined): ScatterPaletteEntry[] — Parses a scatter region's palette from meta: a weighted `palette` array, else a single `item`. -- `readScatterRules` (function): function readScatterRules(path: EditorPath): ScatterRegionRules | null — The path's scatter rules with defaults filled in; null for non-scatter paths. -- `resolveScatter` (function): function resolveScatter(doc: EditorDocument, terrain?: ScatterTerrain, options: ResolveScatterOptions = {}): ScatterInstance[] — Every scatter region's placements across a document, grounded on `terrain` when provided. Regions honor clearance masks: their own manual `avoid` discs, plus (when the region's `autoAvoid` is on and `options.autoAvoid !== false`) the document-wide discs + path corridors from {@link clearanceMasksFrom} — so foliage auto-clears spawns, plots, and paths without hand-carving the polygon. +- `readScatterRules` (function): function readScatterRules(path: ScenePathLike): ScatterRegionRules | null — The path's scatter rules with defaults filled in; null for non-scatter paths. +- `resolveScatter` (function): function resolveScatter(doc: SceneDocumentLike, terrain?: ScatterTerrain, options: ResolveScatterOptions = {}): ScatterInstance[] — Every scatter region's placements across a document, grounded on `terrain` when provided. Regions honor clearance masks: their own manual `avoid` discs, plus (when the region's `autoAvoid` is on and `options.autoAvoid !== false`) the document-wide discs + path corridors from {@link clearanceMasksFrom} — so foliage auto-clears spawns, plots, and paths without hand-carving the polygon. - `resolveScatterRegion` (function): function resolveScatterRegion(region: ScatterRegion, terrain?: ScatterTerrain, avoid?: AvoidMasks): ScatterInstance[] — Deterministic placements for one scatter region: scatter its polygon footprint at `density` items/m² (respecting `minSpacing`), clip to the polygon, thin near the edge, drop placements outside the slope/height mask, and derive item/scale/yaw from the region id + seed — so the same saved region always grows the same field. Grounds each instance on `terrain` when provided. -- `scatterRegionEstimate` (function): function scatterRegionEstimate(path: EditorPath): { area: number; count: number } — Estimated placement count for a scatter path — density × polygon area, for a live UI readout. -- `scatterRegionFromPath` (function): function scatterRegionFromPath(path: EditorPath): ScatterRegion | null — Builds a resolvable {@link ScatterRegion} from a scatter path (XZ polygon + rules), or null. +- `scatterRegionEstimate` (function): function scatterRegionEstimate(path: ScenePathLike): { area: number; count: number } — Estimated placement count for a scatter path — density × polygon area, for a live UI readout. +- `scatterRegionFromPath` (function): function scatterRegionFromPath(path: ScenePathLike): ScatterRegion | null — Builds a resolvable {@link ScatterRegion} from a scatter path (XZ polygon + rules), or null. + +## @jgengine/core/world/sceneShapes + +- `SceneDocumentLike` (interface): interface SceneDocumentLike — The minimal document shape scatter/vegetation resolve against — markers, volumes, and paths only. Any `EditorDocument` satisfies it structurally; this module never imports the editor domain, so world stays the one-directional dependency editor already builds on. +- `SceneMarkerLike` (interface): interface SceneMarkerLike — The minimal point-object shape clearance reads from a document's markers; any `EditorMarker` satisfies it. +- `ScenePathLike` (interface): interface ScenePathLike — The minimal polyline shape {@link resolveScatterRegion} et al. read; any `EditorPath` satisfies it. +- `ScenePoint3` (interface): interface ScenePoint3 — A world-space point — structurally compatible with the editor's `EditorVec3`. +- `SceneVolumeLike` (interface): interface SceneVolumeLike — The minimal volume shape vegetation/clearance read; any `EditorVolume` satisfies it. ## @jgengine/core/world/segment diff --git a/.claude/skills/jgengine-world/capabilities.md b/.claude/skills/jgengine-world/capabilities.md index a971908e7..9e3abdb4b 100644 --- a/.claude/skills/jgengine-world/capabilities.md +++ b/.claude/skills/jgengine-world/capabilities.md @@ -20,9 +20,9 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createPoseState` (function) · `import { createPoseState } from "@jgengine/core/movement/poseState"` -## dash-move — a dash/dodge burst with i-frames and cooldown +## entity-meta — cast-free narrow of SceneEntity.meta via a type guard -- `dashDisplacement` (function) · `import { dashDisplacement } from "@jgengine/core/movement/dash"` +- `entityMetaOf` (function) · `import { entityMetaOf } from "@jgengine/core/scene/entityStore"` ## follow-trail — trailing follower/snake formation that chases a leader @@ -72,10 +72,6 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `SOIL_KIND` (const) · `import { SOIL_KIND } from "@jgengine/core/world/soilKind"` -## visitor-loop — many-agent seek/travel/dwell/depart state machine over weighted POIs - -- `createVisitorLoop` (function) · `import { createVisitorLoop } from "@jgengine/core/ai/crowd"` - ## volumetric-clouds — raymarched cloud layer sky option - `VolumetricCloudsConfig` (interface) · `import { VolumetricCloudsConfig } from "@jgengine/core/world/volumetricClouds"` diff --git a/.claude/skills/jgengine/api.md b/.claude/skills/jgengine/api.md index 52b553959..55ffbea47 100644 --- a/.claude/skills/jgengine/api.md +++ b/.claude/skills/jgengine/api.md @@ -42,8 +42,83 @@ - `tilemap` (function): function tilemap(config: TilemapWorldConfig): WorldFeature — Declares a 2D tilemap world from a map string. - `voxel` (function): function voxel(config: VoxelWorldConfig): WorldFeature — Declares a voxel-grid world for block-based games. +## @jgengine/core/combat + +- `AbilityKit` (interface): interface AbilityKit — ⚠ undocumented +- `AbilitySlotSnapshot` (interface): interface AbilitySlotSnapshot — ⚠ undocumented +- `AbilitySlotState` (type): type AbilitySlotState = "ready" | "cooldown" | "no-resource" | "just-cast" — ⚠ undocumented +- `AnimationClip` (interface): interface AnimationClip — ⚠ undocumented +- `BuildupProc` (interface): interface BuildupProc — ⚠ undocumented +- `CheckAdvantage` (type): type CheckAdvantage = "advantage" | "disadvantage" | "normal" — ⚠ undocumented +- `CheckResult` (interface): interface CheckResult — ⚠ undocumented +- `ComboStep` (interface): interface ComboStep — ⚠ undocumented +- `DEFAULT_EYE_HEIGHT` (const): const DEFAULT_EYE_HEIGHT: number — Shot-origin and first-person camera eye height above an entity's position: 90% of the default 1.8m hitbox top. +- `DEFAULT_FIRE_PULSE_SECONDS` (const): const DEFAULT_FIRE_PULSE_SECONDS: 0.12 — Default `RenderCueTuning.firePulseSeconds`. +- `DEFAULT_HIT_PULSE_SECONDS` (const): const DEFAULT_HIT_PULSE_SECONDS: 0.2 — Default `RenderCueTuning.hitPulseSeconds`. +- `DEFAULT_RENDER_CUES` (const): const DEFAULT_RENDER_CUES: Readonly — Neutral starting cue set: idle, unarmed, undamaged. +- `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; via?: { item?: string } } | { kind: "environment"; source: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event. +- `EntityRenderCues` (interface): interface EntityRenderCues — Per-entity render cues: the motion/animation signal a custom `renderEntity` or first-person viewmodel component needs to drive gait, muzzle flash, reload poses, and hit reactions — without diffing the parent group's position itself or reading a game-side module map for attack timing. +- `EventMeter` (interface): interface EventMeter — ⚠ undocumented +- `EventMeterFeedResult` (interface): interface EventMeterFeedResult — ⚠ undocumented +- `FrameRange` (interface): interface FrameRange — ⚠ undocumented +- `HitReactionConfig` (interface): interface HitReactionConfig — ⚠ undocumented +- `Magazine` (interface): interface Magazine — A per-weapon magazine: discrete loaded rounds, a timed reload that refills from a reserve pool, and the reserve-pool interaction itself — the primitive that replaces hand-rolling mag size, reload delay, and reserve bookkeeping per game (#536.2). +- `MagazineReserve` (interface): interface MagazineReserve — Draws ammo for a `Magazine`'s reload from wherever the reserve pool actually lives. +- `MeterAddResult` (interface): interface MeterAddResult — ⚠ undocumented +- `ObjectRaycastHit` (interface): interface ObjectRaycastHit — ⚠ undocumented +- `OnDeathSpec` (interface): interface OnDeathSpec — ⚠ undocumented +- `ProjectileSystemDeps` (interface): interface ProjectileSystemDeps — ⚠ undocumented +- `RaycastHit` (type): type RaycastHit = EntityRaycastHit | ObjectRaycastHit — ⚠ undocumented +- `ReceiveMap` (type): type ReceiveMap = Record — ⚠ undocumented +- `RenderCueTuning` (interface): interface RenderCueTuning — Tuning knobs for `advanceMotionCues` / `useEntityRenderCues`; every field has a default, override only what a weapon/rig's feel needs to differ. +- `ResourcePool` (interface): interface ResourcePool — ⚠ undocumented +- `ShotOriginPolicy` (type): type ShotOriginPolicy = | { kind: "converge"; muzzle?: EntityPosition; height?: number } | { kind: "eye"; height?: number } | { kind: "legacy" } | { kind: "entity" } | { kind: "entityOffset"; offset: EntityPosition } | { kind: "muzzle"; offset?: EntityPosition } | { kind: "camera"; origin: EntityPos… — How a shot's world-space origin (and optional direction) is resolved before prediction/settlement. - `converge` — the shot leaves the gun `muzzle` yet still passes through whatever the shooter's sightline (crosshair) covers: origin is the muzzle offset, direction is bent from the muzzle to the eye ray's aim point. The projectile system's default for a free `{ yaw, pitch }` aim, so a bullet visibly comes from the barrel without missing the reticle. Needs a scene raycast to find the aim point (`convergeShot`); a bare `resolveShot` degrades to a straight muzzle ray. Passes an explicit `{ origin, direction }` aim through untouched. - `eye` — `aim.origin` when present, else the shooter's entity position raised to eye height; the shot traces the shooter's sightline, so what the crosshair covers is what gets hit. - `legacy` — `aim.origin` when present, else the shooter's raw entity position (feet). - `entity` — always the shooter's entity position. - `entityOffset` / `muzzle` — entity-local offset rotated by the shooter's yaw (muzzle on a weapon model). - `camera` — explicit camera/reticle world origin (and optional direction override). - `world` — absolute world origin. +- `Stats` (interface): interface Stats — ⚠ undocumented +- `TelegraphConfig` (interface): interface TelegraphConfig — ⚠ undocumented +- `TelegraphShape` (type): type TelegraphShape = | { kind: "circle"; radius: number } | { kind: "ring"; radius: number; innerRadius: number } | { kind: "cone"; radius: number; angle: number } | { kind: "line"; length: number; width: number } — ⚠ undocumented +- `advanceCombo` (function): function advanceCombo(input: AdvanceComboInput): AdvanceComboResult — ⚠ undocumented +- `advanceMotionCues` (function): function advanceMotionCues(cues: EntityRenderCues, speed: number, dt: number, tuning?: RenderCueTuning): EntityRenderCues — Advances `bobPhase` and decays `recoil` from a live `speed` sample (e.g. `groundSpeed(entity)`); leaves event-driven fields untouched. +- `applyRenderAnimationEvent` (function): function applyRenderAnimationEvent(cues: EntityRenderCues, event: string): EntityRenderCues — Applies a `entity.animation` event (`"fire"` / `"reload"` / `"reloadEnd"`, or any game-defined name) to the cue set. Unknown event names are a no-op. +- `applyRenderDeathEvent` (function): function applyRenderDeathEvent(cues: EntityRenderCues): EntityRenderCues — Marks the cue set dead after `entity.died`; sticky for the lifetime of the render component. +- `applyRenderHitEvent` (function): function applyRenderHitEvent(cues: EntityRenderCues): EntityRenderCues — Marks a `combat.hitReaction` pulse; the caller clears `hit` again after its own pulse window. +- `attackMeta` (function): function attackMeta(tags: readonly AttackTag[], extra?: Omit): AttackMeta — ⚠ undocumented +- `convergeShot` (function): function convergeShot(deps: ShotOriginDeps, from: string, aim: Aim, range: number, sightHit: (origin: EntityPosition, direction: EntityPosition) => EntityPosition | null, muzzleOffset?: EntityPosition): ResolvedShot | null — Resolves a `converge` shot with scene knowledge: fires from the gun muzzle but bends the direction so the shot passes through the aim point the shooter's eye ray covers. `sightHit` casts the eye ray and returns where it lands (first impact), or `null` to fall back to a point `range` metres down the sightline. A `{ origin, direction }` aim is passed through unchanged (nothing to converge). +- `counters` (function): function counters(meta: AttackMeta, move: CounterMove): boolean — ⚠ undocumented +- `createAbilityKit` (function): function createAbilityKit(configs: readonly AbilitySlotConfig[], options: AbilityKitOptions = {}): AbilityKit — A bar of cooldown-gated abilities the player fires by slot, tracking readiness and cooldown per ability. +- `createAccumulatorMeter` (function): function createAccumulatorMeter(config: AccumulatorMeterConfig): AccumulatorMeter — A raw accumulating gauge that crosses named tier thresholds as a value builds, with optional decay — the primitive under charge, rage, and combo meters. +- `createBuildupMeter` (function): function createBuildupMeter(config: BuildupMeterConfig): BuildupMeter — Accumulate an ailment buildup — bleed, freeze, poison — that procs a status once it fills, then decays. +- `createCastRunner` (function): function createCastRunner(): CastRunner — Run a channeled cast/charge timer that movement or damage can interrupt — the spell cast bar. +- `createComboPoints` (function): function createComboPoints(config: ComboPointsConfig): ComboPoints — Build and spend finisher points — the combo-point economy behind rogue-style builders and spenders. +- `createComboRunner` (function): function createComboRunner(combo: ComboString, anim: AnimationState): ComboRunner — Advance a chained melee string from timed inputs, tracking the current step and its cancel/continue windows. +- `createDeathSystem` (function): function createDeathSystem(deps: DeathSystemDeps): DeathSystem — Resolve entity death and the on-death consequences — drops, respawn eligibility, kill credit. +- `createDefensiveWindow` (function): function createDefensiveWindow(config: DefensiveWindowConfig): DefensiveWindow — Open a timed defensive window — block, parry, or i-frames — and test incoming hits against it. +- `createDotField` (function): function createDotField(): DotField — Builds an empty {@link DotField}; `apply` DoTs onto it and drain damage each frame with `tick`. +- `createDownedState` (function): function createDownedState(config: DownedConfig): DownedState — A downed/bleed-out state that ticks toward death and that teammates can revive before the timer runs out. +- `createEffectSystem` (function): function createEffectSystem(deps: EffectSystemDeps): EffectSystem — Apply, stack, and tick timed status effects — buffs, debuffs, DoTs — on entities. +- `createEventMeter` (function): function createEventMeter(config: EventMeterConfig): EventMeter — A heat/hype gauge that rises as tagged events land and cools between them, firing when it fills or breaks — the streak/overdrive meter shooters and fighters hand-roll. +- `createMagazine` (function): function createMagazine(config: MagazineConfig): Magazine — Builds a {@link Magazine}: discrete loaded ammo, a timed reload, and reserve-pool interaction. +- `createProjectileSystem` (function): function createProjectileSystem(deps: ProjectileSystemDeps): ProjectileSystem — Spawn and advance projectiles each frame, resolving travel, lifetime, and hits. +- `createRegenShield` (function): function createRegenShield(config: RegenShieldConfig): RegenShield — Builds a {@link RegenShield} that suppresses regen for `regenDelayMs` after each hit. +- `createResourcePool` (function): function createResourcePool(config: ResourcePoolConfig): ResourcePool — A regenerating resource pool — mana, stamina, energy — that actions spend from and that refills over time. +- `createStaggerMeter` (function): function createStaggerMeter(config: StaggerMeterConfig): StaggerMeter — Build a stagger/poise gauge from landed hits toward a break threshold that staggers the target. +- `createStats` (function): function createStats(base: Record, options?: CreateStatsOptions): Stats — A stat block whose base values take stacking, timed buffs and debuffs, resolving the modified value on read. +- `eyeHeightFromColliders` (function): function eyeHeightFromColliders(set: EntityColliderSet | null | undefined): number — Eye height derived from a collider set: 90% of the tallest hitbox top, or the humanoid default when unknown. +- `impactPresets` (const): const impactPresets: { readonly pickup: { readonly hitstopMs: 0; readonly trauma: 0.15; }; readonly jumpLand: { readonly hitstopMs: 0; readonly trauma: 0.2; }; readonly enemyKilled: { readonly hitstopMs: 40; readonly trauma: 0.3; }; readonly playerHit: { readonly hitstopMs: 70; readonly trauma: 0.4;… — Calibrated per-event impact feel — hitstop and trauma numbers harvested from a shipped game-feel reference, not hand-invented per game. `explosion` and `playerHit` are "heavy hit" events (60–90ms hitstop @ 0.05 timescale); `pickup`/`jumpLand` are light events with no hitstop. Trauma is later clamped to 1.0 by `resolveHitReaction`. +- `isBlockable` (function): function isBlockable(meta: AttackMeta): boolean — ⚠ undocumented +- `isDodgeable` (function): function isDodgeable(meta: AttackMeta): boolean — ⚠ undocumented +- `isParryable` (function): function isParryable(meta: AttackMeta): boolean — ⚠ undocumented +- `resistanceScale` (function): function resistanceScale(matrix: ResistanceMatrix, category: TCategory | string, targetProperties: readonly (TProperty | string)[]): number — ⚠ undocumented +- `resolveDefense` (function): function resolveDefense(input: ResolveDefenseInput): DefenseResolution — ⚠ undocumented +- `resolveHitReaction` (function): function resolveHitReaction(config: HitReactionConfig | ImpactPresetName, input: HitReactionInput): HitReaction — Resolves hit feel (hitstop, knockback impulse, camera shake) from either a named `impactPresets` event (`resolveHitReaction("explosion", input)`) or a raw `HitReactionConfig` override. +- `resolveResistance` (function): function resolveResistance(matrix: ResistanceMatrix, category: TCategory | string, targetProperties: readonly (TProperty | string)[]): ResistanceResult — ⚠ undocumented +- `resolveShot` (function): function resolveShot(deps: ShotOriginDeps, from: string, aim: Aim, policy: ShotOriginPolicy = { kind: "eye" }): ResolvedShot | null — ⚠ undocumented +- `rollCheck` (function): function rollCheck(input: CheckInput, rng: () => number = Math.random): CheckResult — Resolve a tabletop-style pass/fail roll against a target number with modifiers and crit/fumble bands. +- `tierAt` (function): function tierAt(value: number, tiers: readonly MeterTier[]): string | null — The highest tier id whose `at` threshold `value` has reached, or `null` below every tier — the pure lookup `createAccumulatorMeter`/`createEventMeter` call on every `add`/`tick`. + ## @jgengine/core/commands/commandRegistry +- `CommandDecodeResult` (type): type CommandDecodeResult = | { ok: true; value: TInput } | { ok: false; reason: string } — ⚠ undocumented +- `CommandDecoder` (type): type CommandDecoder = (input: unknown) => CommandDecodeResult — Parses raw `unknown` transport input into `TInput`, rejecting anything that doesn't match the command's declared shape. Runs before `validate`/`apply`, so a malformed payload never reaches game logic. - `CommandDefinition` (interface): interface CommandDefinition — ⚠ undocumented - `CommandRegistry` (interface): interface CommandRegistry — ⚠ undocumented - `CommandRejection` (interface): interface CommandRejection — ⚠ undocumented @@ -130,12 +205,346 @@ - `TunableVec3` (type): type TunableVec3 = [number, number, number] — ⚠ undocumented - `TunableVec4` (type): type TunableVec4 = [number, number, number, number] — ⚠ undocumented +## @jgengine/core/gameplay + +- `ActionCodes` (type): type ActionCodes = | readonly TCode[] | { hold?: readonly TCode[]; toggle?: readonly TCode[]; repeatMs?: number } — ⚠ undocumented +- `ActionCodesMap` (type): type ActionCodesMap = Record< TAction, ActionCodes > — Maps each game action name to the input codes (hold/toggle keys, repeat rate) that trigger it. +- `ActionStateTracker` (interface): interface ActionStateTracker — ⚠ undocumented +- `AffixPool` (interface): interface AffixPool — ⚠ undocumented +- `AxisBindingMap` (type): type AxisBindingMap = Record — ⚠ undocumented +- `AxisChannelConfig` (interface): interface AxisChannelConfig — ⚠ undocumented +- `AxisInput` (interface): interface AxisInput — ⚠ undocumented +- `BackdropConfig` (interface): interface BackdropConfig — Generic sky/background/fog for ANY world kind, including a custom `environment` component (#207.6). +- `Behaviour` (class): class Behaviour — Subclass and override the lifecycle hooks. A behaviour only joins the per-frame update dispatch if it actually overrides `onUpdate` (prototype-identity check at each activation), so hook-only behaviours cost nothing per frame. +- `BehaviourModule` (class): class BehaviourModule — A world-lifetime service with typed sibling access via `this.modules`. Modules awake and start before any behaviour during `world.start()`, subscribe to update dispatch first (their `onUpdate` fires before every behaviour's), and have no disable/destroy — they live as long as the world. +- `BehaviourWorld` (interface): interface BehaviourWorld — ⚠ undocumented +- `BindingOverrides` (type): type BindingOverrides = Record — Player-rebound keys, keyed by action name. Values mirror an `ActionCodes` entry so hold/toggle/repeat semantics survive a rebind — the settings menu only swaps which physical codes drive the action. +- `CAMERA_FRUSTUM_DEFAULTS` (const): const CAMERA_FRUSTUM_DEFAULTS: { readonly fov: 55; readonly near: 0.1; readonly far: 300; readonly zoom: 50; } — ⚠ undocumented +- `CameraKeyframe` (interface): interface CameraKeyframe — One stop on a scripted camera path (#29). +- `CameraRigKind` (type): type CameraRigKind = | "orbit" | "first" | "topDown" | "rts" | "shoulder" | "lockOn" | "chase" | "observer" | "turntable" | "sideScroll" | "inspection" | "none" — Which camera rig the shell mounts. Every rig accepts `followEntityId: null` (avatar-less games — city-builders, card games, auto-battlers — still get a camera). Rigs are tuned through their config block below, never by writing camera positions from `onTick`. - `orbit` — third-person chase (the historical default; `perspective: "third"`). - `first` — pointer-lock mouse-look (`perspective: "first"`). - `topDown` — fixed height/pitch/yaw with decoupled follow (ARPG iso, top-down). - `rts` — free-pan / edge-scroll / rotate / zoom, optional follow. - `shoulder` — over-the-shoulder with ADS transition + shoulder swap. - `lockOn` — yaw bound to the player→target vector; move axis becomes strafe. - `chase` — speed-reactive vehicle chase (speed→FOV, spring arm, shake) + cockpit/hood/rear views. - `observer` — detached spectator/photo cam bound to any entity or fixed point; never reads player input. - `turntable` — slow auto-orbit of a fixed point: a rotating display stand for a scene. The friendly, flat spelling of `observer`'s point-orbit mode; providing `camera.turntable` selects it without an explicit `rig`. - `sideScroll` — fixed lateral follow (2.5D platformer/beat-'em-up side view); reads no player input. - `inspection` — model-viewer rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor; orbits a fixed point, reads no player/entity input. - `none` — no camera rig is mounted; use for HUD-only presentations or a game that manages its own camera. +- `CardPile` (interface): interface CardPile — ⚠ undocumented +- `CardPileState` (interface): interface CardPileState — ⚠ undocumented +- `Cell` (type): type Cell = readonly [number, number] — ⚠ undocumented +- `CellGrid` (interface): interface CellGrid — ⚠ undocumented +- `ChaseCameraConfig` (interface): interface ChaseCameraConfig — Speed-reactive vehicle chase rig (#27) — speed→FOV, spring arm, procedural shake, interior views. +- `Chat` (interface): interface Chat — ⚠ undocumented +- `ChatMessage` (interface): interface ChatMessage — ⚠ undocumented +- `ChatRateLimit` (interface): interface ChatRateLimit — ⚠ undocumented +- `ChatSendResult` (type): type ChatSendResult = | { message: ChatMessage; recipients: ChatRecipients } | { reason: string } — ⚠ undocumented +- `CinematicCameraConfig` (interface): interface CinematicCameraConfig — Scripted keyframe / path player (#29). When set it overrides the active rig. +- `CombatTelegraphEvent` (interface): interface CombatTelegraphEvent — ⚠ undocumented +- `CombatVfxEvent` (interface): interface CombatVfxEvent — A transient sprite-particle effect the shell renders once and expires — one burst of `kind`, tinted `color`, anchored at `from` (and `to` for travel/beam effects). +- `CropDef` (interface): interface CropDef — ⚠ undocumented +- `CropTileState` (interface): interface CropTileState — ⚠ undocumented +- `Curve` (type): type Curve = CurveDef & CurveShape — ⚠ undocumented +- `DEFAULT_CHAT_BODY_LENGTH` (const): const DEFAULT_CHAT_BODY_LENGTH: 500 — ⚠ undocumented +- `DEFAULT_CHAT_HISTORY_LIMIT` (const): const DEFAULT_CHAT_HISTORY_LIMIT: 100 — ⚠ undocumented +- `DEFAULT_CHAT_RATE_LIMIT` (const): const DEFAULT_CHAT_RATE_LIMIT: ChatRateLimit — ⚠ undocumented +- `DEFAULT_PICKUP_RADIUS` (const): const DEFAULT_PICKUP_RADIUS: 2 — ⚠ undocumented +- `DEFAULT_PING_CATEGORIES` (const): const DEFAULT_PING_CATEGORIES: Record — Content-agnostic default ping wheel: enemy / loot / location / danger. +- `DEFAULT_TOUCH_STYLE` (const): const DEFAULT_TOUCH_STYLE: TouchStyle — Skin used when neither the game nor the player picks one. +- `DeliveryEntry` (interface): interface DeliveryEntry — ⚠ undocumented +- `DeliveryQueue` (interface): interface DeliveryQueue — ⚠ undocumented +- `DirectionalLightingConfig` (interface): interface DirectionalLightingConfig — ⚠ undocumented +- `Drop` (interface): interface Drop — A resolved loot outcome — one item or currency grant with its rolled count. +- `DurabilitySpec` (interface): interface DurabilitySpec — ⚠ undocumented +- `DurabilityState` (interface): interface DurabilityState — ⚠ undocumented +- `EntityDiedEvent` (interface): interface EntityDiedEvent — ⚠ undocumented +- `EntityFloatTextEvent` (interface): interface EntityFloatTextEvent — ⚠ undocumented +- `EntitySpriteConfig` (interface): interface EntitySpriteConfig — ⚠ undocumented +- `FeedEntry` (interface): interface FeedEntry — ⚠ undocumented +- `FirstPersonCameraConfig` (interface): interface FirstPersonCameraConfig — ⚠ undocumented +- `FriendEntry` (interface): interface FriendEntry — ⚠ undocumented +- `FriendRequestEntry` (interface): interface FriendRequestEntry — ⚠ undocumented +- `Friends` (interface): interface Friends — ⚠ undocumented +- `GameCameraConfig` (interface): interface GameCameraConfig — ⚠ undocumented +- `GameDefinition` (interface): interface GameDefinition — Fully-resolved game description produced by {@link defineGame} — assets, scene, and opted-in subsystems. +- `GameDefinitionConfig` (type): type GameDefinitionConfig = Omit, "scene" | "assets"> & { assets?: AssetCatalog; } — Input to {@link defineGame} — a `GameDefinition` with `scene` derived and `assets` optional. +- `GameEventMap` (interface): interface GameEventMap — ⚠ undocumented +- `GameEvents` (interface): interface GameEvents — ⚠ undocumented +- `GameLoop` (interface): interface GameLoop — Lifecycle hooks a game implements to drive init, per-tick simulation, and player join/leave. +- `GamePhase` (type): type GamePhase = "menu" | "playing" | "paused" | "ended" — Canonical run phase every game moves through. `menu` (title/main menu), `playing` (live), `paused` (mid-run pause), `ended` (win/lose/results). Touch controls are shown only while `playing`; menus and results never paint the touch dock over themselves. +- `InspectionCameraConfig` (interface): interface InspectionCameraConfig — Model-viewer / inspection rig (#207.7) — orbit + pan + anchored zoom around a fixed point, never reads player input. +- `InspectionZoomAnchor` (type): type InspectionZoomAnchor = "target" | "cursor" | "center" — How scroll-zoom re-anchors the view for the inspection rig (#207.7): - `target` — dolly toward the orbit target (classic OrbitControls behavior). - `cursor` — dolly toward the point under the pointer. - `center` — dolly toward the viewport center; equivalent to `target` for an OrbitControls-driven rig, since the camera always faces `target` and that point already projects to the exact center of the viewport. +- `InstalledPart` (interface): interface InstalledPart — ⚠ undocumented +- `InventoryDeclaration` (interface): interface InventoryDeclaration — Shape of one named inventory a game declares — slot count, accepted item types, HUD binding. +- `InventorySlot` (type): type InventorySlot = { itemId: string; count: number } | null — ⚠ undocumented +- `InventoryState` (interface): interface InventoryState — ⚠ undocumented +- `ItemUseHandler` (interface): interface ItemUseHandler — ⚠ undocumented +- `ItemUseInput` (interface): interface ItemUseInput — ⚠ undocumented +- `KeyValueStorage` (interface): interface KeyValueStorage — Structural, DOM-free storage backend: the browser `localStorage` satisfies it, as does a test stub or `null`. The one storage seam core primitives target so persistence code never needs the DOM `Storage` lib. +- `LaneRule` (interface): interface LaneRule — ⚠ undocumented +- `LeaderboardRow` (interface): interface LeaderboardRow — ⚠ undocumented +- `LeaderboardScope` (type): type LeaderboardScope = "global" | "server" | "profile" — ⚠ undocumented +- `LevelProgress` (interface): interface LevelProgress — ⚠ undocumented +- `LevelSequence` (interface): interface LevelSequence — ⚠ undocumented +- `LevelingConfig` (interface): interface LevelingConfig — ⚠ undocumented +- `LevelingTrack` (interface): interface LevelingTrack — ⚠ undocumented +- `LifecycleConfig` (interface): interface LifecycleConfig — Declarative start/restart run lifecycle: the state transitions a game's run phase every genre repeats (title screen → live run → live run → title screen again), expressed as pure functions over one typed {@link StoreHandle} slot instead of hand-rolled `commands.define("start"/"restart")` glue that re-derives phase after every mutation. `start`/`restart` receive the store's own value type — the store's `TState`, never `ctx.game.store.get(key) as T` — and return the next value; the runtime writes it back and derives {@link GamePhase} from it via `phaseOf` in one place, so every adopting game gets identical, correct phase-sync for free. +- `LightingConfig` (interface): interface LightingConfig — Declarative lighting replacing the shell's hardcoded ambient/directional default (#207.5); mounts regardless of world kind, only when supplied. +- `Listing` (interface): interface Listing — One active post in a {@link ListingBook}: an item stack a seller offered at a fixed price until it expires. +- `LoadoutDef` (interface): interface LoadoutDef — ⚠ undocumented +- `LockOnCameraConfig` (interface): interface LockOnCameraConfig — Lock-on / strafe rig (#26) — yaw bound to player→target, move axis becomes strafe. +- `LootFilterRule` (interface): interface LootFilterRule — ⚠ undocumented +- `LootTableDef` (interface): interface LootTableDef — A named, validated loot table — its roll count, weighted-vs-independent mode, and candidate entries. +- `ModelConfig` (interface): interface ModelConfig — ⚠ undocumented +- `ModelMaterialMaps` (interface): interface ModelMaterialMaps — Real PBR map URLs (e.g. `buildMaterialCatalog(...).resolve(id)!.maps` from `@jgengine/assets`) layered onto a model's material — the seam for texturing an otherwise-flat/untextured GLB. Any role may be omitted to keep the model's own map. +- `ModelMaterialOverride` (interface): interface ModelMaterialOverride — Per-entity PBR material override (#151.3) applied to every `MeshStandardMaterial` in the model's cloned scene graph. +- `ModularItemDef` (interface): interface ModularItemDef — ⚠ undocumented +- `MountSlotDef` (interface): interface MountSlotDef — ⚠ undocumented +- `NEUTRAL_AXIS` (const): const NEUTRAL_AXIS: AxisInput — ⚠ undocumented +- `ObjectStyle` (interface): interface ObjectStyle — ⚠ undocumented +- `ObserverCameraConfig` (interface): interface ObserverCameraConfig — Detached spectator/photo cam (#120) — binds to any entity or fixed point, never reads player input. +- `PING_FEED_ACTION` (const): const PING_FEED_ACTION: "party.ping" — ⚠ undocumented +- `PartDef` (interface): interface PartDef — ⚠ undocumented +- `Party` (interface): interface Party — ⚠ undocumented +- `PartyInviteEntry` (interface): interface PartyInviteEntry — ⚠ undocumented +- `PartyMemberEntry` (interface): interface PartyMemberEntry — ⚠ undocumented +- `PhysicsConfig` (interface): interface PhysicsConfig — World gravity and jump tuning, plus scene-object collision opt-ins, for the game's physics step. +- `PingCategory` (type): type PingCategory = string — ⚠ undocumented +- `PingSystem` (interface): interface PingSystem — ⚠ undocumented +- `PlayableGame` (interface): interface PlayableGame — ⚠ undocumented +- `PointerAxisState` (interface): interface PointerAxisState — ⚠ undocumented +- `PointerConfig` (interface): interface PointerConfig — ⚠ undocumented +- `PointerHit` (interface): interface PointerHit — Renderer-free result of a screen→world raycast. The shell's pointer service produces this from the cursor; core-side gameplay (item.use aim, click-to-move, ground-target abilities, pings) consumes it without touching three.js. +- `PointerVec3` (type): type PointerVec3 = readonly [number, number, number] — ⚠ undocumented +- `PresenceInfo` (interface): interface PresenceInfo — ⚠ undocumented +- `QuestDef` (interface): interface QuestDef — ⚠ undocumented +- `QuestInstance` (interface): interface QuestInstance — ⚠ undocumented +- `QuestRewards` (interface): interface QuestRewards — ⚠ undocumented +- `RaceState` (class): class RaceState — Race state machine (issue #87). Drive it each tick with `update(now, positions)` — `now` is game time (`ctx.time`), `positions` maps each racer to a world point tested against the ordered checkpoint volumes. It emits `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished`, keeps cumulative split times for PB deltas, resolves a pluggable win condition (first-past-post, round-cut, derby last-standing), and `resetToCheckpoint` hands back a respawn pose at the racer's last checkpoint. `removeRacer` drops a racer mid-race and `reset` returns the whole instance to its pre-race state for reuse. +- `RarityStyle` (interface): interface RarityStyle — ⚠ undocumented +- `RecipeDef` (interface): interface RecipeDef — ⚠ undocumented +- `RecipeItem` (interface): interface RecipeItem — ⚠ undocumented +- `Ring` (interface): interface Ring — ⚠ undocumented +- `RingConfig` (interface): interface RingConfig — ⚠ undocumented +- `RingPhase` (interface): interface RingPhase — ⚠ undocumented +- `RoleSpec` (interface): interface RoleSpec — ⚠ undocumented +- `Rotation` (type): type Rotation = 0 | 1 | 2 | 3 — ⚠ undocumented +- `RoundConfig` (interface): interface RoundConfig — ⚠ undocumented +- `RoundSnapshot` (interface): interface RoundSnapshot — ⚠ undocumented +- `RtsCameraConfig` (interface): interface RtsCameraConfig extends TopDownCameraConfig — Free-pan / edge-scroll RTS rig (#24) — pan/rotate/zoom independent of any avatar. +- `RunDraft` (interface): interface RunDraft — ⚠ undocumented +- `RunModifierOffer` (interface): interface RunModifierOffer — ⚠ undocumented +- `SaveBackend` (interface): interface SaveBackend — The one async storage seam a save store persists through. Every backend satisfies this same three-method shape — the browser's `localStorage` (offline), an in-memory map (tests/SSR), or a database/Convex/HTTP endpoint (cloud) — so a game switches offline saves for cloud saves by swapping the backend and changing nothing else. Keys are opaque namespaced strings; values are already-serialized strings, so a backend never needs to know the save shape. +- `SaveStatus` (type): type SaveStatus = "idle" | "loading" | "saving" | "saved" | "error" — Lifecycle of the last save/load — drive a "Saving…"/"Saved" indicator or a loading gate off it. `"error"` means the backend rejected a read or write. +- `SaveStore` (interface): interface SaveStore — A pluggable-backend game save with autosave, named slots, and versioned migration. `value()`/`patch()` hold the live state; `load()` hydrates it from the backend; `save()` (or autosave) writes it back. Backend failures surface as `"error"` status and through `onError` — a save never throws into a tick. +- `ScheduledDelivery` (interface): interface ScheduledDelivery — ⚠ undocumented +- `ShapeTable` (type): type ShapeTable = Record< TShape, readonly (readonly (readonly [number, number])[])[] > — ⚠ undocumented +- `ShoulderCameraConfig` (interface): interface ShoulderCameraConfig — Over-the-shoulder combat rig (#25) — offset, ADS, shoulder swap, decoupled reticle. +- `SideScrollCameraConfig` (interface): interface SideScrollCameraConfig — Fixed lateral 2.5D follow (side-on platformer cam): the camera sits perpendicular to the travel axis, tracks the followed entity, and never reads player look input. +- `SlotGrid` (type): type SlotGrid = readonly Slot[] — ⚠ undocumented +- `Social` (interface): interface Social — ⚠ undocumented +- `SocialDeps` (interface): interface SocialDeps — ⚠ undocumented +- `StatLevelUpEvent` (interface): interface StatLevelUpEvent — ⚠ undocumented +- `TOUCH_STYLES` (const): const TOUCH_STYLES: readonly TouchStyle[] — Every touch skin id, in menu order. +- `TOUCH_STYLE_OPTIONS` (const): const TOUCH_STYLE_OPTIONS: readonly { value: TouchStyle; label: string }[] — Touch skins as `{ value, label }` rows for the Settings → Controls selector. +- `TalentNodeDef` (interface): interface TalentNodeDef — ⚠ undocumented +- `TalentTree` (interface): interface TalentTree — ⚠ undocumented +- `TechNodeDef` (interface): interface TechNodeDef extends UnlockDef — ⚠ undocumented +- `Toast` (interface): interface Toast — A transient HUD message that expires on its own — banner, pickup note, alert. +- `TopDownCameraConfig` (interface): interface TopDownCameraConfig — Fixed top-down / isometric rig (#23) — height/pitch/yaw + decoupled follow. +- `TouchAnchor` (type): type TouchAnchor = | "bottom-left" | "bottom-center" | "bottom-right" | "left" | "right" | "top-left" | "top-center" | "top-right" — Screen zone a touch cluster or button docks to. The four corners plus the mid `left`/`right` rails (vertical stacks, MMO-style hotbars) and the `bottom-center` / `top-center` strips let controls use the whole viewport instead of piling into one bottom bar. +- `TouchButton` (interface): interface TouchButton — ⚠ undocumented +- `TouchButtonShape` (type): type TouchButtonShape = "circle" | "square" | "pedal" | "lever" | "trigger" | "wheel" | "tab" — Physical silhouette a touch button wears. The capture layer draws each as its own shape — a `pedal` reads as a foot pedal, a `lever` as a pull handle, a `trigger` as a firing paddle — so a control looks like the thing it does instead of a labelled circle. `circle`/`square` are the neutral fallbacks. +- `TouchJoystick` (interface): interface TouchJoystick — ⚠ undocumented +- `TouchScheme` (interface): interface TouchScheme — ⚠ undocumented +- `TouchStyle` (type): type TouchStyle = "glass" | "arcade" | "mechanical" | "minimal" — Player-selectable skin for the whole touch layer. A style is a material + geometry preset (not just colours), chosen in Settings → Controls and persisted; `glass` is the translucent default, the rest are opt-in looks. +- `TurnLoop` (interface): interface TurnLoop — ⚠ undocumented +- `UnlockDef` (interface): interface UnlockDef — ⚠ undocumented +- `VfxKind` (type): type VfxKind = "projectile" | "beam" | "nova" | "glow" | "spark" — The visual archetype of a spell/ability effect burst: a traveling bolt, a connecting beam, an expanding ground nova, a soft aura glow, or a scattering impact spark. +- `WORLD_ITEM_ENTITY_NAME` (const): const WORLD_ITEM_ENTITY_NAME: "world_item" — Scene-entity catalog name every dropped-item instance spawns under (see the three buckets: worldItem is an entity, never an inventory item or object). +- `WorldInvite` (interface): interface WorldInvite extends WorldInviteTarget — ⚠ undocumented +- `WorldInviteTarget` (interface): interface WorldInviteTarget — ⚠ undocumented +- `WorldItemRecord` (interface): interface WorldItemRecord — ⚠ undocumented +- `WorldItemRenderConfig` (interface): interface WorldItemRenderConfig — ⚠ undocumented +- `WorldOverlayProps` (interface): interface WorldOverlayProps — Props handed to a `WorldOverlay` component (#542): explicit `ctx` access so canvas-layer VFX read live engine state directly, without an extra hook or a module-global workaround. +- `advanceTransport` (function): function advanceTransport(path: TransportPath, items: readonly TransportItem[], dt: number): { items: TransportItem[]; delivered: TransportItem[] } — ⚠ undocumented +- `aimToPoint` (function): function aimToPoint(origin: PointerVec3, point: PointerVec3): Aim — Build an `origin → point` aim for `item.use` / projectiles, firing toward the cursor. +- `appendToast` (function): function appendToast(toasts: readonly Toast[], toast: Toast, cap: number): readonly Toast[] — Append `toast`, keeping only the newest `cap` entries. +- `applyBindingOverrides` (function): function applyBindingOverrides(input: ActionCodesMap, overrides: BindingOverrides): ActionCodesMap — Merge player rebinds over a game's authored `input` map. Only actions the game already declares can be overridden; unknown override keys are ignored so a stale localStorage entry can't inject phantom actions. +- `applyWear` (function): function applyWear(state: DurabilityState, amount: number): DurabilityState — Apply wear to an item, tracking breakage and repair eligibility. +- `balance` (function): function balance(state: WalletState, currency: string): number — ⚠ undocumented +- `canCraft` (function): function canCraft(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, recipe: RecipeDef, context: CraftContext = {}): CraftCheck — ⚠ undocumented +- `charge` (function): function charge(state: WalletState, currency: string, amount: number, options?: ChargeOptions): ChargeResult — Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft` opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability check stays the default with `options` omitted. +- `chargeAll` (function): function chargeAll(state: WalletState, costs: Readonly>, options?: ChargeOptions): ChargeResult — ⚠ undocumented +- `clearBindingOverride` (function): function clearBindingOverride(gameId: string, action: string, storage: Pick | null | undefined = defaultStorage()): BindingOverrides — ⚠ undocumented +- `computeEffectiveStats` (function): function computeEffectiveStats(def: ModularItemDef, installed: readonly InstalledPart[]): Record — ⚠ undocumented +- `craft` (function): function craft(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, recipe: RecipeDef, context: CraftContext = {}): CraftResult — ⚠ undocumented +- `craftSeconds` (function): function craftSeconds(recipe: RecipeDef): number — ⚠ undocumented +- `createAffixRoller` (function): function createAffixRoller(config: RollerConfig): AffixRoller — ⚠ undocumented +- `createBehaviourWorld` (function): function createBehaviourWorld(): BehaviourWorld — ⚠ undocumented +- `createCardPile` (function): function createCardPile(config: CardPileConfig, initial?: Partial>): CardPile — ⚠ undocumented +- `createCardPileState` (function): function createCardPileState(config: CardPileConfig, initial?: Partial>): CardPileState — ⚠ undocumented +- `createChatRateLimiter` (function): function createChatRateLimiter(limit: ChatRateLimit): ChatRateLimiter — ⚠ undocumented +- `createCommitController` (function): function createCommitController(config: CommitControllerConfig): CommitController — ⚠ undocumented +- `createCosmetics` (function): function createCosmetics(deps: CosmeticsDeps = {}): Cosmetics — Equip cosmetic skins and customizations by slot, independent of gameplay stats. +- `createDeliveryQueue` (function): function createDeliveryQueue(): DeliveryQueue — ⚠ undocumented +- `createDurability` (function): function createDurability(spec: DurabilitySpec): DurabilityState — ⚠ undocumented +- `createDurabilityTracker` (function): function createDurabilityTracker(): DurabilityTracker — ⚠ undocumented +- `createEmptyWallet` (function): function createEmptyWallet(): WalletState — Hold per-currency balances with affordability checks and charge/grant operations. +- `createGameDialogue` (function): function createGameDialogue(store: DialogueStore): GameDialogue — Build a {@link GameDialogue} over one keyed-store slot. Writes flow through the reactive store, so opening or closing bumps `ctx.version()` and a `useOpenDialogueId` selector re-renders. +- `createGameEvents` (function): function createGameEvents(): GameEvents — A typed publish/subscribe bus for gameplay events that systems and HUDs subscribe to. +- `createGameFeed` (function): function createGameFeed(options?: GameFeedOptions): GameFeed — A rolling per-action feed of recent gameplay events, bindable to the event bus — the HUD ticker and killfeed history. +- `createGestureSurfaceTracker` (function): function createGestureSurfaceTracker(bindings: TouchGestureBindings, tuning: GestureSurfaceTuning = DEFAULT_GESTURE_TUNING): GestureSurfaceTracker — ⚠ undocumented +- `createIntentBoard` (function): function createIntentBoard(): IntentBoard — ⚠ undocumented +- `createItemInstanceRegistry` (function): function createItemInstanceRegistry(prefix = "item"): ItemInstanceRegistry — Builds an {@link ItemInstanceRegistry}; generated ids are `"::"`, unique per registry instance. +- `createItemUse` (function): function createItemUse(resolveUse: (itemId: string) => string | null | undefined): ItemUse — Use or consume items, applying their effects and per-item cooldowns. +- `createKeyValueStore` (function): function createKeyValueStore(config: KeyValueStoreConfig): KeyValueStore — A lightweight mutable local save cell for single-player state (a credit bank, a settings blob, level progress) — the read-modify-write counterpart to the monotonic `recordBook`. Persists through a {@link KeyValueStorage} (browser `localStorage` by default); corrupt or unavailable storage degrades to in-memory and never throws into a game tick. +- `createLapTimer` (function): function createLapTimer(): LapTimer — Create a {@link LapTimer} starting at lap 0 with no splits, best, or last time recorded. +- `createLeaderboard` (function): function createLeaderboard(sink?: { onIncrement?(row: LeaderboardRow): void }): Leaderboard — Ranked score tracking across global, server, and per-profile scopes, with top-N queries and per-profile lookups. +- `createLevelSequence` (function): function createLevelSequence(config: LevelSequenceConfig): LevelSequence — A pure, deterministic level campaign: an ordered list of levels, each with its own opaque config, played through a `start` → (`clear` → `advance`)* → `complete` happy path, with `fail`/`retry` handling per-level attempts. Mirrors the reducer style of `game/race.ts` and `ai/spawnDirector.ts` — no I/O, no timers, just state transitions driven by the caller. +- `createListingBook` (function): function createListingBook(config: ListingBookConfig): ListingBook — A player-driven listing marketplace: post/cancel/buy against a shared book with a house cut on every sale, an expiry sweep that pulls unsold goods out of circulation, and a per-seller collection box holding sale proceeds and returned items until claimed. Buyer/seller wallet and inventory movement is the caller's job (mirrors `game/trade`'s split) — this primitive owns only the listing lifecycle and the escrowed collection-box bookkeeping behind it. +- `createLoadouts` (function): function createLoadouts(deps: LoadoutDeps): Loadouts — Save, name, and swap equipment loadouts. +- `createLootRegistry` (function): function createLootRegistry(): LootRegistry — Register named loot tables and roll weighted randomized drops from them. +- `createModularItem` (function): function createModularItem(def: ModularItemDef, initial: readonly InstalledPart[] = []): ModularItem — ⚠ undocumented +- `createNameGenerator` (function): function createNameGenerator(options: NameGeneratorOptions): NameGenerator — Generate procedural names from templates and word banks with an injected random source. +- `createPingSystem` (function): function createPingSystem(deps: PingSystemDeps): PingSystem — Contextual ping/marker communication between teammates, classified by what was pinged. +- `createProductionState` (function): function createProductionState(): ProductionState — A production building that converts input items into outputs over time — factory/crafting station. +- `createQuestJournal` (function): function createQuestJournal(deps: QuestJournalDeps): QuestJournal — Track accepted quests and their per-objective progress, granting rewards on completion. +- `createRaceState` (function): function createRaceState(config: RaceStateConfig): RaceState — A checkpoint race state machine — laps, forks, live standings, splits, and pluggable win conditions. +- `createRecipeGraph` (function): function createRecipeGraph(defs: readonly RecipeDef[] = []): RecipeGraph — ⚠ undocumented +- `createRecordBook` (function): function createRecordBook(config: RecordBookConfig): RecordBook — A personal-best record book: named numeric fields each racing toward "lower" (times) or "higher" (scores, streaks), persisted through a structural key-value storage (pass `localStorage` in a browser, a stub in tests, or `null` for in-memory only). Corrupt or unavailable storage degrades to an empty book — a record write never throws into a game tick. +- `createRing` (function): function createRing(config: RingConfig): Ring — ⚠ undocumented +- `createRunDraft` (function): function createRunDraft(config: RunDraftConfig): RunDraft — A roguelike run built from stacking drafted modifier picks that reshape the run. +- `createSaveStore` (function): function createSaveStore(config: SaveStoreConfig): SaveStore — Create a {@link SaveStore}. Same call for offline and cloud — only the `backend` differs (localStorage, memory, or an async DB/Convex endpoint). Turn on `autosave` and every `set`/`patch` persists on a debounce; leave it off and call `save()` at checkpoints. Bump `version` + pass `migrate` when the save shape changes so old players keep their progress. +- `createSocial` (function): function createSocial(deps: SocialDeps): Social — Emotes and lightweight social interactions between nearby players. +- `createSpawnPoints` (function): function createSpawnPoints(): SpawnPoints — Register spawn locations and choose where entities spawn or respawn. +- `createTalentTree` (function): function createTalentTree(config: TalentTreeConfig): TalentTree — ⚠ undocumented +- `createToastQueue` (function): function createToastQueue(options: ToastQueueOptions = {}): ToastQueue — A capped, self-expiring toast queue — the append-with-limit plus TTL-prune list every HUD hand-rolled on top of a plain array. Feed it game time: `push` raises a message, `prune(now)` drops expired ones, `list()` is what the HUD renders. Unlike the append-only event feed, toasts evict themselves. +- `createTouchGestureTracker` (function): function createTouchGestureTracker(tuning: TouchGestureTuning): TouchGestureTracker — ⚠ undocumented +- `createTurnLoop` (function): function createTurnLoop(config: TurnLoopConfig): TurnLoop — ⚠ undocumented +- `createUnlockCatalog` (function): function createUnlockCatalog(defs: readonly UnlockDef[] = []): UnlockCatalog — A catalog of unlockable content gated behind conditions the player earns, tracking what is unlocked. +- `createUnlocks` (function): function createUnlocks(defs: UnlockDef[] = []): Unlocks — ⚠ undocumented +- `createWeaponStats` (function): function createWeaponStats(resolveEntry: (itemId: string) => WeaponEntry | null | undefined): WeaponStats — Resolve per-weapon stat values — damage, fire rate, spread — for combat math. +- `curve` (function): function curve(spec: Curve): (x: number) => number — ⚠ undocumented +- `defineGame` (function): function defineGame(config: GameDefinitionConfig): GameDefinition — Task-first entry point for authoring a game: fills in `scene` and default `assets`, validates `name`. +- `deriveTouchScheme` (function): function deriveTouchScheme(input: ActionCodesMap | undefined, { reserved, firstPerson, config }: DeriveTouchSchemeOptions): TouchScheme | null — Null means "render no touch controls" — either the game opted out or there is nothing to synthesize. +- `dialogueSlot` (const): const dialogueSlot: StoreHandle — Typed handle onto the open-dialogue slot — React reads it via `useOpenDialogueId`; game code uses `ctx.game.dialogue`. +- `drainOutput` (function): function drainOutput(state: ProductionState, itemId: string, count?: number): { state: ProductionState; taken: number } — ⚠ undocumented +- `draw` (function): function draw(state: CardPileState, n: number, options: { from: ZoneName; to: ZoneName; handLimit?: number; reshuffleFrom?: ZoneName; seed?: string | number; }): DrawResult — ⚠ undocumented +- `durabilityFraction` (function): function durabilityFraction(state: DurabilityState): number — ⚠ undocumented +- `evalCurve` (function): function evalCurve(spec: Curve, x: number): number — ⚠ undocumented +- `evaluateLootFilter` (function): function evaluateLootFilter(rules: readonly LootFilterRule[], item: LootFilterItem): LootFilterOverride — First matching rule wins (PoE/Last Epoch block semantics) — later rules never override an earlier match. Returns overrides only; fields the rule doesn't set are left for the caller's baseline (rarity style) to fill in. +- `evaluateObjective` (function): function evaluateObjective(objective: ThresholdObjective, value: number): ObjectiveStatus — Evaluate a single live-metric objective: is `value` at least (or at most) the target, and how far along. Unlike an event counter, this reads a continuously-changing metric — population, approval, pollution — the objective shape city-builders and management sims track every tick. +- `feedProduction` (function): function feedProduction(def: ProductionBuildingDef, state: ProductionState, itemId: string, count: number): { state: ProductionState; accepted: number } — ⚠ undocumented +- `finishRaceSession` (function): function finishRaceSession(session: RaceSessionState): RaceSessionState — Cross the flag: move a `racing` session to `finished`, freezing its `elapsed`. A no-op in any other phase. +- `firstPastPost` (function): function firstPastPost(count = 1): RaceWinCondition — Race ends when `count` racers have crossed the finish; ranking is the current standings order. +- `gamePhase` (function): function gamePhase(ctx: GameContext): GamePhase — Current phase; defaults to `playing` when unset so always-live games need no wiring. +- `grant` (function): function grant(state: WalletState, currency: string, amount: number): WalletState — ⚠ undocumented +- `idleRaceSession` (function): function idleRaceSession(): RaceSessionState — The pre-race session on the grid: `idle`, both clocks at zero. Call {@link startRaceCountdown} to light the lights, or hold here until the field is ready. +- `install` (function): function install(def: ModularItemDef, installed: readonly InstalledPart[], slotId: string, part: PartDef): InstallResult — ⚠ undocumented +- `insureLost` (function): function insureLost(lost: readonly ItemStack[], policy: InsurancePolicy, userId: string, now: number, rng: () => number = Math.random): ScheduledDelivery | null — ⚠ undocumented +- `isComplete` (function): function isComplete(def: ModularItemDef, installed: readonly InstalledPart[]): boolean — ⚠ undocumented +- `isDisabled` (function): function isDisabled(spec: DurabilitySpec, state: DurabilityState): boolean — ⚠ undocumented +- `isOverdrawn` (function): function isOverdrawn(state: WalletState, currency: string): boolean — True once `balance(state, currency)` has gone negative under an overdraft-enabled charge. +- `lapDurations` (function): function lapDurations(splits: readonly number[], gatesPerLap: number): number[] — Per-lap durations from a cumulative split book with `gatesPerLap` checkpoints per lap — each lap's time is its finish-gate split minus the previous lap's finish. Only complete laps are returned. +- `leveling` (function): function leveling(config: LevelingConfig): LevelingTrack — ⚠ undocumented +- `loadBindingOverrides` (function): function loadBindingOverrides(gameId: string, storage: Pick | null | undefined = defaultStorage()): BindingOverrides — ⚠ undocumented +- `localSaveBackend` (function): function localSaveBackend(storage?: KeyValueStorage | null): SaveBackend — A {@link SaveBackend} over a synchronous {@link KeyValueStorage} — the browser's `localStorage` by default (offline, on-device saves), a test stub, or `null` for memory-only. Storage errors (quota exceeded, private mode, no DOM) degrade to no-ops, so a save never throws into a game tick. +- `lootFilter` (function): function lootFilter(rules: readonly LootFilterRule[]): readonly LootFilterRule[] — Validating factory — rule ids must be unique so authoring mistakes fail loudly. +- `lootTable` (function): function lootTable(def: LootTableDef): LootTableDef — Validates a loot table definition and returns it unchanged, for use with {@link createLootRegistry}. +- `memorySaveBackend` (function): function memorySaveBackend(): SaveBackend — A memory-only {@link SaveBackend} — saves survive a reload only within the same session. For tests, SSR, or a "no persistence" mode that still exercises the same save code path. +- `missingRequiredSlots` (function): function missingRequiredSlots(def: ModularItemDef, installed: readonly InstalledPart[]): string[] — ⚠ undocumented +- `moveCards` (function): function moveCards(state: CardPileState, ids: readonly string[], from: ZoneName, to: ZoneName, position: "top" | "bottom" = "top"): PileResult — ⚠ undocumented +- `normalizePointerToAxis` (function): function normalizePointerToAxis(clientX: number, clientY: number, rect: PointerSurfaceRect): PointerAxisState — Normalize client coordinates against a surface rect into a `PointerAxisState`, clamped to `[-1, 1]` per axis. +- `parDelta` (function): function parDelta(splits: readonly number[], reference: readonly number[]): number[] — Elementwise delta of a cumulative split book against a `reference` book (a personal best or par lap): positive means behind the reference at that checkpoint. Compared up to the shorter length — the `+0.3s` / `−1.2s` gap every racing HUD shows against its ghost. +- `partInSlot` (function): function partInSlot(installed: readonly InstalledPart[], slotId: string): PartDef | null — ⚠ undocumented +- `partitionOnDeath` (function): function partitionOnDeath(containers: readonly ContainerSnapshot[]): DeathPartition — ⚠ undocumented +- `peek` (function): function peek(state: CardPileState, zone: ZoneName, n = 1): readonly string[] — ⚠ undocumented +- `pickUniform` (function): function pickUniform(rng: () => number, items: readonly T[]): T | undefined — Pick one item uniformly at random from `items` using `rng` (a `() => number` in `[0, 1)`); returns undefined when empty. +- `pickWeighted` (function): function pickWeighted(rng: () => number, items: readonly T[], weightOf: (item: T) => number): T | undefined — Pick one item with probability proportional to `weightOf(item)`; skips non-positive weights, returns undefined when nothing is eligible. +- `pileRng` (function): function pileRng(seed: string | number): () => number — ⚠ undocumented +- `placementOf` (function): function placementOf(finishOrder: readonly string[], racerId: string, options?: PlacementOptions): RacePlacement | null — One racer's {@link RacePlacement} within a finish order, or `null` if they never crossed the line. +- `playControlsActive` (function): function playControlsActive(ctx: GameContext): boolean — ⚠ undocumented +- `proceduralLootEntry` (function): function proceduralLootEntry(registry: ItemInstanceRegistry, roll: (rng: () => number) => { baseId: string; def: TDef }): (rng: () => number) => string — Bridges any procedural roller into a `LootEntry.generate` callback: rolls a `{ baseId, def }` pair and registers it, returning the runtime id the loot roll hands back as the drop's `item`. +- `productionBuilding` (function): function productionBuilding(config: ProductionBuildingConfig): ProductionBuildingDef — ⚠ undocumented +- `pruneToasts` (function): function pruneToasts(toasts: readonly Toast[], now: number): readonly Toast[] — Drop every toast whose `expiresAt` is at or before `now`. Returns the same array when nothing expired. +- `raceOutcomeOf` (function): function raceOutcomeOf(finishOrder: readonly string[], racerId: string, options?: PlacementOptions): RaceOutcome — The win/lose verdict for one racer in a finish order — `ranking[0] === player ? "win" : "lose"`, the check every racing game hand-rolls, generalized to a `winningPlaces` cutoff. A racer absent from the order counts as a `lose`. +- `racePlacements` (function): function racePlacements(finishOrder: readonly string[], options?: PlacementOptions): readonly RacePlacement[] — Turn a finish-order ranking (index 0 = winner, e.g. the `ranking` of a `race.finished` event) into per-racer {@link RacePlacement}s — the `1st/2nd/3rd` + win/lose every results screen shows. +- `raceTrack` (function): function raceTrack(config: RaceTrackConfig): RaceTrack — A race track is an ordered ring of checkpoint trigger volumes plus a lap count. The final checkpoint is the lap/finish line: a racer completes a lap by passing all checkpoints in order and hitting the last one. `forks` splice alternate route segments between mainline checkpoints. +- `remoteSaveBackend` (function): function remoteSaveBackend(backend: SaveBackend): SaveBackend — Adopt any async `read`/`write`/`remove` trio as a {@link SaveBackend} — the seam for cloud saves backed by a database, an HTTP endpoint, or Convex (see `@jgengine/convex/convexSaveBackend`). Reads/writes may reject; the save store surfaces the failure as `"error"` status instead of throwing. +- `repairQuote` (function): function repairQuote(spec: DurabilitySpec, state: DurabilityState, options?: { to?: number; station?: string }): RepairQuote | null — ⚠ undocumented +- `resolveConsolation` (function): function resolveConsolation(policy: ConsolationPolicy, partition: DeathPartition): { loadoutId: string } | null — ⚠ undocumented +- `resolveOneShotClip` (function): function resolveOneShotClip(oneShots: Record | undefined, event: string, roll: number): string | null — Resolves the clip name a one-shot `event` should play from a model's `animation.oneShots` map, or `null` if the event isn't bound. A `string[]` binding picks a variant by `roll` (a value in `[0, 1)`), so combat can vary attack swings. Pure and deterministic given `roll` — the shell supplies the randomness. +- `resolvePowerGrid` (function): function resolvePowerGrid(supply: number, consumers: readonly PowerConsumer[]): PowerGridResult — ⚠ undocumented +- `ringSampleAt` (function): function ringSampleAt(config: RingConfig, time: number): RingSample — ⚠ undocumented +- `runPipeline` (function): function runPipeline(base: V, modifiers: readonly Modifier[], equals: (a: V, b: V) => boolean = Object.is): PipelineResult — ⚠ undocumented +- `saveBindingOverride` (function): function saveBindingOverride(gameId: string, action: string, codes: ActionCodes, storage: Pick | null | undefined = defaultStorage()): BindingOverrides — ⚠ undocumented +- `seededRng` (function): function seededRng(seed: string | number): () => number — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence. +- `seededStreams` (function): function seededStreams(seed: string | number): (stream: string) => () => number — Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name. +- `setGamePhase` (function): function setGamePhase(ctx: GameContext, phase: GamePhase): void — Set the current phase. Publishes it to `ctx.game.store` (React reads it via `useGamePhase`) and gates the shell's on-screen touch controls in one call — `playing` shows them, every other phase hides them. This is the whole "main menu shouldn't show touch controls" wiring: call it once per phase transition and the dock follows. +- `shuffleWithRng` (function): function shuffleWithRng(values: readonly T[], rng: () => number): T[] — ⚠ undocumented +- `slotAccepts` (function): function slotAccepts(slot: MountSlotDef, category: string): boolean — Attach parts into an item's mount slots and resolve the combined stats. +- `splitSegments` (function): function splitSegments(splits: readonly number[], start = 0): number[] — Per-segment durations from a cumulative split book (`splits[i]` = elapsed time at checkpoint `i`): `segments[i] = splits[i] − splits[i−1]`, the first measured from `start` (default 0). Turns the cumulative splits {@link RacerProgress} records into the individual leg times a results screen shows. +- `startRaceCountdown` (function): function startRaceCountdown(options?: RaceCountdownOptions): RaceSessionState — Drop the lights: return a fresh `countdown` session of `seconds` (default 3). A non-positive length skips straight to `racing` for a standing start with no countdown. +- `stationSatisfied` (function): function stationSatisfied(recipe: RecipeDef, context: CraftContext): boolean — ⚠ undocumented +- `tickProduction` (function): function tickProduction(def: ProductionBuildingDef, state: ProductionState, input: ProductionTickInput): ProductionState — ⚠ undocumented +- `tickRaceSession` (function): function tickRaceSession(session: RaceSessionState, dt: number): RaceSessionState — Advance the session by `dt` seconds: bleed the countdown down and flip to `racing` when it reaches zero, or accumulate `elapsed` while `racing`. `idle` and `finished` are inert. Overshoot past the countdown is dropped rather than banked into `elapsed`, so the race clock always starts from zero. +- `touchButtonShape` (function): function touchButtonShape(action: string): TouchButtonShape — Default silhouette for an action; `circle` when nothing more specific fits. +- `touchCode` (function): function touchCode(action: string): string — ⚠ undocumented +- `uninstall` (function): function uninstall(installed: readonly InstalledPart[], slotId: string): readonly InstalledPart[] — ⚠ undocumented +- `wear` (function): function wear(spec: DurabilitySpec, state: DurabilityState, kind: WearKind, times = 1): DurabilityState — ⚠ undocumented +- `withTouchCodes` (function): function withTouchCodes(map: ActionCodesMap | undefined): ActionCodesMap — Every action gains a synthetic touch code alongside its physical codes. +- `worldHealthBarAllowsRole` (function): function worldHealthBarAllowsRole(roles: readonly CatalogEntityRole[] | undefined, role: CatalogEntityRole | undefined): boolean — ⚠ undocumented + ## @jgengine/core/meta/changelog - `CHANGELOG` (const): const CHANGELOG: Record — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`). - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md). - `VERSION` (const): const VERSION: "0.10.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. +## @jgengine/core/multiplayer + +- `AuthSession` (interface): interface AuthSession — ⚠ undocumented +- `BoardSnapshot` (interface): interface BoardSnapshot — ⚠ undocumented +- `ChatActions` (interface): interface ChatActions — ⚠ undocumented +- `ChatSendOutcome` (interface): interface ChatSendOutcome — ⚠ undocumented +- `ChatSync` (interface): interface ChatSync — Callback seam for backends that cannot host React hooks (e.g. the ws client): subscribe delivers the channel's recent history on every change; send resolves with the host's verdict. +- `ChatTransport` (interface): interface ChatTransport — Backend seam for remote text chat, mirroring PresenceTransport: the use* members are called as React hooks by consumers, so a mounted transport must never change identity — remount the subtree to switch backends. useMessages returns undefined while the subscription is loading and the channel's recent history once live. +- `EnsurePresenceResult` (interface): interface EnsurePresenceResult — ⚠ undocumented +- `FeedWriteGate` (type): type FeedWriteGate = { allowedActions: readonly string[]; } — ⚠ undocumented +- `MatchFilter` (interface): interface MatchFilter — ⚠ undocumented +- `PlayerIdentity` (interface): interface PlayerIdentity — ⚠ undocumented +- `PlayerPose` (interface): interface PlayerPose — ⚠ undocumented +- `PoseSyncRules` (interface): interface PoseSyncRules — ⚠ undocumented +- `PoseSyncTuning` (interface): interface PoseSyncTuning — ⚠ undocumented +- `PresenceActions` (interface): interface PresenceActions — ⚠ undocumented +- `PresenceFeeds` (interface): interface PresenceFeeds — ⚠ undocumented +- `PresencePoseState` (interface): interface PresencePoseState — ⚠ undocumented +- `PresenceSession` (interface): interface PresenceSession — ⚠ undocumented +- `PresenceTransport` (interface): interface PresenceTransport — Backend seam for multiplayer presence. Feeds are reactive data and change identity whenever any player's pose updates; actions MUST be identity-stable for the lifetime of a mounted session so join/leave lifecycle effects can depend on them without re-running per pose tick. The use* members are called as React hooks by consumers, so a mounted transport must never change identity — remount the subtree to switch backends. +- `PushToTalkMode` (type): type PushToTalkMode = "hold" | "toggle" | "openMic" — ⚠ undocumented +- `PushToTalkStatus` (type): type PushToTalkStatus = "idle" | "keyed" | "open" — ⚠ undocumented +- `SessionListing` (interface): interface SessionListing — ⚠ undocumented +- `SessionVisibility` (type): type SessionVisibility = "public" | "private" — ⚠ undocumented +- `Vec3` (interface): interface Vec3 — ⚠ undocumented +- `VoiceParticipant` (interface): interface VoiceParticipant — ⚠ undocumented +- `VoiceRoute` (interface): interface VoiceRoute — ⚠ undocumented +- `VoiceTransport` (interface): interface VoiceTransport — Signaling seam for voice: who is in a channel and which media stream descriptor they published. The media plane (WebRTC, SFU, or anything else that moves audio bytes) stays behind this seam, host-supplied — the engine never touches it. subscribers delivers the channel roster on every change, starting with the current roster. +- `browseSessions` (function): function browseSessions(listings: readonly SessionListing[], filter: MatchFilter = {}, options: BrowseOptions = {}): SessionListing[] — ⚠ undocumented +- `createFeedWriteGate` (function): function createFeedWriteGate(allowedActions: readonly string[] = []): FeedWriteGate — ⚠ undocumented +- `createLocalVoiceTransport` (function): function createLocalVoiceTransport(options?: { userId?: string }): { transport: VoiceTransport; participants(channelId: string): readonly VoiceParticipant[]; } — ⚠ undocumented +- `createPoseSyncGate` (function): function createPoseSyncGate(tuning: PoseSyncTuning): PoseSyncGate — ⚠ undocumented +- `createPushToTalk` (function): function createPushToTalk(config?: { mode?: PushToTalkMode; onChange?: (transmitting: boolean) => void; }): PushToTalk — ⚠ undocumented +- `findByJoinCode` (function): function findByJoinCode(listings: readonly SessionListing[], code: string): SessionListing | null — ⚠ undocumented +- `normalizeJoinCode` (function): function normalizeJoinCode(code: string): string — ⚠ undocumented +- `quickMatch` (function): function quickMatch(listings: readonly SessionListing[], filter: MatchFilter = {}): SessionListing | null — ⚠ undocumented +- `resolveGuestSession` (function): function resolveGuestSession(seed?: string): AuthSession — ⚠ undocumented +- `sessionPlayer` (function): function sessionPlayer(session: AuthSession): PlayerIdentity — ⚠ undocumented +- `validateFeedWrite` (function): function validateFeedWrite(gate: FeedWriteGate | undefined, action: string): { ok: true } | { ok: false; reason: string } — ⚠ undocumented + +## @jgengine/core/procedural + +- `DecayMeterSet` (interface): interface DecayMeterSet — ⚠ undocumented +- `Moodle` (interface): interface Moodle — ⚠ undocumented +- `MoodleStack` (interface): interface MoodleStack — ⚠ undocumented +- `MultiRegionHealth` (interface): interface MultiRegionHealth — ⚠ undocumented +- `createDecayMeterSet` (function): function createDecayMeterSet(configs: readonly DecayMeterConfig[]): DecayMeterSet — Named decay meters — hunger, thirst, oxygen, sanity, warmth, stamina. Each drains (or recovers) on game-time `dt` at a configurable rate, refills from consumables or actions, and raises moodle statuses at thresholds. Rate modifiers let the environment drive them (colder → faster warmth loss; toxic biome → oxygen drops), so a game reads an environment field then calls `setRateModifier`. +- `createMoodleStack` (function): function createMoodleStack(): MoodleStack — A stateful holder for timed status moodles (food buffs, temporary shelter, warmth). Meters and multi-region health derive their own moodles on read; combine all three through `stackMoodles(stack.list(), meterMoodles, ailmentMoodles)` for one display. +- `createMultiRegionHealth` (function): function createMultiRegionHealth(config: MultiRegionHealthConfig): MultiRegionHealth — Per-region/limb health tracked separately, so each body part takes and heals damage on its own. +- `stackMoodles` (function): function stackMoodles(...groups: readonly (readonly Moodle[])[]): Moodle[] — Merge any number of moodle groups into one stack — meters, ailments, and buffs share this display. Same-id moodles fold together (stacks add, worst severity wins); the result is ordered worst-first so the HUD reads critical statuses at a glance. + ## @jgengine/core/runtime/adapter - `MultiplayerAdapterConfig` (type): type MultiplayerAdapterConfig = | { kind: "convex"; topology?: MultiplayerTopology; authority?: MultiplayerAuthority } | { kind: "ws"; topology?: MultiplayerTopology; url?: string; authority?: MultiplayerAuthority } | { kind: "socketio"; topology?: MultiplayerTopology; url?: string; authority?: Mult… — ⚠ undocumented @@ -143,7 +552,8 @@ - `MultiplayerTopology` (type): type MultiplayerTopology = "shared" | "lobbies" | "private" — ⚠ undocumented - `ServersPoolConfig` (type): type ServersPoolConfig = { maxServers: number; slotsPerServer: number; minPlayersToStart?: number; adapter: MultiplayerAdapterConfig; } — ⚠ undocumented - `adapterOf` (function): function adapterOf(multiplayer: unknown): MultiplayerAdapterConfig | null — ⚠ undocumented -- `convex` (function): function convex(config?: { topology?: MultiplayerTopology; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — ⚠ undocumented +- `convex` (function): function convex(config?: { topology?: MultiplayerTopology; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — Convex transport. Omitting `authority` (or passing `"client"`) is **presence-only** — prefer `convexPresence()` to name that intent explicitly. Pass `{ authority: "server" }` for a shared, host-authoritative world — see `examples/HOSTED.md`. +- `convexPresence` (function): function convexPresence(config?: { topology?: MultiplayerTopology }): MultiplayerAdapterConfig — Presence-only Convex transport — each client runs its own `onTick`; only presence/feeds/chat sync. Sugar for `convex({ ...config, authority: "client" })`. - `fly` (function): function fly(config: { app: string; topology?: MultiplayerTopology; path?: string; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — ⚠ undocumented - `isOffline` (function): function isOffline(multiplayer: unknown): boolean — True for a single-player world — no adapter, or an explicit `offline()` one. Gates offline-only wiring like local whole-world save. - `isPresenceOnly` (function): function isPresenceOnly(multiplayer: unknown): boolean — True when multiplayer is on but the world sim is not host-authoritative — presence/feeds/chat only. Equivalent to `resolveAuthority(m) === "client"`. @@ -155,7 +565,8 @@ - `resolveAuthority` (function): function resolveAuthority(multiplayer: unknown): MultiplayerAuthority | null — Resolved authority for a multiplayer config. - `offline` / missing adapter → `null` (single-player; not multiplayer authority). - unset or `"client"` → `"client"` (presence-only; each client ticks). - `"server"` → host-authoritative shared sim. - `servers` (function): function servers(config: ServersPoolConfig): ServersPoolConfig — ⚠ undocumented - `socketIo` (function): function socketIo(config?: { topology?: MultiplayerTopology; url?: string; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — ⚠ undocumented -- `ws` (function): function ws(config?: { topology?: MultiplayerTopology; url?: string; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — ⚠ undocumented +- `ws` (function): function ws(config?: { topology?: MultiplayerTopology; url?: string; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — WebSocket transport. Omitting `authority` (or passing `"client"`) is **presence-only** — prefer `wsPresence()` to name that intent explicitly. Pass `{ authority: "server" }` for a shared, host-authoritative world — see `examples/HOSTED.md`. +- `wsPresence` (function): function wsPresence(config?: { topology?: MultiplayerTopology; url?: string }): MultiplayerAdapterConfig — Presence-only WebSocket transport — each client runs its own `onTick`; only presence/feeds/chat sync. Sugar for `ws({ ...config, authority: "client" })`. ## @jgengine/core/runtime/cameraDirector @@ -208,6 +619,13 @@ - `RuntimeWorldContext` (type): type RuntimeWorldContext = RuntimeInitContext & { playerIds: readonly string[]; } — ⚠ undocumented - `ServerLoopHooks` (type): type ServerLoopHooks = { onInit?: (ctx: RuntimeInitContext) => void; onNewPlayer?: (ctx: RuntimeLoopContext) => void; onTick?: (ctx: RuntimeWorldContext, dtSeconds: number) => void; } — ⚠ undocumented +## @jgengine/core/runtime/headlessRunner + +- `HeadlessInput` (interface): interface HeadlessInput — One step's worth of player intent handed to {@link HeadlessRunner.step} — the held-action set and pointer state the shell would otherwise publish from the browser each frame. +- `HeadlessRunner` (interface): interface HeadlessRunner — A renderer-free driver for a game loop: builds a {@link GameContext} from a {@link GameDefinition}, runs the init hooks, then advances the simulation one step at a time from injected input. No React, R3F, or three.js — the whole play path (time, input, `onTick`, behaviour nav, optional player movement) runs from `core` primitives alone, so a non-React host (a server tick, a test, a CLI replay) can play a real game and read its world snapshot. The shell's FrameDriver is one such driver bolted to `useFrame`; this is the same step distilled out of the render tree. +- `HeadlessRunnerOptions` (interface): interface HeadlessRunnerOptions — ⚠ undocumented +- `createHeadlessRunner` (function): function createHeadlessRunner(options: HeadlessRunnerOptions): HeadlessRunner — ⚠ undocumented + ## @jgengine/core/runtime/hostPersistence - `FEED_RING_LIMIT` (const): const FEED_RING_LIMIT: 20 — ⚠ undocumented @@ -340,16 +758,496 @@ - `WorldMirror` (interface): interface WorldMirror — The client end of host-authoritative replication: folds a host's baseline + {@link WorldDiff} stream onto a local {@link GameContext}. It keeps the last full {@link WorldSnapshot}, advances it with each diff, and pushes the result through `ctx.hydrate` — so the client mirrors exactly the subsystems its own game opted into and silently ignores host modules it lacks. This is the inverse of a {@link HostedWorldSession}; the transport in between (loopback, ws, Convex) is irrelevant. +## @jgengine/core/runtime/worldProjection + +- `ReplicationPolicy` (interface): interface ReplicationPolicy — Host-side interest/privacy policy — how the authoritative world projects to each viewer over the wire. Unset (the default) means every client receives the whole world, exactly as before. Enabling a field changes only what each client *sees*, never how the host simulates: the game plays identically. The core replication modules read this to attach a {@link SnapshotModule.project} without the engine growing a per-feature branch. +- `policyProjectsViewers` (function): function policyProjectsViewers(policy: ReplicationPolicy | undefined): boolean — True when at least one field of the policy would change the wire payload. A no-op policy needs no projection. +- `projectByVisibleIds` (function): function projectByVisibleIds(byId: Record, visible: Set): Record — Keep only the entries of an entity-id-keyed record whose id is in `visible` — the projection for entity stats under area-of-interest. +- `projectEntitiesForViewer` (function): function projectEntitiesForViewer(entities: readonly SceneEntity[], viewer: SnapshotViewer, radius: number): readonly SceneEntity[] — Cull an entity list to a viewer's area of interest: keep the viewer's own entity plus every entity within `radius` of it. When the viewer has no locatable entity the full list is returned (fail-open — a spectator or not-yet-spawned player still sees the world rather than an empty one). +- `projectPerUserForViewer` (function): function projectPerUserForViewer(byUser: Record, viewer: SnapshotViewer): Record — Narrow a `userId → state` record to only the viewer's own entry — the projection for private per-user state (inventory, wallets) so one client never receives another player's private data. +- `visibleEntityIds` (function): function visibleEntityIds(entities: readonly SceneEntity[], viewer: SnapshotViewer, radius: number): Set — The set of entity ids a viewer can see under an area-of-interest radius — the visibility set entity-keyed modules cull against. + ## @jgengine/core/runtime/worldReplication - `WorldDiff` (interface): interface WorldDiff — A revision-stamped delta over a {@link WorldSnapshot}. The host sends one per tick to each client, carrying only what changed since that client's last acknowledged revision — entity/stat/store deltas plus whole snapshots of any other opted-in module (feed, leaderboard, chat, …) that changed. Fold it onto a prior baseline with {@link applyWorldDiff}. - `WorldReplicator` (type): type WorldReplicator = ReturnType — The stateful diff tracker returned by {@link createWorldReplicator}: `commit()`, `diff(sinceRevision)`, `revision()`. +- `WorldReplicatorOptions` (interface): interface WorldReplicatorOptions — Optional acceleration for {@link createWorldReplicator}: a monotone world-dirty counter (aggregated from each {@link SnapshotModule.version}). When it hasn't advanced since the last commit nothing mutated, so the replicator skips re-reading and re-serializing the whole world — the change-detection short-circuit item #28 asks for. Omit it (or pass a snapshot-only source) to keep the original full-re-serialize-per-commit behavior. ## @jgengine/core/runtime/worldSnapshot - `SnapshotModule` (interface): interface SnapshotModule — The replication seam for host-authoritative shared worlds: the opt-in feature manifest *is* the replication schema. Each live subsystem a game opts into registers a {@link SnapshotModule} keyed by name; the host serializes exactly the registered set into a {@link WorldSnapshot} and a client hydrates the same keys back. Adding a replicated subsystem is a registration, never a new branch. +- `SnapshotViewer` (interface): interface SnapshotViewer — Who a host→client snapshot is being projected for — the identity a {@link SnapshotModule.project} filters against. - `WorldSnapshot` (type): type WorldSnapshot = Record — Full world baseline keyed by {@link SnapshotModule.key} — one entry per opted-in subsystem. +## @jgengine/core/ui + +- `BUILT_IN_SETTING_CATEGORIES` (const): const BUILT_IN_SETTING_CATEGORIES: readonly BuiltInSettingCategory[] — ⚠ undocumented +- `DEFAULT_GRAPHICS_QUALITY` (const): const DEFAULT_GRAPHICS_QUALITY: GraphicsQuality — ⚠ undocumented +- `DEFAULT_GRAPHICS_SHADOWS` (const): const DEFAULT_GRAPHICS_SHADOWS: true — ⚠ undocumented +- `DEFAULT_MASTER_VOLUME` (const): const DEFAULT_MASTER_VOLUME: 1 — ⚠ undocumented +- `DEFAULT_UI_SCALE` (const): const DEFAULT_UI_SCALE: 1 — Player-controlled multiplier on the HUD's computed fit scale — one lever on desktop and mobile alike. +- `GRAPHICS_QUALITY_DPR` (const): const GRAPHICS_QUALITY_DPR: Record — Device-pixel-ratio ceiling per quality tier — the shell's `Canvas` dpr cap. +- `GRAPHICS_QUALITY_OPTIONS` (const): const GRAPHICS_QUALITY_OPTIONS: readonly SettingOption[] — ⚠ undocumented +- `GameLayoutMode` (type): type GameLayoutMode = "desktop-wide" | "desktop-compact" | "mobile-landscape" | "mobile-portrait" — The explicit composition mode a game renders for — not a scaled desktop layout. +- `GameSettingDef` (interface): interface GameSettingDef — Extra setting a game appends to a built-in category via `defineGame({ settings: { extra } })`. +- `GameSettingsConfig` (interface): interface GameSettingsConfig — ⚠ undocumented +- `GameViewportLayout` (interface): interface GameViewportLayout — The shared live geometry the engine allocates once and every UI subsystem reads. +- `GradeConfig` (interface): interface GradeConfig — Final colour-grade stage: lift/gain/gamma, saturation, vignette, film grain — applied in display space after tone mapping. +- `GraphicsQuality` (type): type GraphicsQuality = "low" | "medium" | "high" — ⚠ undocumented +- `HUD_ANCHOR_FRACTIONS` (const): const HUD_ANCHOR_FRACTIONS: Record — ⚠ undocumented +- `HudAnchor` (type): type HudAnchor = | "top-left" | "top" | "top-right" | "left" | "center" | "right" | "bottom-left" | "bottom" | "bottom-right" — ⚠ undocumented +- `HudLayoutStore` (interface): interface HudLayoutStore — ⚠ undocumented +- `HudPlacement` (interface): interface HudPlacement — ⚠ undocumented +- `HudPlatform` (type): type HudPlatform = "web" | "mobile" — Where a game is meant to be played. `"web"` alone keeps today's desktop-first HUD; adding `"mobile"` turns on design-resolution fit scaling on compact displays. +- `HudPriority` (type): type HudPriority = "critical" | "secondary" | "tertiary" — Gameplay-importance tier of a HUD element. +- `HudSize` (interface): interface HudSize — ⚠ undocumented +- `HudViewportConfig` (interface): interface HudViewportConfig extends HudFitConfig — Per-game HUD viewport declaration carried on `PlayableGame.hudFit`; `mobile` overrides the fit on compact displays so the owner can tune the phone layout separately. +- `Insets` (interface): interface Insets — Edge insets in CSS pixels (safe areas, reservations). +- `LayoutCollision` (interface): interface LayoutCollision — One detected forbidden/warned overlap between two regions. +- `LayoutCollisionPolicy` (type): type LayoutCollisionPolicy = "forbid" | "allow" | "warn" — How a region participates in collision reporting. +- `LayoutOrientation` (type): type LayoutOrientation = "portrait" | "landscape" — A concrete device orientation. +- `LayoutRect` (interface): interface LayoutRect — Axis-aligned rectangle in CSS pixels (origin top-left). Structurally compatible with a `DOMRect`'s edge fields. +- `LayoutRegion` (interface): interface LayoutRegion — A physical rectangle a UI subsystem occupies, published to the shared registry. +- `LookPreset` (type): type LookPreset = "cinematic" | "flat" — Named default-look preset composing the existing lighting/sky/fog/post knobs into one field. `"cinematic"` (the default when unset) draws a scene lit like a shipped game — a real day sky with a view-following shadow-casting sun + hemisphere fill, a network-free image-based-lighting environment so PBR surfaces catch soft reflections, and a tuned tone-map/bloom/AO/vignette post stack. `"flat"` opts out of the sky/IBL/post rig to the bare ambient+directional default (pre-#773). The upgraded default primitive materials — tuned roughness/metalness plus subtle procedural surface detail so un-modeled boxes/capsules stop reading as flat plastic — apply under both presets. +- `MobileHudBehavior` (type): type MobileHudBehavior = | "persistent" | "compact" | "icon" | "transient" | "hidden" | "sheet" | "modal" — How a HUD element adapts on phones. +- `PostProcessingConfig` (interface): interface PostProcessingConfig — Declarative post-processing chain (RenderPass → AO → Bloom → tone-map output → Grade). Present on a game means the shell mounts an `EffectComposer` and owns the render; absent means the renderer draws directly (unchanged). Each stage is a config object, `false` to skip, or omitted for its default. Pure data — no three.js types leak into core. +- `SETTING_IDS` (const): const SETTING_IDS: { readonly masterVolume: "sound.master"; readonly graphicsQuality: "graphics.quality"; readonly graphicsShadows: "graphics.shadows"; readonly graphicsUiScale: "graphics.uiScale"; readonly touchStyle: "controls.touchStyle"; } — ⚠ undocumented +- `STUDIO_STAGE_POST` (const): const STUDIO_STAGE_POST: PostProcessingConfig — A cinematic "product shot" post preset — the full chain on (contact-AO, soft bloom, a warm film grade with vignette + a touch of grain + chromatic aberration). Meant for a `StudioStage` where a single parametric asset is framed on a backdrop, so every studio reads shipped, not intern-tier. DoF is left off by default (it needs a per-scene focus distance); set `dof` to enable it. +- `SettingCategory` (type): type SettingCategory = BuiltInSettingCategory | (string & {}) — Built-in category ids keep autocomplete; any other string makes a fresh category. +- `SettingCategoryDef` (interface): interface SettingCategoryDef — Declares or relabels/reorders a category tab; use it for a custom category or to reshape the built-ins. +- `SettingKind` (type): type SettingKind = "slider" | "toggle" | "select" — ⚠ undocumented +- `SettingOption` (interface): interface SettingOption — ⚠ undocumented +- `SettingValue` (type): type SettingValue = number | boolean | string — ⚠ undocumented +- `SettingsActionDef` (interface): interface SettingsActionDef — A game-state action (Restart, Quit to menu, …) shown as rows in the first "Game" settings tab — never a floating button or a rebindable key. +- `SettingsStore` (interface): interface SettingsStore — ⚠ undocumented +- `SettingsSurface` (type): type SettingsSurface = "quick" — `quick` shows compact on-screen volume/graphics buttons; `false` (default) mounts no engine trigger — open the menu from your own UI with `` or `useSettings().open()`. +- `SettingsVariant` (type): type SettingsVariant = "panel" | "sheet" | "sidebar" | "fullscreen" — The four themed settings layouts, chosen with `defineGame({ settings: { variant } })`. All read the game's `--jg-*` theme tokens. +- `SwingTargetInput` (interface): interface SwingTargetInput — The current target, or the fields the bar needs from it. +- `ToneMappingMode` (type): type ToneMappingMode = "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none" — Renderer tone-mapping curve applied by the post chain's output stage. +- `UI_SCALE_MAX` (const): const UI_SCALE_MAX: 1.5 — ⚠ undocumented +- `UI_SCALE_MIN` (const): const UI_SCALE_MIN: 0.5 — ⚠ undocumented +- `busVolumeSettingId` (function): function busVolumeSettingId(busId: string): string — ⚠ undocumented +- `createSettingsStore` (function): function createSettingsStore(storage: Pick | null | undefined = defaultStorage()): SettingsStore — Reactive, localStorage-backed settings store shared by the shell wiring and React hooks. +- `formatDelta` (function): function formatDelta(seconds: number, decimals: 0 | 1 | 2 = 2): string — Format a signed time gap as `+m:ss.ff` / `-m:ss.ff`, for race deltas and split times. +- `formatDistance` (function): function formatDistance(meters: number, options: DistanceFormat = {}): string — Format a distance given in meters as a HUD-ready string, switching to km automatically past 1000m when `unit: "auto"`. +- `formatDuration` (function): function formatDuration(seconds: number, options: DurationFormat = {}): string — Format a duration in seconds as a clock string (`m:ss`, `m:ss.ff`, or `h:mm:ss`), the shape every timer and racing HUD needs. +- `formatOrdinal` (function): function formatOrdinal(value: number): string — English ordinal for a placement number: 1 → "1st", 2 → "2nd", 3 → "3rd", 11 → "11th". +- `formatSpeed` (function): function formatSpeed(metersPerSecond: number, options: SpeedFormat = {}): string — Format a speed given in meters/second as a HUD-ready string in km/h, mph, knots, or m/s — the one conversion table every speedometer and telemetry readout should share. +- `hudScaleForViewport` (function): function hudScaleForViewport(fit: Required, viewport: HudSize): number — The one scaling rule for every display: the ratio of the live viewport to the authored design size along the limiting axis, clamped. 1 on a viewport at or above design size; smoothly below 1 down to `minScale` on phones. +- `orientationGateActive` (function): function orientationGateActive(requirement: OrientationRequirement, liveOrientation: LayoutOrientation): boolean — The rotate gate blocks gameplay: a hard requirement (or `unsupported`) the live orientation doesn't satisfy. +- `orientationHintActive` (function): function orientationHintActive(requirement: OrientationRequirement, liveOrientation: LayoutOrientation): boolean — An advisory rotate hint applies: a preference (not a hard gate) the live orientation doesn't satisfy. +- `overflowingPanels` (function): function overflowingPanels(panels: readonly { id: string; rect: HudRect }[], viewport: HudSize, tolerance = 1.5): HudOverflow[] — Every panel rect that escapes the viewport — the data behind the HUD overflow gate. +- `resolveGameLook` (function): function resolveGameLook(input: GameLookInput): ResolvedGameLook — Expand a game's `look` into concrete lighting/backdrop/post. The default is `"cinematic"`, so a scene reads lit-like-a-game out of the box; `"flat"` passes the explicit knobs through untouched. Anything the game authored wins — the preset only fills unset knobs, and it never adds a sky when the world already owns one (so the sky's tuned sun/hemisphere serve as the lighting rig). +- `resolveHudFit` (function): function resolveHudFit(config: HudViewportConfig | undefined, mobile: boolean): Required — ⚠ undocumented +- `resolveOrientationRequirement` (function): function resolveOrientationRequirement(orientation: GameOrientation | undefined, platform: "mobile" | "desktop"): OrientationRequirement — Resolve the game's orientation declaration into a concrete requirement for a platform. Desktop is always unconstrained. +- `swingTimerState` (function): function swingTimerState(player: SwingPlayerInput, target: SwingTargetInput | null, prevPeriod: number, prevTimer: number): SwingTimerState — Pure swing-timer bar state — no hidden state, no clock, no DOM. The caller threads `prevPeriod`/`prevTimer` back each frame. The period is recovered on the reset edge (when `swingTimer` jumps up = a new swing began) as `max(swingTimer, weapon.speed)`, so the fill is correct even without knowing the weapon's exact cadence. Hidden unless auto-attacking a live, non-object target. + +## @jgengine/core/world + +- `Aabb` (interface): interface Aabb — ⚠ undocumented +- `AddBodyOptions` (type): type AddBodyOptions = BoxBodyOptions | SphereBodyOptions — ⚠ undocumented +- `Aim` (type): type Aim = | { origin: EntityPosition; direction: EntityPosition } | { yaw: number; pitch: number; spread?: number } — ⚠ undocumented +- `AssetCatalog` (interface): interface AssetCatalog — ⚠ undocumented +- `AudioBusDef` (interface): interface AudioBusDef — ⚠ undocumented +- `AudioFalloffConfig` (interface): interface AudioFalloffConfig — ⚠ undocumented +- `AutoTargetPolicy` (type): type AutoTargetPolicy = | "nearest" | "farthest" | "random" | "strongest" | "weakest" | "first" | "last" — ⚠ undocumented +- `AvoidZone` (interface): interface AvoidZone — A circular clearance around a gameplay spot (spawn, plot, path point, POI): scatter is repelled from it and terrain is flattened toward its center. `feather` (meters) is the soft outer band — full effect within `radius - feather`, ramping to zero at `radius`. +- `BallisticSweep` (type): type BallisticSweep = ( origin: readonly [number, number, number], velocity: readonly [number, number, number], gravity: number, maxTime: number, ) => BallisticSweepHit | null — ⚠ undocumented +- `BallisticSweepHit` (interface): interface BallisticSweepHit — ⚠ undocumented +- `BehaviorDescriptor` (type): type BehaviorDescriptor = | WanderBehavior | PatrolBehavior | PromptableBehavior | PlayerBehavior — ⚠ undocumented +- `BiomeBand` (interface): interface BiomeBand — A z-ordered ground palette zone — the linear-boundary counterpart to the radial `materialRegions`. Adjacent bands cross-fade into each other across a `fade`-wide window centered on the midpoint z between their centers, so a multi-biome world (vale → marsh → peaks along z) blends its ground color instead of hard-switching. Bands may also carry per-zone `fog`, `sky`, and `weather`. Order the list by ascending `z`. +- `BoundsSpec` (type): type BoundsSpec = | { readonly kind: "sphere"; readonly radius: number; readonly offset?: Vec3 } | { readonly kind: "aabb"; readonly half: Vec3; readonly offset?: Vec3 } | { readonly kind: "rect"; readonly halfWidth: number; readonly halfDepth: number; readonly halfHeight?: number; readonly offset?:… — How a renderable declares its extent. AABB, bounding sphere, and 2D rectangle cover the common cases; `point` is the degenerate zero-size default for objects that never override. `offset` shifts the volume from the object origin (e.g. a tall model whose pivot is at its feet). +- `BuildRole` (type): type BuildRole = "owner" | "editor" | "viewer" — ⚠ undocumented +- `BuildingEnvironmentDescriptor` (type): type BuildingEnvironmentDescriptor = { kind: "building" } & Required< Pick > & Pick — ⚠ undocumented +- `BuildingIndex` (interface): interface BuildingIndex — ⚠ undocumented +- `BuildingPaletteOverrides` (type): type BuildingPaletteOverrides = Partial — ⚠ undocumented +- `BuildingStyle` (type): type BuildingStyle = | "generic" | "capital" | "village" | "desert" | "industrial" | "coastal" | "neon" | "ruin" | "frontier" | "aerial" — ⚠ undocumented +- `CameraView` (type): type CameraView = PerspectiveView | OrthographicView — ⚠ undocumented +- `CameraVisibilityContext` (interface): interface CameraVisibilityContext — A camera's contribution to visibility. The VisibilitySystem unions results across every active context: an object stays renderable/loaded if *any* relevant camera needs it. A camera can opt out of driving asset streaming (e.g. a minimap that only needs positions, not loaded meshes) via `influencesStreaming: false`. +- `Cardinal` (type): type Cardinal = "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" — ⚠ undocumented +- `Carryable` (class): class Carryable — A grabbed physics object following a moving hold point through a spring constraint (the pick — a raycast — is the caller's/shell's job; core owns the constraint). Supports shared multi-owner carry (the follow point is the average of owners' hold points), an encumbrance read, and drop/throw. Reuses `PhysicsWorld.springJoint` to a world anchor moved each frame. +- `CarvableField` (class): class CarvableField implements TerrainField — A `TerrainField` you can write craters and mounds into at runtime — the height-field side of destructible terrain (Helldivers 2 explosion craters, engineer-deposited berms). Wraps a base field and layers smooth radial deformations on top, so `sampleHeight` (and therefore ground-snap, collision, and the shell's terrain mesh) all read the deformed surface. `carve` digs a bowl, `deposit` raises a mound. +- `ClockSnapshot` (interface): interface ClockSnapshot — ⚠ undocumented +- `CollapseEvent` (interface): interface CollapseEvent — ⚠ undocumented +- `ColliderPurpose` (type): type ColliderPurpose = "physical" | "damage" — ⚠ undocumented +- `CollisionEvent` (interface): interface CollisionEvent — A contact reported to `onCollision`. The object is reused each call — read/copy, never retain. +- `ConcealmentSensor` (interface): interface ConcealmentSensor — ⚠ undocumented +- `ContextMenu` (interface): interface ContextMenu — ⚠ undocumented +- `ContextVerb` (interface): interface ContextVerb — One right-click verb: a label plus the command it dispatches (walk-then-act supported by args). +- `DEFAULT_FORWARD` (const): const DEFAULT_FORWARD: readonly [number, number, number] — The forward-axis convention: a generator or scene kind declares which way its "front" faces (a bookcase's open/book face, a building's entrance) once, as data, instead of leaving every placement to hand-tuned `rotationY` trial-and-error. `StudioStage`'s `faceCamera` (`@jgengine/shell/scene/ StudioStage`) reads the declared axis to auto-orient a product shot; a placement tool can read the same field to face a freshly dropped asset toward the camera/path by default. `DEFAULT_FORWARD` (+Z) is what a generator/scene-kind gets when it omits `forward` — build your front toward it. +- `DEFAULT_GRIP_CURVE` (const): const DEFAULT_GRIP_CURVE: GripCurve — ⚠ undocumented +- `DEFAULT_MARKER_KINDS` (const): const DEFAULT_MARKER_KINDS: Record — ⚠ undocumented +- `DEFAULT_REPUTATION_TIERS` (const): const DEFAULT_REPUTATION_TIERS: readonly ReputationTier[] — ⚠ undocumented +- `EditableTerrain` (interface): interface EditableTerrain extends TerrainField — ⚠ undocumented +- `EnclosedFootprint` (interface): interface EnclosedFootprint — ⚠ undocumented +- `EntityColliderSet` (interface): interface EntityColliderSet — ⚠ undocumented +- `EntityPosition` (type): type EntityPosition = readonly [number, number, number] — ⚠ undocumented +- `EnvironmentField` (interface): interface EnvironmentField — ⚠ undocumented +- `EnvironmentWorldFeature` (interface): interface EnvironmentWorldFeature — ⚠ undocumented +- `FactionDef` (interface): interface FactionDef — ⚠ undocumented +- `FireGrid` (interface): interface FireGrid — ⚠ undocumented +- `FogCells` (interface): interface FogCells — ⚠ undocumented +- `FogField` (interface): interface FogField — Reveal-on-event fog of war over a fixed grid. Walking (`revealAlong`) and digging/acting (`reveal`) clear cells; once revealed a cell stays revealed. Pure and renderer-free — the shell/react map draws `cells()`. +- `ForceVolume` (class): class ForceVolume — A trigger region that pushes bodies passing through it — boost pads (`impulse` + `once`), conveyors (`velocity`), fans/wind (`accelerate`). Call `apply` each tick; `once` mode fires only on entry by tracking membership between ticks. +- `FramingConfig` (interface): interface FramingConfig — ⚠ undocumented +- `FreezeMonitor` (interface): interface FreezeMonitor — ⚠ undocumented +- `FreezeViolation` (interface): interface FreezeViolation — ⚠ undocumented +- `Frustum` (interface): interface Frustum — ⚠ undocumented +- `FrustumProjection` (interface): interface FrustumProjection — ⚠ undocumented +- `FrustumSample` (interface): interface FrustumSample — ⚠ undocumented +- `FrustumSensor` (interface): interface FrustumSensor — ⚠ undocumented +- `FrustumTarget` (interface): interface FrustumTarget — ⚠ undocumented +- `GRASS_SCHEMA` (const): const GRASS_SCHEMA: ParamSchema — The grass parameter schema — drives the inspector and `meta` parse via the studio seam. +- `GeneratedAsset` (interface): interface GeneratedAsset — A resolved generator asset: its parts plus the overall local-space bounds (min/max corners). +- `GeneratedPart` (interface): interface GeneratedPart — One generated primitive part — a box/panel placed in the asset's local space. +- `Glide` (class): class Glide — A reduced-gravity, forward-thrust glide over a physics body — wingsuit / glider / paraglider (Enshrouded, Grounded). Call `apply(dt, steerX, steerZ)` each frame *before* `world.step`: it feeds back most of the gravity the sim is about to apply (leaving `gravityScale` of it), pushes the body along the steer vector by `thrust`, and clamps descent to `maxFallSpeed`. Stop calling it to fall normally again — no attach/detach state to leak. +- `Grapple` (class): class Grapple — A fired-anchor rope on the joint API — grapple (reel toward a hit point), zipline (rigid cable to a far anchor you then slide/reel along), swing (rigid rope + gravity = a pendulum). `fire` attaches a `distance`/`spring` joint from the traveller body to a fixed world point; `reel` shrinks its rest length so the constraint drags the body in; `moveAnchor` re-points it (zipline glide, grapple-to- moving-target). The pick — a raycast to find the anchor — is the caller's; core owns the constraint. +- `GrassEnvironmentDescriptor` (type): type GrassEnvironmentDescriptor = { kind: "grass" } & Required< Pick > & Pick — ⚠ undocumented +- `GripCurve` (interface): interface GripCurve — ⚠ undocumented +- `HeatConfig` (interface): interface HeatConfig — Tuning for {@link createHeatState}/{@link advanceHeat} — levels, decay, and pursuit-spawn ring. +- `HeatGain` (interface): interface HeatGain — One crime tick's contribution — only `witnessed` gains raise heat (unseen crimes are free, GTA-style). +- `HeatLevelDef` (interface): interface HeatLevelDef — One escalation tier — the heat threshold it begins at and the pursuer count it wants active. +- `HeatSource` (interface): interface HeatSource — A localized warmth source — campfire, forge, geothermal vent. +- `HeatState` (interface): interface HeatState — Serializable heat-system state — round-trips through `createHeatState`/`advanceHeat` each tick. +- `HiddenStateSource` (interface): interface HiddenStateSource — ⚠ undocumented +- `Job` (interface): interface Job — ⚠ undocumented +- `JobDef` (interface): interface JobDef — ⚠ undocumented +- `JobReport` (interface): interface JobReport — ⚠ undocumented +- `KinematicVehicle` (interface): interface KinematicVehicle — The pure-kinematic arcade car every racing game hand-rolled (#282.1): steer-yaw scaled by speed, throttle/brake acceleration, and a grip-curve lateral-slip bleed — no `PhysicsWorld`, no wheels, just the drift-friendly integration the three shipped racers proved out. Games keep their flavor (drift meters, boost, off-track rules) via `surfaceFriction`/`dragAt` hooks and the returned slip. +- `KinematicVehicleStep` (interface): interface KinematicVehicleStep — ⚠ undocumented +- `KinematicVehicleTuning` (interface): interface KinematicVehicleTuning — ⚠ undocumented +- `LOCK_ACTIONS` (const): const LOCK_ACTIONS: readonly LockAction[] — The five pick actions, in display order (shallow → deep). +- `LockAction` (type): type LockAction = "hardSet" | "set" | "steady" | "ease" | "drop" — One discrete pick move: how far the pick drives into the lock this step. +- `LockCell` (interface): interface LockCell — One cell inside the fogged {@link visibleCells} window: its board position and kind. +- `LockSpec` (interface): interface LockSpec — A generated lock board. `open[col]` holds every enterable row in that column. +- `LockStepResult` (type): type LockStepResult = "advanced" | "slip" | "bind" | "trap" | "success" — Outcome of one {@link stepLock} call: `advanced`/`success` move the pick, `slip`/`bind`/`trap` do not and should cost a life. +- `LockTierSpec` (interface): interface LockTierSpec — Difficulty dials for one lock: board size, forgiveness band, gates, fog window, traps. +- `MOVEMENT_TUNING` (const): const MOVEMENT_TUNING: { readonly standEyeHeight: 1.7; readonly crouchEyeHeight: 1.15; readonly walkSpeedMultiplier: 1.75; readonly runSpeedMultiplier: 2.25; readonly crouchSpeedMultiplier: 0.45; readonly backpedalSpeedMultiplier: 0.65; readonly groundAcceleration: 26; readonly airAcceleration: 12; … — Kinematics + feel tuning for the first-person controller. Centralised here so movement feel lives in one place rather than scattered through the renderer. +- `MapCellStates` (interface): interface MapCellStates — ⚠ undocumented +- `MapMarker` (interface): interface MapMarker — ⚠ undocumented +- `MapRoute` (interface): interface MapRoute — ⚠ undocumented +- `MapZone` (interface): interface MapZone — ⚠ undocumented +- `MarkerKindStyle` (interface): interface MarkerKindStyle — Visual descriptor for a marker kind. Games supply their own palette; the engine ships `DEFAULT_MARKER_KINDS` as a content-agnostic starting set that the react minimap/compass read for colors and glyphs. +- `MarkerSet` (interface): interface MarkerSet — ⚠ undocumented +- `MinimapView` (interface): interface MinimapView — ⚠ undocumented +- `ModelAssetRef` (interface): interface ModelAssetRef — ⚠ undocumented +- `ModelDims` (interface): interface ModelDims — Measured horizontal footprint, footprint center, and lowest Y of a model in model space. +- `ModelNode` (interface): interface ModelNode — Generic named-socket reader for loaded 3D models. Walks a node tree (any object with `.name`, `.position`, and `.children` — structurally satisfied by `THREE.Object3D`) and collects the local offsets of nodes whose name marks an attachment point. Genre-agnostic: wire anchors on a pylon, muzzle/hand mounts on a character, hardpoints on a ship, seat/decal slots on furniture — anything an artist tags with an empty in the GLB. Pure data (no three.js import), so it lives in core. +- `MountController` (class): class MountController — Mount / rideable control-transfer (issue #83). Registers rideables (each with one or more seats — a control seat drives, the rest ride) and tracks who is on what. It owns no camera or physics: game code reads `cameraTarget(riderId)` to point the follow camera at the mount, and `driveTarget(riderId)` to route that rider's {@link import("../physics/vehicleBody").AxisInput}-driven input at the mount's movement kit — the same seam a horse, a truck, or a shared multi-seat ship all plug into. +- `MovementPose` (type): type MovementPose = "standing" | "crouch" | "prone" | "running" — ⚠ undocumented +- `MusicInstrument` (type): type MusicInstrument = | "strings" | "flute" | "harp" | "horn" | "choir" | "bell" | "timpani" | "bass" | "stacc" | "pad" | "lute" | "dulcimer" | "frameDrum" | "warDrum" | "reed" | "pipe" | "squareLead" | "woodBlock" | "tinyBell" | "piano" | "shaker" | "brassStab" | "cymSwell" | "oboe" — Named synthesised instrument. Each maps to a voice in the shell's instrument library (`@jgengine/shell/audio/musicVoices`); an unknown name falls back to a plain sine voice so a theme is never silent. +- `MusicTheme` (interface): interface MusicTheme — A through-composed, looping music track. `events` need not be sorted; the director schedules them ahead against a fixed anchor so loops are seamless. +- `NavGrid` (interface): interface NavGrid — ⚠ undocumented +- `NavPoint` (type): type NavPoint = readonly [number, number] — ⚠ undocumented +- `NoiseFieldConfig` (interface): interface NoiseFieldConfig — Configuration for {@link noiseField}: seed, amplitude, and fractal noise shaping. +- `NoiseVoice` (interface): interface NoiseVoice — A filtered white-noise burst — impacts, whooshes, breath, crackle. Realised from a shared 1s noise buffer at a randomised playback rate and start offset, decaying exponentially to silence at `duration * decay`. +- `NoteEvent` (interface): interface NoteEvent — One scheduled note in a theme, positioned on the loop's quarter-note grid. +- `ObjectVisual` (interface): interface ObjectVisual — ⚠ undocumented +- `OceanEnvironmentDescriptor` (type): type OceanEnvironmentDescriptor = { kind: "ocean" } & Required< Pick > & Pick — ⚠ undocumented +- `POSE_HITBOX` (const): const POSE_HITBOX: Record — ⚠ undocumented +- `PadEnvironmentDescriptor` (type): type PadEnvironmentDescriptor = { kind: "pad" } & Required< Pick > & Pick — ⚠ undocumented +- `PadSize` (type): type PadSize = readonly [number, number] | { radius: number } — ⚠ undocumented +- `PaintStroke` (interface): interface PaintStroke — ⚠ undocumented +- `ParamField` (type): type ParamField = | RangeParamField | NumberParamField | BoolParamField | SelectParamField | ColorParamField | TextParamField | SeedParamField | WeightedListParamField | ActionParamField — One row in a kind's parameter schema — the union the generic inspector knows how to render. +- `ParamSchema` (interface): interface ParamSchema — A kind's full parameter surface: an ordered list of fields the inspector renders top-to-bottom. +- `ParsedParams` (type): type ParsedParams = Record — Parsed params after `parseParams`: every schema field present with a validated, defaulted value. +- `PathFollowConfig` (interface): interface PathFollowConfig — ⚠ undocumented +- `PathFollowState` (interface): interface PathFollowState — ⚠ undocumented +- `PhysicsStats` (interface): interface PhysicsStats — ⚠ undocumented +- `PhysicsWorld` (class): class PhysicsWorld — ⚠ undocumented +- `PlacedStructure` (interface): interface PlacedStructure — ⚠ undocumented +- `PlacementCommit` (interface): interface PlacementCommit — ⚠ undocumented +- `PlacementController` (interface): interface PlacementController — ⚠ undocumented +- `PlacementPreview` (interface): interface PlacementPreview — ⚠ undocumented +- `PlacementRules` (interface): interface PlacementRules — ⚠ undocumented +- `PlatformCarry` (class): class PlatformCarry — Carries bodies standing on a moving platform by composing their transform with the platform's per-`step` delta — moving/rotating lifts and conveyor floors (Fall Guys, Gang Beasts). The platform is a body the game repositions each frame; riders are detected by overlap on its top face. +- `PositionedPrompt` (interface): interface PositionedPrompt — ⚠ undocumented +- `ProximityPrompt` (interface): interface ProximityPrompt — ⚠ undocumented +- `QteStep` (interface): interface QteStep — ⚠ undocumented +- `RainEnvironmentDescriptor` (type): type RainEnvironmentDescriptor = { kind: "rain" } & Required< Pick > — ⚠ undocumented +- `RecordingBuffer` (interface): interface RecordingBuffer — ⚠ undocumented +- `RecordingBufferOptions` (interface): interface RecordingBufferOptions — ⚠ undocumented +- `RegionField` (interface): interface RegionField extends TerrainField — ⚠ undocumented +- `Renderable` (interface): interface Renderable — A scene object the visibility system considers. A normal game object already carries a position and a version counter, so it becomes cullable automatically — no separate "cullable" component. Everything else is optional override. +- `ResolvedCollider` (interface): interface ResolvedCollider — ⚠ undocumented +- `ResolvedTerrainDetail` (type): type ResolvedTerrainDetail = Required> & { waterLevel: number; material?: ResolvedTerrainDetailMaterial; } — A {@link TerrainDetailConfig} with every field resolved to a concrete value — the shape the shell's detail material consumes. +- `ResolvedWeather` (interface): interface ResolvedWeather — ⚠ undocumented +- `RevealHit` (interface): interface RevealHit — ⚠ undocumented +- `RevealQuery` (interface): interface RevealQuery — ⚠ undocumented +- `RoadEnvironmentDescriptor` (type): type RoadEnvironmentDescriptor = { kind: "road" } & Required< Pick > & { /** Resolved sidewalk band, or `false` when the road has none. */ sidewalk: { width: number; color: string } | false; } — Resolved road descriptor produced by {@link road} and rendered by the shell environment scene. +- `RoofPlan` (interface): interface RoofPlan — ⚠ undocumented +- `RosterEntry` (interface): interface RosterEntry — ⚠ undocumented +- `SCATTER_PATH_KIND` (const): const SCATTER_PATH_KIND: "scatter" — The editor path kind that marks a closed polyline as a foliage/scatter region. +- `SHAPE_BOX` (const): const SHAPE_BOX: 0 — ⚠ undocumented +- `SHAPE_SPHERE` (const): const SHAPE_SPHERE: 1 — ⚠ undocumented +- `SOIL_KIND` (const): const SOIL_KIND: "soil" — The editor volume kind marking a box as a soil crack/moss patch. +- `SOIL_SCHEMA` (const): const SOIL_SCHEMA: ParamSchema — The soil parameter schema — drives the inspector and `meta` parse via the studio seam. +- `ScatterInstance` (interface): interface ScatterInstance — ⚠ undocumented +- `ScatterPoint` (interface): interface ScatterPoint — ⚠ undocumented +- `ScatterTerrain` (interface): interface ScatterTerrain — Ground sampler a scatter resolve reads height/normal from (the sculpt terrain or the game's ground). +- `SceneEntity` (interface): interface SceneEntity — ⚠ undocumented +- `SceneKindObject` (interface): interface SceneKindObject — The raw document object a resolver receives — shape shared by markers, volumes, and paths. +- `SceneKindResolveContext` (interface): interface SceneKindResolveContext — Ground sampler + options a resolver may read (terrain height/normal snap). +- `SceneObject` (interface): interface SceneObject — ⚠ undocumented +- `SceneRaycastApi` (interface): interface SceneRaycastApi — ⚠ undocumented +- `SceneRaycastHit` (interface): interface SceneRaycastHit — ⚠ undocumented +- `ScreenRect` (interface): interface ScreenRect — ⚠ undocumented +- `SelectionSet` (interface): interface SelectionSet — ⚠ undocumented +- `SensorProbeOptions` (interface): interface SensorProbeOptions — ⚠ undocumented +- `SensorReading` (interface): interface SensorReading — ⚠ undocumented +- `SimClock` (interface): interface SimClock — ⚠ undocumented +- `SkillCheckConfig` (interface): interface SkillCheckConfig — ⚠ undocumented +- `SkillCheckResult` (interface): interface SkillCheckResult — ⚠ undocumented +- `SkyEnvironmentDescriptor` (type): type SkyEnvironmentDescriptor = { kind: "sky" } & Required< Pick > & Omit — ⚠ undocumented +- `SnapMode` (type): type SnapMode = "grid" | "free" | "surface" — ⚠ undocumented +- `SnowEnvironmentDescriptor` (type): type SnowEnvironmentDescriptor = { kind: "snow" } & Required< Pick > — ⚠ undocumented +- `SoilRules` (interface): interface SoilRules — Fully-defaulted soil params parsed from a volume's `meta`. +- `SoundDef` (interface): interface SoundDef — ⚠ undocumented +- `SpatialGrid` (class): class SpatialGrid — A uniform-grid broad-phase over the x/z plane, separate from the rigid-body sim, for cheap same-tick proximity across hundreds–thousands of simple movers (swarm enemies). Rebuild each tick from the caller's own position arrays, then `queryCircle` (enemies hitting the player / an AoE) or `forEachPair` (mutual separation). Both are precise: no false negatives, no false positives beyond the exact distance test. +- `SpawnDirectorConfig` (interface): interface SpawnDirectorConfig — ⚠ undocumented +- `SpawnDirectorState` (interface): interface SpawnDirectorState — ⚠ undocumented +- `SpawnEntry` (interface): interface SpawnEntry — ⚠ undocumented +- `SpawnRequest` (interface): interface SpawnRequest — ⚠ undocumented +- `StatCatalog` (type): type StatCatalog = Record — ⚠ undocumented +- `StatValue` (interface): interface StatValue — ⚠ undocumented +- `Station` (interface): interface Station — ⚠ undocumented +- `StructureGraph` (class): class StructureGraph — A structural-integrity graph over a building — nodes are pieces (walls, beams, floors), edges are load-bearing connections, some nodes are anchored foundations. `damage`/`damageEdge` wear pieces and connections down; when a piece shatters or an edge severs, the graph recomputes which pieces still reach an anchor and hands back every newly-disconnected piece as one `CollapseEvent`. Feed that to `toDebris` to sink the fallen pieces into a `PhysicsWorld` as rigid bodies ("The Finals" smooth destruction, Rainbow Six walls). Coarse by design: it replicates the collapse event, not per fragment. +- `StructureMaterial` (interface): interface StructureMaterial — ⚠ undocumented +- `SupportResult` (interface): interface SupportResult — ⚠ undocumented +- `SurfaceDelta` (interface): interface SurfaceDelta — A compact record of the surface-material cells a paint stroke touched: parallel `indices`/`before`/`after` arrays into the per-cell surface grid. One per stroke keeps paint undo history small. +- `SurfaceStroke` (interface): interface SurfaceStroke — Accumulates a whole paint drag — many surface stamps — into one compact {@link SurfaceDelta}. Keeps each cell's first `before` and latest `after`, so undo replays the paint as a single step. +- `SynthPatch` (interface): interface SynthPatch — A procedural sound cue: a set of voices triggered together, each with its own `delay`, summed into one one-shot. Pure serialisable data — the shell realises it on Web Audio, so the same catalog runs headless in tests with no `AudioContext`. +- `TERRAIN_MATERIAL_PALETTES` (const): const TERRAIN_MATERIAL_PALETTES: Record — ⚠ undocumented +- `TerraformDelta` (interface): interface TerraformDelta — A compact record of the vertices a sculpt stroke touched: parallel `indices`/`before`/`after` arrays into the offset grid. Storing one of these per stroke keeps undo history small — the whole terrain document is never copied. +- `TerraformEdit` (interface): interface TerraformEdit — A single sculpt stamp: which brush, where, and its shaping parameters. +- `TerraformFalloff` (type): type TerraformFalloff = "smooth" | "linear" | "none" — How a brush's strength fades from its center to its rim. +- `TerraformMode` (type): type TerraformMode = "raise" | "lower" | "smooth" | "flatten" | "noise" | "ramp" | "paint" — A sculpt operation kind: heightfield brushes plus the surface-paint brush. +- `TerraformShape` (type): type TerraformShape = "circle" | "square" — A brush footprint: a round disc or an axis-aligned square. +- `TerraformSnapshot` (interface): interface TerraformSnapshot — ⚠ undocumented +- `TerraformStroke` (interface): interface TerraformStroke — Accumulates a whole drag — many brush stamps — into one compact {@link TerraformDelta}. Keeps each vertex's first `before` and latest `after`, so undo replays the stroke as a single step even though the pointer fired dozens of moves. +- `TerrainCircleRegion` (interface): interface TerrainCircleRegion extends TerrainRegionStyle — A circular palette zone painted over the base terrain palette — snow caps, ash wastes, spawn circles. +- `TerrainDetailConfig` (interface): interface TerrainDetailConfig — Procedural detail-surface layer for terrain: a noise-driven shader that keeps the biome-tinted base ground (from `colors`/`biomeBands`) and blends distinct rock, sand, and snow over it by slope, height, and waterline — turning a flat vertex-colour surface into varied, textured-reading ground with no image assets. +- `TerrainDetailMaterialConfig` (interface): interface TerrainDetailMaterialConfig — Real PBR texture applied over the ground surface — the seam that lets a game put a `buildMaterialCatalog` material on terrain. Blends with, never replaces, the procedural detail shader: color/roughness/ao tile the maps by world position, `strength` fades them over the existing vertex-colour + noise look. +- `TerrainEnvironmentDescriptor` (type): type TerrainEnvironmentDescriptor = { kind: "terrain" } & Required< Pick > & Omit — ⚠ undocumented +- `TerrainField` (interface): interface TerrainField — A sampleable ground surface: height and normal at any x/z, with optional bounds and water level. +- `TerrainFlattenMask` (interface): interface TerrainFlattenMask — ⚠ undocumented +- `TerrainMaterialLayer` (interface): interface TerrainMaterialLayer — One material layer in a terrain's reorderable stack: a palette `surface` id (drives the base color) plus its render parameters. Array order is the stack order — lower index paints under higher. `roughness`/`tiling`/`triplanar`/`tint`/`opacity` are carried as data so a runtime game reads them straight off the snapshot. +- `TerrainMaterialRegion` (type): type TerrainMaterialRegion = TerrainCircleRegion | TerrainPolylineRegion | TerrainRectRegion — A palette zone painted over the base terrain palette. Circle (the default when no `shape` is given), `polyline` ribbons for roads/rivers, and rotatable `rect` districts all paint fully inside their core and blend back across `falloff`; later regions in the list win overlaps. +- `TerrainPalette` (interface): interface TerrainPalette — ⚠ undocumented +- `TerrainPolylineRegion` (interface): interface TerrainPolylineRegion extends TerrainRegionStyle — A ribbon palette zone following a centerline — roads and rivers, instead of chaining overlapping circles. +- `TerrainRectRegion` (interface): interface TerrainRectRegion extends TerrainRegionStyle — A rectangular palette zone, optionally rotated about the world y axis — plazas, fields, districts. +- `TerrainRegionStyle` (interface): interface TerrainRegionStyle — Palette and blend fields shared by every `TerrainMaterialRegion` shape. +- `TerrainSurfaceRule` (interface): interface TerrainSurfaceRule — A height/slope predicate for auto-painting a surface layer (e.g. rock on steep slopes, snow up high). +- `ThreatTable` (interface): interface ThreatTable — ⚠ undocumented +- `ToneVoice` (interface): interface ToneVoice — A pitched oscillator voice: a 12ms linear attack to `gain`, then an exponential decay to silence across `duration`, with an optional exponential pitch slide from `freq` to `slideTo`. +- `VEGETATION_VOLUME_KIND` (const): const VEGETATION_VOLUME_KIND: "vegetation" — The editor volume kind that marks an area as vegetation fill. +- `Vec3` (type): type Vec3 = EntityPosition — ⚠ undocumented +- `VehicleSeats` (class): class VehicleSeats — Composes `scene/mount`'s control-transfer bookkeeping with the seat/camera/movement-mode transition every enter/exit-vehicle flow needs (#533.2): boarding resolves a free seat and reports the camera target, drive target, and rider movement-lock patch in one call; leaving computes a side-door placement next to the vehicle and reports the same triad in reverse. Pure — no entity/camera side effects — the caller applies `riderMovementPatch`/`placement`/`cameraTarget` via its own `ctx`. +- `VisibilityConfig` (interface): interface VisibilityConfig — Per-game visibility configuration, surfaced on `PlayableGame.visibility`. Everything is optional: an existing game that sets nothing gets the conservative engine defaults automatically. This is the scene-level and per-kind override seam (requirement: per-object, per-layer, per-scene, and global controls). +- `VisibilitySystem` (interface): interface VisibilitySystem — ⚠ undocumented +- `VolumetricCloudsConfig` (interface): interface VolumetricCloudsConfig — Volumetric cloud layer config for `sky()` — a raymarched cloud slab mounted from the environment `sky` seam. Pure config + defaulting here; the raymarch shader lives in the `shell` renderer (`environment/VolumetricClouds.tsx`), mounted alongside `SkyDome` whenever a sky descriptor carries this field. Off by default — omit `volumetricClouds` on `sky({...})` and no layer mounts. +- `VolumetricCloudsRules` (interface): interface VolumetricCloudsRules — Fully-defaulted volumetric cloud params, resolved from a `VolumetricCloudsConfig`. +- `VoxelFace` (type): type VoxelFace = "px" | "nx" | "py" | "ny" | "pz" | "nz" — ⚠ undocumented +- `VoxelMaterial` (interface): interface VoxelMaterial — ⚠ undocumented +- `VoxelVolume` (class): class VoxelVolume — A runtime-editable dense voxel grid — the carve/deposit op behind destructible dig worlds (Deep Rock Galactic tunnels, Astroneer terrain). Cells hold a material id (0 = empty); `carve` clears a sphere of solid cells that a tool is strong enough to break and returns how many it removed (feed that to a loot roll), `deposit` fills a sphere with a material. World↔cell mapping is `origin`+`scale`. +- `WATER_SCHEMA` (const): const WATER_SCHEMA: ParamSchema — The water parameter schema — drives the inspector and `meta` parse via the studio seam. +- `WaterRules` (interface): interface WaterRules — Fully-defaulted water surface params parsed from a volume's `meta`. +- `WaterSurface` (interface): interface WaterSurface — ⚠ undocumented +- `WaveManifest` (interface): interface WaveManifest — ⚠ undocumented +- `Waypoint` (type): type Waypoint = readonly [number, number, number] — ⚠ undocumented +- `WeatherEnvironmentDescriptor` (type): type WeatherEnvironmentDescriptor = RainEnvironmentDescriptor | SnowEnvironmentDescriptor — ⚠ undocumented +- `WeatherModifierTable` (type): type WeatherModifierTable = Record — ⚠ undocumented +- `WeatherState` (interface): interface WeatherState — ⚠ undocumented +- `WeightedParamEntry` (interface): interface WeightedParamEntry — One weighted entry in a `weightedList` param — an item id and its relative spawn weight. +- `WindField` (interface): interface WindField — ⚠ undocumented +- `WorldFeature` (type): type WorldFeature = | ({ kind: "biomes" } & BiomesWorldConfig) | ({ kind: "voxel" } & VoxelWorldConfig) | ({ kind: "plots" } & PlotsWorldConfig) | ({ kind: "tilemap" } & TilemapWorldConfig) | EnvironmentWorldFeature | { kind: "flat" } — A declared world shape — biomes, voxel grid, plots, tilemap, environment, or flat — passed to `defineGame`. +- `WorldGridCell` (interface): interface WorldGridCell — ⚠ undocumented +- `WorldGridConfig` (interface): interface WorldGridConfig — Shared by `biomes()`/`voxel()`/`plots()`/`tilemap()` so the shell can render their declared content as instanced boxes without a hand-written renderer. +- `WorldXZ` (type): type WorldXZ = readonly [number, number] — ⚠ undocumented +- `advanceBehaviors` (function): function advanceBehaviors(ctx: GameContext, dt: number): void — Advance every spawned entity carrying a `patrol` or `wander` {@link BehaviorDescriptor} one tick — the engine reads the descriptor, keeps the per-entity nav state itself, and poses the entity, so ambient traffic and idle NPC routes are register-once (attach the behavior at spawn) instead of a per-game per-frame `advancePathFollow` + `setPose` loop. The shell/host call this each frame; a game never does. +- `advancePathFollow` (function): function advancePathFollow(config: PathFollowConfig, state: PathFollowState, dt: number): PathFollowState — Advance a path-follower by `speed * dt` along its authored polyline. Pure — returns the next state. Crosses multiple waypoints in one step, loops when configured, and reports `done` at the end of a non-looping path. No navmesh required (#52); feed it a navmesh route via `pathFromNav` for click-to-move (#51). +- `advanceSpawnDirector` (function): function advanceSpawnDirector(config: SpawnDirectorConfig, state: SpawnDirectorState, dt: number, ctx: DirectorContext): DirectorStep — ⚠ undocumented +- `advanceWave` (function): function advanceWave(config: SpawnDirectorConfig, state: SpawnDirectorState): SpawnDirectorState — ⚠ undocumented +- `applyDeltaToSnapshot` (function): function applyDeltaToSnapshot(snapshot: TerraformSnapshot, delta: TerraformDelta): TerraformSnapshot — Returns a new snapshot with a delta's `after` offsets applied (copy-on-write — inputs untouched). +- `applySurfaceDeltaToSnapshot` (function): function applySurfaceDeltaToSnapshot(snapshot: TerraformSnapshot, delta: SurfaceDelta): TerraformSnapshot — Returns a new snapshot with a surface delta's `after` ids applied (copy-on-write). +- `bearingToCardinal` (function): function bearingToCardinal(bearing: number): Cardinal — ⚠ undocumented +- `beginSurfaceStroke` (function): function beginSurfaceStroke(terrain: Pick): SurfaceStroke — Opens a paint-stroke recorder over `terrain`; stamp paint edits into it, then read one net delta. +- `beginTerraformStroke` (function): function beginTerraformStroke(terrain: Pick): TerraformStroke — Opens a stroke recorder over `terrain`; stamp edits into it, then read one net delta. +- `biomes` (function): function biomes(config: BiomesWorldConfig): WorldFeature — Declares a biome-painted world — the whole-world alternative to a single `environment()` terrain. +- `boundaryNeighbors` (function): function boundaryNeighbors(grid: FootprintGrid, cells: readonly GridCell[]): AdjacentCell[] — Every occupied cell orthogonally touching `cells` but outside them — the connective-piece neighbor set. +- `buildContextMenu` (function): function buildContextMenu(input: BuildContextMenuInput): ContextMenu | null — Assemble a menu from a target's catalog verbs; null when the target lists none. +- `buildRoadRibbon` (function): function buildRoadRibbon(path: readonly RoadPoint[], width: number, sampleHeight: (x: number, z: number) => number, options: RoadRibbonOptions = {}): RoadRibbon — Triangulate a road centerline into a ground-draped ribbon mesh: the polyline is subdivided, each vertex is offset half a `width` along the local perpendicular, and every vertex sits at `sampleHeight(x, z) + elevation`. Pure geometry — the shell (or any renderer) turns the result into a mesh, and tests can assert on it directly. +- `building` (function): function building(config: BuildingEnvironmentConfig = {}): BuildingEnvironmentDescriptor — Declares a cluster of procedurally-massed buildings for `environment()` — count, footprint, stories, style. +- `buildingIndex` (function): function buildingIndex(buildings: readonly GeneratedBuilding[]): BuildingIndex — ⚠ undocumented +- `carrySpeedMultiplier` (function): function carrySpeedMultiplier(mass: number, carryCapacity: number, owners: number): number — Movement multiplier (1 = unhindered, →0 = crushed) for a body of `mass` carried by `owners`. Pure — the HUD/movement kit reads it to slow a laden hauler (Lethal Company) and to gate items that need 2+ people (R.E.P.O.). +- `carvableTerrain` (function): function carvableTerrain(base: TerrainField): CarvableField — ⚠ undocumented +- `catenaryCurve` (function): function catenaryCurve(a: Vec3, b: Vec3, slack: number, segments: number): Vec3[] — True hyperbolic catenary between two anchors — the shape a uniform cable actually takes under gravity. `slack` is the extra length beyond the straight-line distance, as a fraction (0.1 = 10% longer than taut); larger slack droops deeper. Falls back to {@link sagCurve} for a near-taut cable. Returns `segments + 1` points. Anchors may differ in height; the curve interpolates the chord. +- `clampToMinimapEdge` (function): function clampToMinimapEdge(point: MinimapPoint, size: number): { x: number; y: number } — Clamp a projected point to the minimap edge, preserving direction (edge markers). +- `clearanceZonesFrom` (function): function clearanceZonesFrom(doc: SceneDocumentLike, options: ClearanceOptions = {}): AvoidZone[] — Point-pad clearance **discs** from a document's markers/volumes — the terrain-flatten set (spawns, plots, POIs get a level pad). A marker/volume contributes a disc when it carries `meta.clearance` or its kind is in `kinds`. Paths are *not* included (they render draped, never flattened — see {@link clearanceMasksFrom} for their foliage corridor). Pass `ids`/`kinds` to scope it. +- `command` (function): function command(name: string, input?: unknown): PromptCommand — ⚠ undocumented +- `compassBearing` (function): function compassBearing(from: WorldXZ, to: WorldXZ): number — Compass bearing (radians, 0 = map north = −Z, increasing clockwise toward +X = east) from one world XZ point to another. Feeds both the minimap direction and the compass strip. +- `composeRealm` (function): function composeRealm(base: RealmBase, cards: readonly RealmCard[]): ComposedRealm — Assemble a played realm instance at runtime from a deck of modifier cards — the Nightingale "realm card" model. A major card is the biome base; minor cards layer weather, day length, and spawn edits. The result recomposes both the environment (into a sampleable field via `environmentField()`) and the spawn table, and it depends on the weather hooks in this group (#92) to turn its `weather` into gameplay modifiers. Cards apply in array order; sort your deck (majors first) before composing. +- `computeFalloffGain` (function): function computeFalloffGain(distance: number, config: AudioFalloffConfig = {}): number — ⚠ undocumented +- `constrainToNavGrid` (function): function constrainToNavGrid(grid: NavGrid, options?: NavConstrainOptions): (proposed: NavConstrainProposed, entity: NavConstrainEntity) => NavConstrainProposed | null — ⚠ undocumented +- `contextVerb` (function): function contextVerb(label: string, command: string, args?: Record): ContextVerb — Builds a {@link ContextVerb} for a right-click menu entry. +- `contextVerbInput` (function): function contextVerbInput(menu: ContextMenu, verb: ContextVerb): Record — Command input a chosen verb dispatches: the verb's own args, plus the target id and the world point, so a single handler can walk the actor to the target then perform it. +- `createAssetCatalog` (function): function createAssetCatalog(): AssetCatalog — ⚠ undocumented +- `createBallisticSweep` (function): function createBallisticSweep(world: PhysicsWorld, options: BallisticSweepOptions = {}): BallisticSweep — Marches the closed-form arc (constant gravity, straight lateral) through `world` and reports the first sample inside any live body's AABB — sleeping bodies included — refined by one bisection between the last clear sample and the hit sample. Returns `null` when the whole arc is clear. +- `createBodyBind` (function): function createBodyBind(deps: BodyBindDeps): BodyBind — Mirror a sim's body snapshots onto scene entities each tick — spawn on first sight, pose while bound, despawn on drop — replacing a per-body `setPose` loop plus its `despawn`/`spawn` respawn dance. +- `createBuoyantBody` (function): function createBuoyantBody(world: PhysicsWorld, config: BuoyantBodyConfig): BuoyantBody — ⚠ undocumented +- `createContributionPool` (function): function createContributionPool(goal: ContributionGoal): ContributionPool — ⚠ undocumented +- `createDamageModel` (function): function createDamageModel(config: DamageModelConfig): DamageModel — ⚠ undocumented +- `createEditableTerrain` (function): function createEditableTerrain(config: EditableTerrainConfig): EditableTerrain — ⚠ undocumented +- `createEnvironmentField` (function): function createEnvironmentField(config: EnvironmentFieldConfig = {}): EnvironmentField — A sampleable environment field: read temperature, wetness, sun/sky exposure, and ambient light at any world position and time. Built on the same renderer-free footing as terrain/wind/water so meters, spawn gating, and damage-in-sunlight read the world the shell renders — no three.js. Instantaneous and pure (no accumulation); stateful build-up belongs to a decay meter reading this field. +- `createFactionGraph` (function): function createFactionGraph(config: FactionGraphConfig): FactionGraph — ⚠ undocumented +- `createFactionRoster` (function): function createFactionRoster(graph: FactionGraph): FactionRoster — ⚠ undocumented +- `createFireGrid` (function): function createFireGrid(config: FireGridConfig): FireGrid — ⚠ undocumented +- `createFogField` (function): function createFogField(config: FogConfig): FogField — ⚠ undocumented +- `createFootprintGrid` (function): function createFootprintGrid(options: FootprintGridOptions = {}): FootprintGrid — Multi-cell footprint occupancy/reservation on a shared build grid — `world/placementController` only owns the ghost preview; this is the persistent claim a committed placement holds so the next hover's `isFree` check (or another player's, in a shared world) sees it. Bridge into `world/placement`'s `PlacementRules.obstacles` with {@link footprintObstacles} instead of hand-rolling an occupancy map per game. +- `createGlideModel` (function): function createGlideModel(config: GlideModelConfig = {}): GlideModel — Gliding/wingsuit descent control — lift, drag, and steering from a launch. +- `createGrappleSwing` (function): function createGrappleSwing(config: GrappleSwingConfig = {}): GrappleSwing — Grappling-hook rope swing physics with anchor, pendulum motion, and reel-in. +- `createKinematicVehicle` (function): function createKinematicVehicle(tuning: KinematicVehicleTuning, options: KinematicVehicleOptions = {}): KinematicVehicle — ⚠ undocumented +- `createLeaderTrail` (function): function createLeaderTrail(config: LeaderTrailConfig): LeaderTrail — A trailing follower formation that chases a leader along its past path — snake/convoy trails. +- `createLodScheduler` (function): function createLodScheduler(config: LodSchedulerConfig): LodScheduler — ⚠ undocumented +- `createMarkerSet` (function): function createMarkerSet(now: () => number = Date.now): MarkerSet — ⚠ undocumented +- `createMountController` (function): function createMountController(): MountController — ⚠ undocumented +- `createNavGrid` (function): function createNavGrid(config: NavGridConfig): NavGrid — ⚠ undocumented +- `createPathFollow` (function): function createPathFollow(config: PathFollowConfig): PathFollowState — ⚠ undocumented +- `createPlacedStructureStore` (function): function createPlacedStructureStore(): PlacedStructureStore — ⚠ undocumented +- `createPlacementController` (function): function createPlacementController(config: PlacementControllerConfig): PlacementController — ⚠ undocumented +- `createPlotPermissions` (function): function createPlotPermissions(config: PlotPermissionConfig): PlotPermissions — ⚠ undocumented +- `createPoseState` (function): function createPoseState(resolveAllowed: (instanceId: string) => PoseAllowedStates | null | undefined): PoseState — Stance/pose transitions — stand, crouch, prone — that change the hitbox and movement. +- `createRagdoll` (function): function createRagdoll(world: PhysicsWorld, config: RagdollConfig): Ragdoll — ⚠ undocumented +- `createRegionField` (function): function createRegionField(config: RegionFieldConfig): RegionField — ⚠ undocumented +- `createReputationLedger` (function): function createReputationLedger(config: ReputationLedgerConfig = {}): ReputationLedger — ⚠ undocumented +- `createSelectionSet` (function): function createSelectionSet(initial?: Iterable): SelectionSet — An ordered, deduplicated set of selected instance ids for RTS unit-command routing. +- `createSpawnDirectorState` (function): function createSpawnDirectorState(config: SpawnDirectorConfig): SpawnDirectorState — ⚠ undocumented +- `createStationClaim` (function): function createStationClaim(controller?: MountController): StationClaim — ⚠ undocumented +- `createTerraformBrush` (function): function createTerraformBrush(terrain: Pick, config: TerraformBrushConfig = {}): TerraformBrush — ⚠ undocumented +- `createTerrainSnapshot` (function): function createTerrainSnapshot(config: EditableTerrainConfig): TerraformSnapshot — A fresh, unedited terrain snapshot sized to `bounds`/`cellSize` — the seed for a new sculpt document. +- `createThreatTable` (function): function createThreatTable(config: ThreatTableConfig = {}): ThreatTable — ⚠ undocumented +- `createVehicleBody` (function): function createVehicleBody(world: PhysicsWorld, config: VehicleBodyConfig): VehicleBody — ⚠ undocumented +- `createVehicleSeats` (function): function createVehicleSeats(controller?: MountController): VehicleSeats — Builds a {@link VehicleSeats}, optionally over an existing `MountController` to share its occupancy. +- `createVisibilitySystem` (function): function createVisibilitySystem(options: VisibilitySystemOptions): VisibilitySystem — ⚠ undocumented +- `createVoxelField` (function): function createVoxelField(config?: VoxelFieldConfig): VoxelField — ⚠ undocumented +- `dashSegments` (function): function dashSegments(path: readonly RoadPoint[], dashLength = 3, gapLength = 3): readonly (readonly RoadPoint[])[] — Split a centerline into dash sub-polylines for lane markings: `dashLength` of painted line, `gapLength` of asphalt, repeated along the path's arc length. Feed each returned sub-path back through {@link buildRoadRibbon} with a thin width to mesh the dashes. +- `distance` (function): function distance(a: Vec3, b: Vec3): number — ⚠ undocumented +- `distance3` (function): function distance3(a: { x: number; y: number; z: number }, b: { x: number; y: number; z: number }): number — ⚠ undocumented +- `distanceToPolygonEdge` (function): function distanceToPolygonEdge(point: Vec2, polygon: readonly Vec2[]): number — Shortest distance from a point to a polygon's boundary. +- `editableTerrainFromSnapshot` (function): function editableTerrainFromSnapshot(snapshot: TerraformSnapshot, base?: TerrainField): EditableTerrain — Rebuilds a live {@link EditableTerrain} from a snapshot, layered over `base` ground. +- `effectiveRelation` (function): function effectiveRelation(input: EffectiveRelationInput): FactionRelation — ⚠ undocumented +- `entityMetaOf` (function): function entityMetaOf(entity: SceneEntity, isMeta: (value: unknown) => value is T): T | null — Narrow `entity.meta` with a type guard — prefer this over `entity.meta as T` so failed shapes return `null` instead of lying to the type checker. +- `environment` (function): function environment(config: EnvironmentWorldConfig = {}): EnvironmentWorldFeature — Composes an `environment()` world feature from terrain, sky, weather, vegetation, water, structures, roads, and pads. +- `evaluateQteSequence` (function): function evaluateQteSequence(steps: readonly QteStep[], inputs: readonly QteInputEvent[]): QteOutcome — Evaluate a quick-time-event input sequence against timed hit windows. +- `evaluateSkillCheck` (function): function evaluateSkillCheck(config: SkillCheckConfig, elapsedSeconds: number): SkillCheckResult — ⚠ undocumented +- `findPath` (function): function findPath(grid: NavGrid, from: NavPoint, to: NavPoint, options: FindPathOptions = {}): NavPoint[] | null — A* over the walkable grid. Returns a polyline of world-space `[x, z]` waypoints from `from` to `to`, or `null` when no route exists. Blocked start/goal snap to the nearest walkable cell so a click on an obstacle still routes to its edge. +- `firstImpact` (function): function firstImpact(hits: readonly SceneRaycastHit[]): SceneRaycastHit | null — First impact: nearest hit that blocks, or nearest hit if none block. +- `flat` (function): function flat(): WorldFeature — Declares an empty flat world — the minimal `WorldFeature` for games with no terrain of their own. +- `footprintObstacles` (function): function footprintObstacles(grid: FootprintGrid): PlacementObstacle[] — Bridges live reservations into `world/placement`'s `PlacementRules.obstacles` so `validatePlacement`/`createPlacementController` see the grid's committed footprints unchanged. +- `furnitureSpots` (function): function furnitureSpots(road: RoadEnvironmentDescriptor, options: FurnitureSpotOptions = {}): readonly FurnitureSpot[] — Evenly spaced street-furniture anchors along a road's curb lines — streetlights, palms, signs, hydrants, benches. Each spot sits just outside the asphalt (plus `outset`), faces away from the street, and alternates sides by default so lights stagger like a real avenue. This is the answer to "where do I put it": furniture is an asset of the street, never a hand-typed coordinate. +- `gauge` (function): function gauge(gaugeId: string): GaugePromptDisplay — ⚠ undocumented +- `generateLock` (function): function generateLock(seed: string | number, tier: LockTierSpec): LockSpec — Generate a solvable depth-puzzle lock: a "Tumbler's Path" board with a guaranteed solution path carved first, an open-row forgiveness band wrapped around it, tumbler gate columns that pinch to a single exact row, and optional ward-traps that look open but jam on contact. Deterministic: the same (seed, tier) always yields the same board. +- `getCurrentGameTimestamp` (function): function getCurrentGameTimestamp(createdAt: number, now: number, timeScale?: number | null): number — ⚠ undocumented +- `grass` (function): function grass(config: GrassEnvironmentConfig = {}): GrassEnvironmentDescriptor — Declares a grass vegetation patch for `environment()` — area, blade sizing, density, and colors. +- `groundSpeed` (function): function groundSpeed(entity: SceneEntity): number — Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code. +- `hasValidAdjacency` (function): function hasValidAdjacency(grid: FootprintGrid, cells: readonly GridCell[], accepts: (neighborKind: string) => boolean, requireConnection = false): boolean — Connective-piece adjacency validity: every occupied neighbor of `cells` must satisfy `accepts` (no incompatible piece touching), and when `requireConnection` is true at least one neighbor must (a road/pipe/belt segment placed with nothing to connect to is invalid). An empty-bordered footprint (no occupied neighbors at all) passes unless `requireConnection` demands one. +- `headingToBearing` (function): function headingToBearing(yaw: number): number — Bearing of an entity facing direction given its `rotationY` (yaw) in radians. +- `hitsUntilBlocked` (function): function hitsUntilBlocked(hits: readonly SceneRaycastHit[]): SceneRaycastHit[] — Hits up to and including the first blocking collider (damage hitboxes before a wall stay). +- `isMarquee` (function): function isMarquee(rect: ScreenRect, thresholdPx = 4): boolean — True when the drag is large enough to be a marquee rather than a click. +- `isRegionField` (function): function isRegionField(field: TerrainField): field is RegionField — ⚠ undocumented +- `isScatterPath` (function): function isScatterPath(path: ScenePathLike): boolean — True when an editor path is a foliage/scatter region. +- `keybind` (function): function keybind(actionId: string, label?: string): KeybindPromptDisplay — ⚠ undocumented +- `label` (function): function label(text: string): LabelPromptDisplay — ⚠ undocumented +- `laneCenters` (function): function laneCenters(road: RoadEnvironmentDescriptor): readonly [StreetLane, StreetLane] — Two right-hand-traffic lane centerlines for a road — each offset a quarter of the drivable width from the center and ordered in its direction of travel. Feed a lane's `path` straight into `nav/pathFollow` for traffic AI, or use its endpoints as directed car spawn points. +- `mapLayerColor` (function): function mapLayerColor(tone: MapLayerTone | undefined): string — ⚠ undocumented +- `markerKindStyle` (function): function markerKindStyle(kind: string, styles: Record = DEFAULT_MARKER_KINDS): MarkerKindStyle — ⚠ undocumented +- `migrateTerrainSnapshot` (function): function migrateTerrainSnapshot(snapshot: TerraformSnapshot): TerraformSnapshot — Upgrades a pre-2.0 snapshot in place-safe (copy-on-write) form: derives a {@link TerrainMaterialLayer} stack from the distinct painted surfaces (first-seen order, default params) when none exists. Leaves the lazy `weights` buffer absent — a single-layer terrain stays compact until blended. Idempotent: a snapshot that already carries `layers` is returned unchanged. +- `mtof` (function): function mtof(midi: number): number — Standard equal-temperament MIDI-to-frequency (A4 = 440 Hz at MIDI 69). +- `notesInWindow` (function): function notesInWindow(theme: MusicTheme, anchorSec: number, fromSec: number, toSec: number): ScheduledNote[] — Pure lookahead scheduler: every note occurrence of `theme` whose onset falls in the half-open window `(fromSec, toSec]`, given the theme's loop-zero at `anchorSec`. Handles any number of loop wraps, so a director calls it once per tick with a non-overlapping window and never double-schedules a note. +- `objectVisualScale` (function): function objectVisualScale(visual: ObjectVisual | undefined): readonly [number, number, number] — ⚠ undocumented +- `ocean` (function): function ocean(config: OceanEnvironmentConfig = {}): OceanEnvironmentDescriptor — Declares an ocean water body for `environment()` — bounds, level, and wave tuning. +- `offsetPath` (function): function offsetPath(path: readonly RoadPoint[], offset: number): readonly RoadPoint[] — Offset a centerline sideways by a signed distance along its local perpendicular — the building block for lanes, curb lines, and sidewalk paths. Positive offsets fall on the left of the direction of travel, negative on the right. +- `pad` (function): function pad(config: PadEnvironmentConfig): PadEnvironmentDescriptor — ⚠ undocumented +- `parkingSpots` (function): function parkingSpots(road: RoadEnvironmentDescriptor, options: ParkingSpotOptions = {}): readonly ParkingSpot[] — Curbside parking anchors along a road: hugging the edge of the asphalt, headed parallel to the street in that side's direction of travel. Spawn parked vehicles here instead of eyeballing coordinates in the middle of the carriageway. +- `parseParams` (function): function parseParams(schema: ParamSchema, meta: Record | undefined): ParsedParams — Parse a raw `meta` bag against a schema into typed params — every field present, invalid/missing values replaced by the field default, numbers clamped to their range. The single parser every studio shares instead of hand-writing its own `metaNumber`/`metaBool` ladder. +- `partsBounds` (function): function partsBounds(parts: readonly GeneratedPart[]): GeneratedAsset["bounds"] — Compute bounds from parts (each part is an axis-aligned box at its center) — a helper generators return so callers can frame/ground the asset without re-deriving it. +- `pathFromNav` (function): function pathFromNav(points: readonly NavPoint[], elevation: number | HeightSampler = 0, offset = 0): Waypoint[] — ⚠ undocumented +- `patrol` (function): function patrol({ waypoints, speed, loop = true, }: { waypoints: readonly Waypoint[]; speed: number; loop?: boolean; }): PatrolBehavior — ⚠ undocumented +- `pendingQteStep` (function): function pendingQteStep(steps: readonly QteStep[], elapsedSeconds: number): QteStep | null — ⚠ undocumented +- `pickSpawnPoint` (function): function pickSpawnPoint(options: SpawnPointSelectionOptions): NavPoint | null — Selects a candidate spawn point using a semantic distance preference and caller-supplied randomness. +- `pickWeighted` (function): function pickWeighted(entries: readonly { value: T; weight: number }[], roll: number): T | null — Weighted pick from opaque entries; `roll` in [0, 1). Returns null when empty. +- `placeAlongPath` (function): function placeAlongPath(points: readonly { x: number; z: number }[], options: PlaceAlongPathOptions): PathInstance[] — Evenly place transforms along `points` (XZ polyline). The run length is divided into the whole number of equal spans closest to `spacing`, so instances always land on both endpoints and stay evenly distributed. Returns `spans + 1` instances. Empty for fewer than 2 points. +- `player` (function): function player(): PlayerBehavior — ⚠ undocumented +- `plots` (function): function plots(config: PlotsWorldConfig = {}): WorldFeature — Declares a subdivided-plots world — farming, base-building, and other parcel-based layouts. +- `pointInPolygon` (function): function pointInPolygon(point: Vec2, polygon: readonly Vec2[]): boolean — Ray-casting point-in-polygon test on the XZ plane. +- `polygonArea` (function): function polygonArea(polygon: readonly Vec2[]): number — Shoelace area of a polygon (always non-negative), in square meters. +- `polygonBounds` (function): function polygonBounds(polygon: readonly Vec2[]): Aabb | null — Axis-aligned bounds of a polygon, or null if it has no points. +- `populateNavGridFromEnvironment` (function): function populateNavGridFromEnvironment(grid: NavObstacleGrid, world: EnvironmentWorldFeature): number — Expands every structure descriptor on an environment world feature into its generated buildings and blocks their footprints on `grid`. Returns the number of buildings blocked. +- `projectToMinimap` (function): function projectToMinimap(world: WorldXZ | readonly [number, number, number], view: MinimapView): MinimapPoint — Project a world XZ (or XYZ) point into minimap pixel space. Origin is the top-left of the `size×size` box; north (−Z) maps to −Y (up). Pass `view.rotate` to spin the map under a fixed north-up player arrow. +- `proximityPrompt` (function): function proximityPrompt({ radius, display, invoke = null }: ProximityPromptConfig): ProximityPrompt — ⚠ undocumented +- `quarterTurnsToRotationY` (function): function quarterTurnsToRotationY(quarterTurns: number): number — ⚠ undocumented +- `rain` (function): function rain(config: RainEnvironmentConfig = {}): RainEnvironmentDescriptor — Declares a rainfall weather effect for `environment()` — area, density, speed, wind, and drop width/opacity. +- `raiseAlert` (function): function raiseAlert(state: SpawnDirectorState, amount: number): SpawnDirectorState — ⚠ undocumented +- `readNamedSockets` (function): function readNamedSockets(root: ModelNode, pattern: RegExp = SOCKET_PATTERN): ModelSocket[] — Depth-first collect every socket-named node's local offset, sorted by descending Y then ascending X so socket indices are stable across loads (top first, left-to-right). Empty when the model tags none — callers then fall back to computed offsets. Pass a custom `pattern` for a bespoke naming convention. +- `readScatterPalette` (function): function readScatterPalette(meta: Record | undefined): ScatterPaletteEntry[] — Parses a scatter region's palette from meta: a weighted `palette` array, else a single `item`. +- `readScatterRules` (function): function readScatterRules(path: ScenePathLike): ScatterRegionRules | null — The path's scatter rules with defaults filled in; null for non-scatter paths. +- `registerAssetGenerator` (function): function registerAssetGenerator(definition: AssetGeneratorDefinition): void — Register a parametric asset generator. Idempotent per id (last wins); call at module load. +- `registerSceneKind` (function): function registerSceneKind(definition: SceneKindDefinition): void — Register a scene kind — the plug-in point for a new parametric studio. Idempotent per `kind` (last registration wins), so a game's registration overrides a default. Call at module load; the editor inspector, `+ Add` menu, and `AuthoredScene` renderer lookup all read this registry. +- `relativeBearing` (function): function relativeBearing(bearing: number, reference: number): number — Signed offset of `bearing` from `reference`, wrapped into (−π, π]. +- `resolveActivePrompt` (function): function resolveActivePrompt(playerPosition: PromptPoint, prompts: readonly T[]): T | null — Nearest prompt strictly within its radius wins; a higher-priority prompt in range beats any lower-priority one regardless of distance; equal priority and distance keep the earliest prompt in the list. +- `resolveEmitterGain` (function): function resolveEmitterGain(distance: number, sound: Pick, busGain: number): number — ⚠ undocumented +- `resolveGridInstances` (function): function resolveGridInstances(config: WorldGridConfig | GridWorldFeature): readonly GridInstanceTransform[] — ⚠ undocumented +- `resolvePlayerMovementTuning` (function): function resolvePlayerMovementTuning(opts: { collision?: VoxelCollisionConfig; movement?: PlayerMovementConfig; physics?: PhysicsConfig; world?: WorldFeature; }): PlayerMovementTuning — Gather a game's collision/movement/physics/world config into a {@link PlayerMovementTuning} — call once per world; both the shell and a host pass the result to {@link stepPlayerMovement}. +- `resolveScatter` (function): function resolveScatter(doc: SceneDocumentLike, terrain?: ScatterTerrain, options: ResolveScatterOptions = {}): ScatterInstance[] — Every scatter region's placements across a document, grounded on `terrain` when provided. Regions honor clearance masks: their own manual `avoid` discs, plus (when the region's `autoAvoid` is on and `options.autoAvoid !== false`) the document-wide discs + path corridors from {@link clearanceMasksFrom} — so foliage auto-clears spawns, plots, and paths without hand-carving the polygon. +- `resolveScatterRegion` (function): function resolveScatterRegion(region: ScatterRegion, terrain?: ScatterTerrain, avoid?: AvoidMasks): ScatterInstance[] — Deterministic placements for one scatter region: scatter its polygon footprint at `density` items/m² (respecting `minSpacing`), clip to the polygon, thin near the edge, drop placements outside the slope/height mask, and derive item/scale/yaw from the region id + seed — so the same saved region always grows the same field. Grounds each instance on `terrain` when provided. +- `resolveStructureBuildings` (function): function resolveStructureBuildings(descriptor: BuildingEnvironmentDescriptor): GeneratedBuilding[] — ⚠ undocumented +- `resolveWeather` (function): function resolveWeather(state: WeatherState, table: TTable): ResolvedWeather — ⚠ undocumented +- `revertDeltaFromSnapshot` (function): function revertDeltaFromSnapshot(snapshot: TerraformSnapshot, delta: TerraformDelta): TerraformSnapshot — Returns a new snapshot with a delta's `before` offsets restored (copy-on-write undo). +- `revertSurfaceDeltaFromSnapshot` (function): function revertSurfaceDeltaFromSnapshot(snapshot: TerraformSnapshot, delta: SurfaceDelta): TerraformSnapshot — Returns a new snapshot with a surface delta's `before` ids restored (copy-on-write undo). +- `road` (function): function road(config: RoadEnvironmentConfig): RoadEnvironmentDescriptor — Declare a road ribbon for an `environment()` world; the shell drapes and renders it over the terrain. +- `sagCurve` (function): function sagCurve(a: Vec3, b: Vec3, sag: number, segments: number): Vec3[] — Quadratic-Bézier sag between two anchors: the control point is pulled straight down so the mid-span lowest point droops by exactly `sag` meters below the chord. Cheap and stable; the go-to for cables where exact catenary physics don't matter. Returns `segments + 1` points. +- `sampleGripCurve` (function): function sampleGripCurve(curve: GripCurve, slip: number): number — Piecewise-linear tire-grip curve: normalized lateral slip → available grip (0..1). Grip peaks near the breakaway slip then falls off as the tire slides — the shape that separates a planted corner from a drift. Points are read in ascending slip order; ends clamp. +- `sanitizeGameTimeScale` (function): function sanitizeGameTimeScale(timeScale?: number | null): number — ⚠ undocumented +- `scatter` (function): function scatter(config: ScatterConfig): ScatterPoint[] — ⚠ undocumented +- `scatterItems` (function): function scatterItems(field: RegionField, area: Aabb, layersFor: (sample: RegionSample) => readonly ScatterLayer[], options: { cell?: number; max?: number; saltKey?: number } = {}): ScatterInstance[] — Deterministically place opaque items across `area`, grounded on a region field. For each grid cell it asks `layersFor` which items may appear in that region and rolls one against their densities. The engine never interprets `item` — a game maps it to a mesh or entity. Content scatter (region-driven density) as opposed to `scatter` in `./scatter`, which is renderer-free geometric point distribution. +- `scatterRegionEstimate` (function): function scatterRegionEstimate(path: ScenePathLike): { area: number; count: number } — Estimated placement count for a scatter path — density × polygon area, for a live UI readout. +- `scatterRegionFromPath` (function): function scatterRegionFromPath(path: ScenePathLike): ScatterRegion | null — Builds a resolvable {@link ScatterRegion} from a scatter path (XZ polygon + rules), or null. +- `screenRect` (function): function screenRect(ax: number, ay: number, bx: number, by: number): ScreenRect — Normalize two drag corners (in any order) into a rectangle. +- `selectAutoTarget` (function): function selectAutoTarget(policy: AutoTargetPolicy, fromId: string, deps: AutoTargetDeps): string | null — ⚠ undocumented +- `selectWithinRect` (function): function selectWithinRect(candidates: readonly ScreenPoint[], rect: ScreenRect): string[] — Ids of the projected candidates whose screen point falls inside the marquee. +- `sidewalkPoint` (function): function sidewalkPoint(road: RoadEnvironmentDescriptor, side: "left" | "right", fraction: number): RoadPoint | null — A deterministic point on one of a road's sidewalks at a normalized position — `side` picks the band, `fraction` (0..1) picks how far along. The canonical pedestrian spawn helper. +- `sidewalkWidthOf` (function): function sidewalkWidthOf(road: RoadEnvironmentDescriptor): number — Resolved sidewalk band widths for a road; zero when the road declares no sidewalk. +- `skillCheckZoneAt` (function): function skillCheckZoneAt(config: SkillCheckConfig, elapsedSeconds: number): SkillCheckZone — A timing-bar skill check that succeeds when the moving marker is released inside the target zone. +- `sky` (function): function sky(config: SkyEnvironmentConfig = {}): SkyEnvironmentDescriptor — ⚠ undocumented +- `slopeStepCost` (function): function slopeStepCost(field: { sampleHeight(x: number, z: number): number }, weight = DEFAULT_SLOPE_STEP_WEIGHT): (from: NavPoint, to: NavPoint) => number — `FindPathOptions.stepCost` factory that penalizes steep terrain: cost is `1 + weight * |Δheight| / horizontalDistance`, so with the default weight a 45° slope roughly doubles the step cost. +- `snapToNearest` (function): function snapToNearest(registry: ConnectorRegistry, placed: readonly PlacedPiece[], movingDef: ConnectorPieceDef, cursor: ConnectorVec3, options: SnapOptions = {}): SnapResult | null — ⚠ undocumented +- `snow` (function): function snow(config: SnowEnvironmentConfig = {}): SnowEnvironmentDescriptor — Declares a snowfall weather effect for `environment()` — area, density, drift, wind, and flake opacity. +- `socketWorldPosition` (function): function socketWorldPosition(socket: ConnectorSocket, origin: ConnectorVec3, rotationY: number): ConnectorVec3 — ⚠ undocumented +- `socketsCompatible` (function): function socketsCompatible(a: ConnectorSocket, b: ConnectorSocket): boolean — ⚠ undocumented +- `solveLock` (function): function solveLock(spec: LockSpec): boolean — Whether the board has a path from the start row to the bolt seat at all. +- `solveLockPath` (function): function solveLockPath(spec: LockSpec): number[] | null — Return a concrete row-per-column solution, or null if the board is unsolvable. +- `solveSupport` (function): function solveSupport(pieces: readonly SupportPiece[], links: readonly SupportLink[], config: SupportConfig = {}): SupportResult — ⚠ undocumented +- `steerYaw` (function): function steerYaw(yaw: number, steerRight: number, turnRatePerSecond: number, dt: number): number — Integrate one steering step. `steerRight` is the signed steer input (+1 = turn right, matching `DRIVE_AXIS_BINDINGS`' KeyD/ArrowRight), `turnRatePerSecond` is radians per second at full lock. Steering right decreases yaw in the engine frame; this helper owns that sign so game code never re-derives it. +- `stepLock` (function): function stepLock(spec: LockSpec, col: number, row: number, action: LockAction): { result: LockStepResult; col: number; row: number } — Authoritative single step. The caller owns the lives economy: a slip/bind/trap does not advance the pick and should cost a life; advanced/success move the pick. +- `stepPlayerMovement` (function): function stepPlayerMovement(ctx: GameContext, userId: string, input: InputFrame, dt: number, tuning: PlayerMovementTuning, heading?: number): void — Integrate one player's movement for a tick from their held-input frame and commit the pose — the single genre-agnostic controller both the shell (its local player) and a host (each connected player in `onTick`) call, so single-player and server-authoritative movement are identical. Reads the player's controlled entity, terrain, scene solids, and pending motion impulses; writes the entity pose via `setPose`. Retains heading + kinematic body per `userId` on the `ctx`. Pass `heading` to override the internally-integrated yaw (the shell owns yaw for its camera); omit it and the controller turns from the frame's `turnLeft`/`turnRight` actions. +- `summarizeEnvironment` (function): function summarizeEnvironment(feature: EnvironmentWorldFeature): EnvironmentSummary — ⚠ undocumented +- `talkable` (function): function talkable(dialogueId: string): PromptableBehavior — ⚠ undocumented +- `terrain` (function): function terrain(config: TerrainEnvironmentConfig = {}): TerrainEnvironmentDescriptor — Declares a heightfield terrain patch for `environment()` — bounds, noise, materials, and flatten masks. +- `themeLoopSeconds` (function): function themeLoopSeconds(theme: MusicTheme): number — Loop length of a theme in seconds. +- `tickDrivableVehicle` (function): function tickDrivableVehicle(vehicle: KinematicVehicle, dt: number, axis: AxisInput, options: DrivableVehicleOptions = {}): DrivableVehicleStep — Connects an `AxisInput` sample straight through a {@link KinematicVehicle} to a scene entity's pose for one tick (#533.1) — the throttle/steer/handbrake → sim → `setPose` loop every drivable-vehicle game hand-rolled. Ground-snaps the result when `groundHeight` is given (terrain-following cars, not just flat racetracks). Pair with `scene/vehicleSeat` for who is allowed to drive and where the camera points; this function only steps the sim and shapes the pose patch, nothing else. +- `tierForStanding` (function): function tierForStanding(tiers: readonly ReputationTier[], standing: number): ReputationTier — Map a faction standing value to its named reputation tier. +- `tilemap` (function): function tilemap(config: TilemapWorldConfig): WorldFeature — Declares a 2D tilemap world from a map string. +- `toDebrisBodies` (function): function toDebrisBodies(pieces: readonly SupportPiece[], collapsedIds: readonly string[], options: DebrisOptions = {}): AddBodyOptions[] — ⚠ undocumented +- `unprojectFromMinimap` (function): function unprojectFromMinimap(point: { x: number; y: number }, view: MinimapView): WorldXZ — Invert `projectToMinimap` (#285.6): minimap pixel → world XZ, rotate-aware — click-to-pin, tap-to-ping, drag-to-set-waypoint map interactions. +- `validatePlacement` (function): function validatePlacement(request: PlacementRequest, rules: PlacementRules = {}): PlacementResult — ⚠ undocumented +- `visibleCells` (function): function visibleCells(spec: LockSpec, col: number, window: number): LockCell[] — The render-safe slice: every open cell in columns [0, col + window]. The single source of truth for fog and the anti-cheat boundary — never serialize the full spec to a client. +- `voxel` (function): function voxel(config: VoxelWorldConfig): WorldFeature — Declares a voxel-grid world for block-based games. +- `wander` (function): function wander({ radius }: { radius: number }): WanderBehavior — ⚠ undocumented +- `waterSurface` (function): function waterSurface(config: WaterSurfaceConfig = {}): WaterSurface — ⚠ undocumented +- `waterSurfaceFromDescriptor` (function): function waterSurfaceFromDescriptor(descriptor: OceanEnvironmentDescriptor, waves?: number): WaterSurface — ⚠ undocumented +- `windField` (function): function windField(config: WindFieldConfig = {}): WindField — ⚠ undocumented +- `worldSockets` (function): function worldSockets(def: ConnectorPieceDef, piece: PlacedPiece): WorldSocket[] — ⚠ undocumented + ## @jgengine/shell/cartridge - `CartridgeAbilitySlot` (interface): interface CartridgeAbilitySlot — ⚠ undocumented diff --git a/.claude/skills/jgengine/capabilities.md b/.claude/skills/jgengine/capabilities.md index 1791fcee0..806e7d0a0 100644 --- a/.claude/skills/jgengine/capabilities.md +++ b/.claude/skills/jgengine/capabilities.md @@ -4,10 +4,339 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*. -## loot-table — validate a loot table definition for use with the registry +## ability-bar — a bar of cooldown-gated abilities the player triggers by slot -- `lootTable` (function) · `import { lootTable } from "@jgengine/core/authoring"` +- `createAbilityKit` (function) · `import { createAbilityKit } from "@jgengine/core/combat"` + +## behavior-tick — auto-advance patrol/wander behaviors on spawned entities, no per-game route loop + +- `advanceBehaviors` (function) · `import { advanceBehaviors } from "@jgengine/core/world"` + +## best-record — persist personal-best times/scores with safe storage fallback + +- `createRecordBook` (function) · `import { createRecordBook } from "@jgengine/core/gameplay"` + +## body-bind — mirror sim-body snapshots onto scene entities each tick, no per-body setPose + +- `createBodyBind` (function) · `import { createBodyBind } from "@jgengine/core/world"` + +## cast-bar — run a channeled cast timer that movement or damage can interrupt + +- `createCastRunner` (function) · `import { createCastRunner } from "@jgengine/core/combat"` + +## charge-meter — fill a gauge toward named tier thresholds as a value accumulates + +- `createAccumulatorMeter` (function) · `import { createAccumulatorMeter } from "@jgengine/core/combat"` + +## clock-format — format a signed time gap like a race split (+/- m:ss.ff) + +- `formatDelta` (function) · `import { formatDelta } from "@jgengine/core/ui"` +- `formatDuration` (function) · `import { formatDuration } from "@jgengine/core/ui"` + +## combo-chain — advance a chained melee string from timed button inputs + +- `createComboRunner` (function) · `import { createComboRunner } from "@jgengine/core/combat"` + +## combo-points — build up and spend finisher/combo points + +- `createComboPoints` (function) · `import { createComboPoints } from "@jgengine/core/combat"` + +## consumables — use/consume items with cooldowns and effects + +- `createItemUse` (function) · `import { createItemUse } from "@jgengine/core/gameplay"` + +## cosmetics — equip cosmetic skins and customizations by slot + +- `createCosmetics` (function) · `import { createCosmetics } from "@jgengine/core/gameplay"` + +## crouch-prone — stance/pose transitions that change the hitbox + +- `createPoseState` (function) · `import { createPoseState } from "@jgengine/core/world"` + +## death-system — resolve entity death and its on-death consequences + +- `createDeathSystem` (function) · `import { createDeathSystem } from "@jgengine/core/combat"` + +## decay-meter — survival meters that drain/refill over game time (hunger, water, oxygen, stamina) + +- `createDecayMeterSet` (function) · `import { createDecayMeterSet } from "@jgengine/core/procedural"` + +## default-look — one field that lights a scene like a shipped game (opt out with "flat") + +- `LookPreset` (type) · `import { LookPreset } from "@jgengine/core/ui"` + +## dialogue-bridge — open/close the talkable→DialogueBox flow with no per-game store or command glue + +- `createGameDialogue` (function) · `import { createGameDialogue } from "@jgengine/core/gameplay"` + +## dice-check — resolve a pass/fail roll against a target number with modifiers and crits + +- `rollCheck` (function) · `import { rollCheck } from "@jgengine/core/combat"` + +## distance-format — render meters as m or km for HUD stats, telemetry, and range readouts + +- `formatDistance` (function) · `import { formatDistance } from "@jgengine/core/ui"` + +## dot-field — track stacking damage-over-time effects and drain damage each frame + +- `createDotField` (function) · `import { createDotField } from "@jgengine/core/combat"` + +## downed-revive — a downed/bleed-out state teammates can revive before death + +- `createDownedState` (function) · `import { createDownedState } from "@jgengine/core/combat"` + +## durability — track item wear, breakage, and repair + +- `applyWear` (function) · `import { applyWear } from "@jgengine/core/gameplay"` + +## entity-meta — cast-free narrow of SceneEntity.meta via a type guard + +- `entityMetaOf` (function) · `import { entityMetaOf } from "@jgengine/core/world"` + +## event-bus — typed publish/subscribe bus for gameplay events + +- `createGameEvents` (function) · `import { createGameEvents } from "@jgengine/core/gameplay"` + +## event-feed — a rolling feed of recent gameplay events for a HUD ticker or killfeed + +- `createGameFeed` (function) · `import { createGameFeed } from "@jgengine/core/gameplay"` + +## event-meter — a heat/streak gauge that builds from repeated hits and cools down over time + +- `createEventMeter` (function) · `import { createEventMeter } from "@jgengine/core/combat"` + +## follow-trail — trailing follower/snake formation that chases a leader + +- `createLeaderTrail` (function) · `import { createLeaderTrail } from "@jgengine/core/world"` + +## footprint-grid — multi-cell footprint occupancy/reservation on a shared build grid + +- `createFootprintGrid` (function) · `import { createFootprintGrid } from "@jgengine/core/world"` + +## forward-axis — declared front convention for parametric assets — auto-orient product shots + +- `DEFAULT_FORWARD` (const) · `import { DEFAULT_FORWARD } from "@jgengine/core/world"` + +## game-save — save/load game state with a pluggable backend, autosave, slots, and migration + +- `createSaveStore` (function) · `import { createSaveStore } from "@jgengine/core/gameplay"` + +## glide — gliding/wingsuit descent control from a launch + +- `createGlideModel` (function) · `import { createGlideModel } from "@jgengine/core/world"` + +## grapple-swing — grappling-hook rope swing physics with reel-in + +- `createGrappleSwing` (function) · `import { createGrappleSwing } from "@jgengine/core/world"` + +## headless-runner — play a real game loop with no renderer — tick, feed input, read the world snapshot + +- `HeadlessRunner` (interface) · `import { HeadlessRunner } from "@jgengine/core/runtime/headlessRunner"` + +## impact-feel — calibrated hitstop + trauma preset for a named impact event + +- `impactPresets` (const) · `import { impactPresets } from "@jgengine/core/combat"` +- `resolveHitReaction` (function) · `import { resolveHitReaction } from "@jgengine/core/combat"` + +## item-instance-registry — a runtime store for procedurally generated item instances + +- `createItemInstanceRegistry` (function) · `import { createItemInstanceRegistry } from "@jgengine/core/gameplay"` +- `proceduralLootEntry` (function) · `import { proceduralLootEntry } from "@jgengine/core/gameplay"` + +## lap-splits — per-lap durations from a cumulative split book + +- `lapDurations` (function) · `import { lapDurations } from "@jgengine/core/gameplay"` +- `parDelta` (function) · `import { parDelta } from "@jgengine/core/gameplay"` +- `splitSegments` (function) · `import { splitSegments } from "@jgengine/core/gameplay"` + +## lap-timer — wall-clock current/last/best lap timing with splits + +- `createLapTimer` (function) · `import { createLapTimer } from "@jgengine/core/gameplay"` + +## leaderboard — ranked score tracking across global, server, and per-profile scopes + +- `createLeaderboard` (function) · `import { createLeaderboard } from "@jgengine/core/gameplay"` + +## lifecycle — declarative start/restart run flow — the engine owns the command glue and phase sync, the game supplies pure state transitions + +- `LifecycleConfig` (interface) · `import { LifecycleConfig } from "@jgengine/core/gameplay"` + +## limb-health — per-body-part/region health tracked separately + +- `createMultiRegionHealth` (function) · `import { createMultiRegionHealth } from "@jgengine/core/procedural"` + +## listing-book — player-driven marketplace listings with a house cut, expiry sweep, and seller collection box + +- `createListingBook` (function) · `import { createListingBook } from "@jgengine/core/gameplay"` + +## loadouts — save and swap named equipment loadouts + +- `createLoadouts` (function) · `import { createLoadouts } from "@jgengine/core/gameplay"` + +## local-save — persist a game to on-device localStorage (offline) + +- `localSaveBackend` (function) · `import { localSaveBackend } from "@jgengine/core/gameplay"` + +## lockpick — a solvable grid depth-puzzle with fog-of-war, gates, and hidden traps + +- `generateLock` (function) · `import { generateLock } from "@jgengine/core/world"` + +## loot-filter — filter and highlight drops by rarity/name rules + +- `evaluateLootFilter` (function) · `import { evaluateLootFilter } from "@jgengine/core/gameplay"` + +## loot-table — register loot tables and roll weighted randomized drops + +- `createLootRegistry` (function) · `import { createLootRegistry } from "@jgengine/core/gameplay"` +- `lootTable` (function) · `import { lootTable } from "@jgengine/core/gameplay"` + +## magazine — a weapon magazine with capacity, timed reload, and reserve-pool interaction + +- `createMagazine` (function) · `import { createMagazine } from "@jgengine/core/combat"` + +## model-sockets — named attachment points read from a model + +- `ModelNode` (interface) · `import { ModelNode } from "@jgengine/core/world"` + +## modular-item — attach parts into item mount slots to compute combined stats + +- `slotAccepts` (function) · `import { slotAccepts } from "@jgengine/core/gameplay"` + +## name-generator — generate procedural names from templates and word banks + +- `createNameGenerator` (function) · `import { createNameGenerator } from "@jgengine/core/gameplay"` + +## objectives — check progress of a threshold objective against a live metric + +- `evaluateObjective` (function) · `import { evaluateObjective } from "@jgengine/core/gameplay"` + +## ordinal-format — format a placement number as 1st/2nd/3rd for HUD ranks + +- `formatOrdinal` (function) · `import { formatOrdinal } from "@jgengine/core/ui"` + +## parry-window — time a block/parry/i-frame defensive window against incoming hits + +- `createDefensiveWindow` (function) · `import { createDefensiveWindow } from "@jgengine/core/combat"` + +## ping-wheel — contextual ping/marker communication between teammates + +- `createPingSystem` (function) · `import { createPingSystem } from "@jgengine/core/gameplay"` + +## production-building — a factory building converting inputs to outputs over time + +- `createProductionState` (function) · `import { createProductionState } from "@jgengine/core/gameplay"` + +## projectiles — spawn and advance projectiles with travel and hit resolution + +- `createProjectileSystem` (function) · `import { createProjectileSystem } from "@jgengine/core/combat"` + +## proximity-prompt — a "press E" contextual prompt shown near an interactable + +- `resolveActivePrompt` (function) · `import { resolveActivePrompt } from "@jgengine/core/world"` + +## qte — a quick-time-event timed input sequence with hit windows + +- `evaluateQteSequence` (function) · `import { evaluateQteSequence } from "@jgengine/core/world"` + +## quest-log — track accepted quests and their per-objective progress + +- `createQuestJournal` (function) · `import { createQuestJournal } from "@jgengine/core/gameplay"` + +## race-placements — look up one racer's place + win/lose within a finish order + +- `placementOf` (function) · `import { placementOf } from "@jgengine/core/gameplay"` +- `racePlacements` (function) · `import { racePlacements } from "@jgengine/core/gameplay"` + +## race-session — pure idle→countdown→racing→finished clock every racer wraps around its state + +- `idleRaceSession` (function) · `import { idleRaceSession } from "@jgengine/core/gameplay"` + +## race-track — a checkpoint race with laps, standings, splits, and win conditions + +- `createRaceState` (function) · `import { createRaceState } from "@jgengine/core/gameplay"` + +## regen-shield — a rechargeable overshield that absorbs damage and refills after a lull + +- `createRegenShield` (function) · `import { createRegenShield } from "@jgengine/core/combat"` + +## reputation — faction standing that crosses named reputation tiers + +- `tierForStanding` (function) · `import { tierForStanding } from "@jgengine/core/world"` + +## resolve-game-look — expand a look preset into concrete lighting/backdrop/post knobs + +- `resolveGameLook` (function) · `import { resolveGameLook } from "@jgengine/core/ui"` + +## resource-pool — a regenerating pool like mana or stamina that actions spend from + +- `createResourcePool` (function) · `import { createResourcePool } from "@jgengine/core/combat"` + +## run-modifiers — a roguelike run built from stacking drafted modifier picks + +- `createRunDraft` (function) · `import { createRunDraft } from "@jgengine/core/gameplay"` ## runtime-save — save/load the whole game world through a pluggable backend, autosave or save points - `createRuntimeSave` (function) · `import { createRuntimeSave } from "@jgengine/core/runtime/runtimeSave"` + +## skill-check — a timing-bar skill check where you release inside a zone + +- `skillCheckZoneAt` (function) · `import { skillCheckZoneAt } from "@jgengine/core/world"` + +## social-emotes — emotes and social interactions between nearby players + +- `createSocial` (function) · `import { createSocial } from "@jgengine/core/gameplay"` + +## soil-patch — editor-authorable terrain crack/moss material variation + +- `SOIL_KIND` (const) · `import { SOIL_KIND } from "@jgengine/core/world"` + +## spawn-points — register spawn locations and pick where entities respawn + +- `createSpawnPoints` (function) · `import { createSpawnPoints } from "@jgengine/core/gameplay"` + +## speed-format — render a m/s speed as km/h, mph, knots, or m/s for speedometers and telemetry HUDs + +- `formatSpeed` (function) · `import { formatSpeed } from "@jgengine/core/ui"` + +## stagger-meter — accumulate an ailment buildup (bleed, freeze) until it procs + +- `createBuildupMeter` (function) · `import { createBuildupMeter } from "@jgengine/core/combat"` +- `createStaggerMeter` (function) · `import { createStaggerMeter } from "@jgengine/core/combat"` + +## stat-block — base stats with stacking, expiring buffs and debuffs applied on read + +- `createStats` (function) · `import { createStats } from "@jgengine/core/combat"` + +## status-effects — apply and tick timed status effects and buffs on entities + +- `createEffectSystem` (function) · `import { createEffectSystem } from "@jgengine/core/combat"` + +## toast-feed — queue of transient self-expiring on-screen messages (toasts, announcer, kill-feed) + +- `appendToast` (function) · `import { appendToast } from "@jgengine/core/gameplay"` +- `createToastQueue` (function) · `import { createToastQueue } from "@jgengine/core/gameplay"` + +## touch-controls — default on-screen button silhouette for a touch action + +- `touchButtonShape` (function) · `import { touchButtonShape } from "@jgengine/core/gameplay"` + +## unlockables — gate content behind unlock conditions the player earns + +- `createUnlockCatalog` (function) · `import { createUnlockCatalog } from "@jgengine/core/gameplay"` + +## volumetric-clouds — raymarched cloud layer sky option + +- `VolumetricCloudsConfig` (interface) · `import { VolumetricCloudsConfig } from "@jgengine/core/world"` + +## wallet — hold currency balances with charge and affordability checks + +- `createEmptyWallet` (function) · `import { createEmptyWallet } from "@jgengine/core/gameplay"` + +## weapon-stats — resolve per-weapon stat values for combat math + +- `createWeaponStats` (function) · `import { createWeaponStats } from "@jgengine/core/gameplay"` + +## weighted-pick — pick one item from a set with an injected random source + +- `pickUniform` (function) · `import { pickUniform } from "@jgengine/core/gameplay"` diff --git a/CLAUDE.md b/CLAUDE.md index 949ec717c..e4f82fcf2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,7 @@ Genre-agnostic pure-TypeScript game engine SDK plus its agent skills. Published - **A fixed issue must be closed.** Put `Closes #N` in the PR body; if it won't auto-close (cross-repo, etc.), close it yourself (`issue_write`) with a one-line reason pointing at the PR. - **Issues: telegraph style, three headings, cold reader.** `## Problem` (first line = what's missing + why, never buried) / `## Context` (bullets a reader without your session needs — what exists, links) / `## Suggested scope` (numbered, primitive-level, no essays). - **Docs ship in the same PR as the change.** Any change to public API, a workflow, a convention, or tooling updates everything that teaches it — affected `.claude/skills/*` (+ `bun run gen:skill-api` when exports changed), `CLAUDE.md`, script `--help`/README. Grep the skills for the changed command/API/flag before shipping; a stale skill is worse than none. +- **No freestanding docs — ever.** Never create decision records, ADRs, architecture notes, or any stray markdown under `docs/` or elsewhere. Those rot fast and freeze patterns nobody asked to keep; only user/maintainer efficiency matters. A decision worth keeping is a sentence in `CLAUDE.md`, a skill, or the nearest `README.md` (existing ones only — creating a new README needs the user to ask); a plan lives in the issue/PR that finishes it, and dies when it merges. - **Engine gaps and improvement ideas are never papercuts.** Note and keep going; file as issues in one pass at session end (or before a big implementation). - **Log papercuts the moment they happen.** Workflow friction only (retried call, dead-end command, misleading error, steering-wrong wording): `bun run papercut -m "doing X → Y got in the way"` right then — rides along in whatever ships next. Never engine gaps (those are issues), not `CHANGELOG.md`, not a bug issue. `/papercut` sweeps a session, user-triggered only. - **The user owns release timing.** Merging to `main` can trigger npm publish (`publish.yml` ships any `@jgengine/*` version not yet on npm). Never bump a version to force a release unless asked. diff --git a/docs/adr/0001-feature-descriptor-ownership.md b/docs/adr/0001-feature-descriptor-ownership.md deleted file mode 100644 index 6673f7625..000000000 --- a/docs/adr/0001-feature-descriptor-ownership.md +++ /dev/null @@ -1,110 +0,0 @@ -# 0001 — FeatureDescriptor owns subsystem lifecycle; GameLoop.onTick owns simulation - -- Status: Accepted -- Date: 2026-07-16 -- Deciders: engine -- Supersedes: none -- Informs: #11 (FeatureDescriptor seam, done), #14 (per-descriptor save/restore, next), #37 (this ADR) - -## Context - -`gameContext.ts` carries three mental models for "an entity/behaviour lives here and advances over time," with no written rule saying which one owns the tick. Each audit (gpt/grok/sonnet) independently flagged the ambiguity as the top architectural problem behind the save and replication gaps. - -The three models, as they exist in the tree today: - -1. **Bag-of-entities — `entityStore`** (`packages/core/src/runtime/`, wired into `ctx.scene.entity`). A passive, id-keyed store: `snapshot()`, `hydrate()`, `update()`, `setPose()`, `get`/`list`/`ids`, blackboard, stats. It holds no clock and never self-advances — something else must call into it each frame. - -2. **Subsystem facades — the `FeatureDescriptor` array** (`gameContext.ts:553-805`). #11's seam: each opt-in `GameFeatures` flag maps to one descriptor whose `create(deps)` returns a `FeatureBuild` — `{ value, replicate?, save? }`. `createGameContext` iterates the list (`:1648-1654`), registering each `value` under `ctx.game.*` / `ctx.player.*`, its `replicate` into the host→client set and its `save` into the persistence set. The descriptor is the composition seam, but today it only owns **construction and serialization** — it cannot advance state and cannot release resources. - -3. **Bolted-on `Behaviour` lifecycle** (`packages/shell/src/behaviour.ts`, `behaviourDriver.ts`, `behaviourAttach.ts`, over `packages/core/src/behaviour/behaviour.ts`). A Unity-style `onAwake→onEnable→onStart→onUpdate→onDisable→onDestroy` node tree ported from three-start. `useBehaviourWorld` calls `world.update(dt)` **every render frame** as a second, independent tick loop. It is marked `@internal`, lives entirely in `shell`, is keyed by `Object3D.uuid` rather than entity instance ids, and has **no** connection to `entityStore`, to snapshot/replication, or to host authority. - -The friction this produces: - -- **Two competing ticks.** `GameLoop.onTick(ctx, dt)` (`defineGame.ts:59`, the game-authored simulation hook driven by the shell's scaled clock) and the `Behaviour` world's per-frame `world.update(dt)` both advance state, but only `onTick` runs on a hosted authority. A behaviour that mutates game state ticks on the client render loop and never reaches the snapshot — silently client-only, the exact "multiplayer defaults are silently not multiplayer" failure class. -- **Hand-wired subsystems bypass the descriptor seam.** `time` (`createSimClock`, `:817`), `stats` (`statsByInstance`, `:825`), `pose` (`createPoseState`, `:993`), `possession` (`createPossession`, `:1013`) and `motion` (`createMotionIntents`, `:1616`) are constructed inline and, where they persist at all, are appended by hand to the `snapshotModules` / `saveModules` arrays (`:1656-1711`). Their save/replicate wiring is a parallel code path the descriptor loop cannot see. -- **Silent save/replication gaps.** Because descriptors only carry an optional `save`/`replicate`, several ship with neither: `cosmetics` (`:674`), `cards` (`:785`), `turn` (`:792`) and `race` (`:799`) register **no** save module — turn counters, card-pile contents, race progress and equipped cosmetics are lost across a save/restore. `possession`, `pose` and `time` never appear in `saveModules` at all. This is the #14 gap. - -## Decision - -### 1. `GameLoop.onTick` is the single authoritative simulation tick - -`GameLoop.onTick(ctx: GameContext, dt: number)` (`defineGame.ts:59`; hosted twin `ServerLoopHooks.onTick`, `gameRuntime.ts:17`) is **the** tick. It is the only place simulation state advances, and on a hosted world it is the only tick that runs on the authority. Everything a frame must advance is reached from `onTick`, directly or through the descriptor tick pass (below). - -The other two models reconcile under it: - -- **`entityStore` stays a passive bag.** It never grows a clock. `onTick` (and commands) mutate it; it serializes cleanly via its `snapshot`/`hydrate` module. This is already true and is now the rule. -- **`Behaviour` is demoted to a render-only view concern.** The `Behaviour` world MUST NOT own simulation tick. `onBeforeRender`/`onAfterRender` and view-side interpolation are legitimate; mutating authoritative game state from `Behaviour.onUpdate` is not. Its `world.update(dt)` is driven from the game clock (via `onTick` / the shell's scaled dt), never from a second free-running frame loop, so it cannot advance state the snapshot doesn't see. Behaviours that today carry game logic move that logic into a subsystem behind a descriptor, or into `onTick`. - -### 2. The `FeatureDescriptor` contract owns the full subsystem lifecycle - -A descriptor is the one place a subsystem declares how it is built, advanced, replicated, persisted, restored and disposed. `FeatureBuild` widens from a serialization pair into a lifecycle handle: - -```ts -interface FeatureInstance { - /** The ctx-facing facade registered under ctx.game.* / ctx.player.* (unchanged). */ - value: unknown; - /** Advance one simulation step. Called by createGameContext's tick pass, itself - * driven by the single GameLoop.onTick — NOT by any render frame loop. Omit for - * purely event/command-driven subsystems (unlocks, roster, trade). */ - tick?(dt: number): void; - /** Host→client replication module (ctx.snapshot()/ctx.hydrate()). Unchanged. */ - replicate?: SnapshotModule; - /** Persistence module (ctx.game.save/restore). save ⊇ replicate; a descriptor that - * replicates and also persists provides both. Restore is the module's hydrate(). */ - save?: SnapshotModule; - /** Release timers, listeners, GPU/audio handles on stop()/unmount. Runs in LIFO - * with the rest of the context teardown. Omit if the subsystem holds no resources. */ - dispose?(): void; -} - -interface FeatureDescriptor { - /** Widened beyond keyof GameFeatures so always-on baseline subsystems - * (entities, stats, store, feed, inventory, economy) are descriptors too. */ - readonly key: FeatureKey; - enabled(features: GameFeatures): boolean; // always-on baseline returns true - create(deps: FeatureDeps): FeatureInstance; -} -``` - -`createGameContext` runs one loop over `[...baselineDescriptors, ...featureDescriptors]` and, per enabled descriptor, collects `value`, `tick`, `replicate`, `save`, `dispose`. The four inline arrays it maintains today — the descriptor loop plus the hand-written `snapshotModules`, `saveModules`, and scattered dispose calls — collapse into that one pass. This yields four assembled sets with **no hand-maintained membership**: - -- `snapshotModules` = every descriptor's `replicate` (host→client baseline). -- `saveModules` = every descriptor's `save` (persistence superset; unchanged invariant that `save ⊇ replicate`). -- the tick pass = every descriptor's `tick`, invoked in registration order from the single `onTick` driver. -- context teardown = every descriptor's `dispose`, LIFO. - -`save`/`replicate` are `SnapshotModule` (`worldSnapshot.ts:7`), so restore is already the module's `hydrate(data)` — per-descriptor save/restore falls out for free once a subsystem owns its `save`. That is the contract #14 builds on: #14 becomes "give each named subsystem below a `save` module," not new plumbing. - -### 3. Subsystems that move under descriptor ownership - -Every subsystem currently hand-wired outside the descriptor loop, and every descriptor missing `save`, is brought under the contract. Concretely: - -| Subsystem | Today | Gains | Notes | -| --- | --- | --- | --- | -| `time` (`createSimClock`, `:817`) | inline construct, no save | `tick`, `save` | clock must advance each step and persist calendar/scale/speed | -| `stats` (`statsByInstance`, `:825`) | inline, hand-appended to snapshot/save | `save` (baseline descriptor) | per-instance stat maps | -| `cosmetics` (`:674`) | descriptor, **no save** | `save` | equipped skins lost on restore today | -| `cards` (`:785`) | descriptor, **no save** | `save` | pile contents/order | -| `turn` (`:792`) | descriptor, **no save**, `loop` timers | `tick`, `save` | turn/phase counters + timer advance | -| `race` (`:799`) | descriptor, **no save** | `tick`, `save` | race progress/positions | -| `motion` (`createMotionIntents`, `:1616`) | inline | `tick`, `dispose` | per-frame intent integration | -| `pose` (`createPoseState`, `:993`) | inline, no save | `save` | pose + constraints | -| `possession` (`createPossession`, `:1013`) | inline, no save — **the #14 gap** | `save`, `replicate` | ownership must replicate to clients and survive restore | -| baseline (`entities`, `store`, `feed`, `inventory`, `economy`) | hand-written module arrays | `save`/`replicate` via baseline descriptors | membership stops being a hand-maintained list | - -## Consequences - -Positive: - -- One written owner of tick (`GameLoop.onTick`), one written owner of a subsystem's lifecycle (its descriptor). The "which model ticks?" ambiguity is closed. -- Adding a subsystem is one descriptor registration carrying its own tick/save/replicate/dispose — never a new inline `createX()` plus edits to three module arrays plus a dispose call. Extends #11's seam instead of fighting it. -- #14 (per-descriptor save/restore) reduces to filling in the `save` column of the table above; no new mechanism. -- Save/replication gaps become structurally visible: a subsystem with mutable state and no `save` is a reviewable omission on one object, not a silent gap between two distant arrays. -- The `Behaviour` world can no longer silently diverge from the authority — its update is clock-driven and render-scoped. - -Costs / follow-ups: - -- Migrating the six inline subsystems (`time`, `stats`, `pose`, `possession`, `motion`, and the baseline five) into descriptors touches `createGameContext`'s assembly and must keep `ctx.snapshot()`/`ctx.game.save` byte-identical for existing games — covered by `gameContextSave.test.ts` / `worldReplication.test.ts` / `worldSnapshot.test.ts`. -- `FeatureDescriptor.key` widening to `FeatureKey` (beyond `keyof GameFeatures`) needs a small internal-key union so baseline descriptors have stable keys. -- `Behaviour` game-logic misuse must be audited game-by-game before its update loop is reparented; extraction must not change how any shipped game plays. -- `tick` ordering is registration order; a subsystem that depends on another advancing first (e.g. `motion` after `time`) relies on descriptor list order — documented, not enforced by types. diff --git a/packages/core/src/physics/README.md b/packages/core/src/physics/README.md index 47da7823c..c82762c10 100644 --- a/packages/core/src/physics/README.md +++ b/packages/core/src/physics/README.md @@ -1,4 +1,4 @@ -# Physics strategy (ADR) +# Physics strategy (decision record) **Status:** accepted for current tree · critique action **Y1** diff --git a/packages/core/src/runtime/context/combatFx.ts b/packages/core/src/runtime/context/combatFx.ts new file mode 100644 index 000000000..5c6b261e1 --- /dev/null +++ b/packages/core/src/runtime/context/combatFx.ts @@ -0,0 +1,174 @@ +import type { EffectInput, EffectResult } from "../../combat/effects"; +import { resolveHitReaction, type HitReaction } from "../../combat/hitReaction"; +import { pointInTelegraph, type TelegraphConfig } from "../../combat/telegraph"; +import type { GameEventMap, GameEvents, VfxKind } from "../../game/events"; +import type { EntityPosition, EntityStore } from "../../scene/entityStore"; +import type { SimClock } from "../../time/simClock"; +import type { FloatTextInput, HitReactionInput, TelegraphInput, VfxInput } from "../gameContext"; + +/** @internal What the combat-presentation helpers need from the live context: entity poses, the event bus, the sim clock (telegraph windups), and the raw effect application. */ +export interface CombatFxDeps { + entities: EntityStore; + events: GameEvents; + time: SimClock; + applyEffect: (input: EffectInput) => EffectResult[]; +} + +/** @internal Combat presentation surface registered under `ctx.scene.entity`: float text, VFX bursts, telegraphs, hit reactions, and the effect wrapper that emits damage/heal float text. */ +export interface CombatFx { + emitFloatText(input: FloatTextInput): void; + emitVfx(input: VfxInput): void; + fireTelegraph(input: TelegraphInput): () => void; + applyHitReaction(input: HitReactionInput): HitReaction | null; + applyEffectAndFloat(input: EffectInput): EffectResult[]; +} + +/** @internal */ +export function createCombatFx(d: CombatFxDeps): CombatFx { + const { entities, events, time } = d; + + function emitFloatText(input: FloatTextInput): void { + const position = + input.position ?? + (input.instanceId === undefined ? undefined : entities.get(input.instanceId)?.position); + if (position === undefined) return; + const text = input.text ?? (input.amount === undefined ? "" : String(Math.round(input.amount))); + const event: GameEventMap["entity.floatText"] = { + position: [position[0], position[1], position[2]], + text, + kind: input.kind ?? "info", + }; + if (input.instanceId !== undefined) event.instanceId = input.instanceId; + if (input.amount !== undefined) event.amount = input.amount; + if (input.hitType !== undefined) event.hitType = input.hitType; + if (input.element !== undefined) event.element = input.element; + if (input.crit !== undefined) event.crit = input.crit; + if (input.scale !== undefined) event.scale = input.scale; + events.emit("entity.floatText", event); + } + + const vfxDefaultDurationMs: Record = { + projectile: 380, + beam: 260, + nova: 520, + glow: 700, + spark: 240, + }; + let vfxSeq = 0; + + function resolveVfxPoint( + ref: string | readonly [number, number, number] | undefined, + ): [number, number, number] | undefined { + if (ref === undefined) return undefined; + if (typeof ref === "string") { + const entity = entities.get(ref); + if (entity === null) return undefined; + return [entity.position[0], entity.position[1], entity.position[2]]; + } + return [ref[0], ref[1], ref[2]]; + } + + function emitVfx(input: VfxInput): void { + const to = resolveVfxPoint(input.to); + const from = resolveVfxPoint(input.from) ?? to; + if (from === undefined) return; + const event: GameEventMap["combat.vfx"] = { + id: vfxSeq++, + kind: input.kind, + color: input.color, + from, + durationMs: input.durationMs ?? vfxDefaultDurationMs[input.kind], + }; + if (to !== undefined) event.to = to; + if (input.radius !== undefined) event.radius = input.radius; + events.emit("combat.vfx", event); + } + + let telegraphSeq = 0; + + function fireTelegraph(input: TelegraphInput): () => void { + const id = telegraphSeq++; + const telegraphEvent: GameEventMap["combat.telegraph"] = { + id, + shape: input.shape, + position: [input.at[0], input.at[1], input.at[2]], + windupMs: input.windupMs, + kind: input.kind ?? "danger", + }; + if (input.dir !== undefined) telegraphEvent.dir = input.dir; + events.emit("combat.telegraph", telegraphEvent); + const cancelVisual = () => events.emit("combat.telegraphCancelled", { id }); + const bound = input.effect; + if (bound === undefined) return cancelVisual; + const config: TelegraphConfig = { shape: input.shape, at: input.at, windupMs: input.windupMs }; + if (input.dir !== undefined) config.dir = input.dir; + const cancelEffect = time.after(input.windupMs / 1000, () => { + const targets = entities.list().filter((entity) => pointInTelegraph(config, entity.position)); + for (const target of targets) { + applyEffectAndFloat({ + from: input.from, + to: target.id, + effect: bound.effect, + ...(bound.via === undefined ? {} : { via: bound.via }), + }); + } + }); + return () => { + cancelEffect(); + cancelVisual(); + }; + } + + function applyHitReaction(input: HitReactionInput): HitReaction | null { + const attacker = entities.get(input.from); + const target = entities.get(input.to); + if (target === null) return null; + const attackerPos = attacker?.position ?? target.position; + const reaction = resolveHitReaction(input.config, { + attackerPos, + targetPos: target.position, + ...(input.power === undefined ? {} : { power: input.power }), + }); + entities.setPose(input.to, { + position: [ + target.position[0] + reaction.impulse[0], + target.position[1] + reaction.impulse[1], + target.position[2] + reaction.impulse[2], + ], + rotationY: target.rotationY, + }); + const reactionEvent: GameEventMap["combat.hitReaction"] = { + instanceId: input.to, + position: [target.position[0], target.position[1], target.position[2]], + hitstopMs: reaction.hitstopMs, + }; + if (reaction.shake !== null) reactionEvent.shake = reaction.shake; + if (reaction.trauma !== null) reactionEvent.trauma = reaction.trauma; + events.emit("combat.hitReaction", reactionEvent); + return reaction; + } + + function applyEffectAndFloat(input: EffectInput): EffectResult[] { + const positionsBefore = new Map(); + for (const entity of entities.list()) positionsBefore.set(entity.id, entity.position); + const results = d.applyEffect(input); + for (const result of results) { + let total = 0; + for (const delta of result.applied) total += delta.delta; + if (total === 0) continue; + const position = entities.get(result.instanceId)?.position ?? positionsBefore.get(result.instanceId); + if (position === undefined) continue; + const magnitude = Math.abs(total); + emitFloatText({ + instanceId: result.instanceId, + position: [position[0], position[1], position[2]], + text: String(Math.round(magnitude)), + kind: total < 0 ? "damage" : "heal", + amount: magnitude, + }); + } + return results; + } + + return { emitFloatText, emitVfx, fireTelegraph, applyHitReaction, applyEffectAndFloat }; +} diff --git a/packages/core/src/runtime/context/registries.ts b/packages/core/src/runtime/context/registries.ts new file mode 100644 index 000000000..4ecb5e35c --- /dev/null +++ b/packages/core/src/runtime/context/registries.ts @@ -0,0 +1,109 @@ +import { createCardPile, type CardPile, type CardPileConfig } from "../../cards/cardPile"; +import { RaceState, type RaceEvent, type RaceStateConfig } from "../../game/race"; +import { notifyAfter } from "../../store/changeSignal"; +import { createTurnLoop, type TurnLoop, type TurnLoopConfig } from "../../turn/turnLoop"; + +/** @internal Lazily-created, id-keyed card-pile / turn-loop / race registries shared by `ctx.game.cards|turn|race` and their save modules. */ +export interface ContextRegistries { + pile(id: string, config?: CardPileConfig): CardPile; + loop(id: string, config?: TurnLoopConfig): TurnLoop; + raceState(id: string, config?: RaceStateConfig): RaceState; + cardPiles: ReadonlyMap; + turnLoops: ReadonlyMap; +} + +/** @internal */ +export function createContextRegistries(signalNotify: () => void): ContextRegistries { + const cardPiles = new Map(); + function pile(id: string, config?: CardPileConfig): CardPile { + const existing = cardPiles.get(id); + if (existing !== undefined) return existing; + if (config === undefined) { + throw new Error(`cardPile "${id}" has not been created yet; pass a config on first access`); + } + const created = notifyAfter( + createCardPile(config), + ["shuffle", "draw", "discard", "exhaust", "move", "reset"], + signalNotify, + ); + cardPiles.set(id, created); + return created; + } + + const turnLoops = new Map(); + function loop(id: string, config?: TurnLoopConfig): TurnLoop { + const existing = turnLoops.get(id); + if (existing !== undefined) return existing; + if (config === undefined) { + throw new Error(`turn loop "${id}" has not been created yet; pass a config on first access`); + } + const raw = createTurnLoop(config); + const wrappedCommit = notifyAfter( + raw.commit, + ["submit", "expected", "commit", "discard", "clear"], + signalNotify, + ); + const wrapped: TurnLoop = { + ...notifyAfter( + raw, + [ + "setOrder", + "addParticipant", + "removeParticipant", + "advancePhase", + "advanceTurn", + "advanceRound", + "spend", + "gain", + "refill", + "restore", + ], + signalNotify, + ), + commit: wrappedCommit, + }; + turnLoops.set(id, wrapped); + return wrapped; + } + + class NotifyingRaceState extends RaceState { + override addRacer(racerId: string, startTime?: number): void { + super.addRacer(racerId, startTime); + signalNotify(); + } + override removeRacer(racerId: string): void { + super.removeRacer(racerId); + signalNotify(); + } + override reset(): void { + super.reset(); + signalNotify(); + } + override eliminate(racerId: string): void { + super.eliminate(racerId); + signalNotify(); + } + override update( + now: number, + positions: Record | Map, + ): readonly RaceEvent[] { + const raceEvents = super.update(now, positions); + if (raceEvents.length > 0) signalNotify(); + return raceEvents; + } + } + + const raceStates = new Map(); + function raceState(id: string, config?: RaceStateConfig): RaceState { + const existing = raceStates.get(id); + if (existing !== undefined) return existing; + if (config === undefined) { + throw new Error(`race "${id}" has not been created yet; pass a config on first access`); + } + const created = new NotifyingRaceState(config); + raceStates.set(id, created); + return created; + } + + return { pile, loop, raceState, cardPiles, turnLoops }; +} diff --git a/packages/core/src/runtime/context/worldItems.ts b/packages/core/src/runtime/context/worldItems.ts new file mode 100644 index 000000000..4d2d71e66 --- /dev/null +++ b/packages/core/src/runtime/context/worldItems.ts @@ -0,0 +1,70 @@ +import type { GameEvents } from "../../game/events"; +import { + createWorldItemStore, + WORLD_ITEM_ENTITY_NAME, + type WorldItemRecord, + type WorldItemSpawnInput, + type WorldItemStore, +} from "../../game/worldItem"; +import type { Drop } from "../../game/lootTable"; +import type { EntityStore } from "../../scene/entityStore"; +import { notifyAfter } from "../../store/changeSignal"; +import type { WorldItemPickupResult } from "../gameContext"; + +/** @internal What the ground-item surface needs from the live context: entity spawn/despawn, the event bus, loot granting, and change notification. */ +export interface WorldItemContextDeps { + entities: EntityStore; + events: GameEvents; + despawnEntity: (instanceId: string) => boolean; + grantToPlayer: (userId: string, drops: Drop[], source?: string) => void; + signalNotify: () => void; +} + +/** @internal The `ctx.scene.worldItem` wiring: the reactive store plus the spawn/pickup verbs that emit `worldItem.*` events and route pickups through loot. */ +export interface WorldItemContext { + worldItems: WorldItemStore; + spawnWorldItem(input: WorldItemSpawnInput): WorldItemRecord; + pickupWorldItem(instanceId: string, userId: string): WorldItemPickupResult; +} + +/** @internal */ +export function createWorldItemContext(d: WorldItemContextDeps): WorldItemContext { + const worldItems = notifyAfter( + createWorldItemStore({ + spawnEntity: (position) => d.entities.spawn(WORLD_ITEM_ENTITY_NAME, { position, role: "prop" }), + despawnEntity: d.despawnEntity, + resolvePosition: (instanceId) => d.entities.get(instanceId)?.position, + }), + ["spawn", "take", "remove"], + d.signalNotify, + ); + + function spawnWorldItem(input: WorldItemSpawnInput): WorldItemRecord { + const record = worldItems.spawn(input); + d.events.emit("worldItem.dropped", { + instanceId: record.instanceId, + itemId: record.itemId, + rarity: record.rarity, + count: record.count, + position: [input.position[0], input.position[1], input.position[2]], + ...(record.source !== undefined ? { source: record.source } : {}), + }); + return record; + } + + function pickupWorldItem(instanceId: string, userId: string): WorldItemPickupResult { + const record = worldItems.take(instanceId); + if (record === null) return { status: "rejected", reason: "not-found" }; + d.grantToPlayer(userId, [{ item: record.itemId, count: record.count }], "worldItem.pickup"); + d.events.emit("worldItem.picked_up", { + instanceId, + userId, + itemId: record.itemId, + rarity: record.rarity, + count: record.count, + }); + return { status: "ok", record }; + } + + return { worldItems, spawnWorldItem, pickupWorldItem }; +} diff --git a/packages/core/src/runtime/descriptors/baseline.ts b/packages/core/src/runtime/descriptors/baseline.ts new file mode 100644 index 000000000..42fd89948 --- /dev/null +++ b/packages/core/src/runtime/descriptors/baseline.ts @@ -0,0 +1,184 @@ +import type { WalletState } from "../../economy/wallet"; +import type { FeedEntry, GameFeed } from "../../game/feed"; +import type { InventorySet, InventoryState } from "../../inventory/inventoryModel"; +import type { PoseSnapshot, PoseState } from "../../movement/poseState"; +import { + hydrateEntityStats, + snapshotEntityStats, + type StatValueMap, +} from "../../scene/entityStats"; +import type { EntityStore, SceneEntity } from "../../scene/entityStore"; +import type { Possession, PossessionSnapshot } from "../../scene/possession"; +import type { ObservableKeyedStore } from "../../store/observableKeyedStore"; +import type { ClockSnapshot, SimClock } from "../../time/simClock"; +import type { MotionIntentBatch, MotionIntents } from "../motionIntents"; +import type { SnapshotModule } from "../worldSnapshot"; + +/** @internal The live always-on subsystems the baseline descriptors serialize — handed in by `createGameContext`. */ +export interface BaselineDeps { + signalNotify: () => void; + entities: EntityStore; + statsByInstance: Map; + store: ObservableKeyedStore; + feed: GameFeed; + inventoryIds: readonly string[]; + inventoryByUser: ReadonlyMap>; + inventoryFor: (userId: string) => InventorySet; + wallets: Map; + time: SimClock; + pose: PoseState; + possession: Possession; + motionByUser: ReadonlyMap; + motionFor: (userId: string) => MotionIntents; +} + +/** @internal What an always-on baseline descriptor contributes: a replication module, a save-only module, or both. */ +export interface BaselineBuild { + /** Registered into the host→client replication set (`ctx.snapshot`/`ctx.hydrate`), and therefore also into the save set. */ + replicate?: SnapshotModule; + /** Registered into the whole-world save-only set (`ctx.game.save`) — persisted but never sent to clients. */ + save?: SnapshotModule; +} + +/** + * Always-on counterpart to {@link featureDescriptors}: each baseline subsystem (entities, stats, + * store, feed, inventory, economy, time, pose, possession, motion) owns its own serialization here + * instead of `createGameContext` hand-maintaining two distant module arrays. Order is + * load-bearing — it fixes the snapshot key order, so it must stay stable. + * @internal + */ +export interface BaselineDescriptor { + readonly key: string; + create(deps: BaselineDeps): BaselineBuild; +} + +/** @internal */ +export const baselineDescriptors: readonly BaselineDescriptor[] = [ + { + key: "entities", + create: (d) => ({ + replicate: { + key: "entities", + snapshot: () => d.entities.snapshot(), + hydrate: (data) => d.entities.hydrate(data as SceneEntity[]), + }, + }), + }, + { + key: "stats", + create: (d) => ({ + replicate: { + key: "stats", + snapshot: () => snapshotEntityStats(d.statsByInstance), + hydrate: (data) => hydrateEntityStats(d.statsByInstance, data as Record), + }, + }), + }, + { + key: "store", + create: (d) => ({ + replicate: { + key: "store", + snapshot: () => d.store.snapshot(), + hydrate: (data) => d.store.hydrate(data as readonly (readonly [string, unknown])[]), + }, + }), + }, + { + key: "feed", + create: (d) => ({ + replicate: { + key: "feed", + snapshot: () => d.feed.snapshot(), + hydrate: (data) => d.feed.hydrate(data as Record), + }, + }), + }, + { + key: "inventory", + create: (d) => ({ + replicate: { + key: "inventory", + snapshot: () => { + const byUser: Record> = {}; + for (const [userId, set] of d.inventoryByUser) { + const states: Record = {}; + for (const inventoryId of d.inventoryIds) states[inventoryId] = set.state(inventoryId); + byUser[userId] = states; + } + return byUser; + }, + hydrate: (data) => { + for (const [userId, states] of Object.entries(data as Record>)) { + const set = d.inventoryFor(userId); + for (const [inventoryId, state] of Object.entries(states)) set.replaceState(inventoryId, state); + } + }, + }, + }), + }, + { + key: "economy", + create: (d) => ({ + save: { + key: "economy", + snapshot: () => Object.fromEntries(d.wallets), + hydrate: (data) => { + d.wallets.clear(); + for (const [userId, state] of Object.entries(data as Record)) { + d.wallets.set(userId, state); + } + d.signalNotify(); + }, + }, + }), + }, + { + key: "time", + create: (d) => ({ + save: { + key: "time", + snapshot: () => d.time.snapshot(), + hydrate: (data) => d.time.hydrate(data as ClockSnapshot), + }, + }), + }, + { + key: "pose", + create: (d) => ({ + save: { + key: "pose", + snapshot: () => d.pose.snapshotAll(), + hydrate: (data) => d.pose.hydrateAll(data as PoseSnapshot), + }, + }), + }, + { + key: "possession", + create: (d) => ({ + save: { + key: "possession", + snapshot: () => d.possession.snapshotAll(), + hydrate: (data) => d.possession.hydrateAll(data as PossessionSnapshot), + }, + }), + }, + { + key: "motion", + create: (d) => ({ + save: { + key: "motion", + snapshot: () => { + const out: Record = {}; + for (const [userId, queue] of d.motionByUser) out[userId] = queue.snapshot(); + return out; + }, + hydrate: (data) => { + for (const [userId, batch] of Object.entries(data as Record)) { + d.motionFor(userId).hydrate(batch); + } + }, + }, + }), + }, +]; diff --git a/packages/core/src/runtime/descriptors/features.ts b/packages/core/src/runtime/descriptors/features.ts new file mode 100644 index 000000000..677ce30da --- /dev/null +++ b/packages/core/src/runtime/descriptors/features.ts @@ -0,0 +1,377 @@ +import type { CardPile, CardPileConfig, CardPileState } from "../../cards/cardPile"; +import type { CommandRegistry } from "../../commands/commandRegistry"; +import { + canAfford as walletCanAfford, + chargeAll as walletChargeAll, + type WalletState, +} from "../../economy/wallet"; +import { createCosmetics } from "../../game/cosmetics"; +import type { GameFeatures } from "../../game/defineGame"; +import type { GameEvents } from "../../game/events"; +import { createLeaderboard, type LeaderboardRow } from "../../game/leaderboard"; +import { createGameDialogue } from "../../game/dialogue"; +import { createQuestJournal, type QuestSnapshotEntry } from "../../game/quest"; +import { createChat, type ChatSnapshot } from "../../game/chat"; +import { type Social, type SocialSnapshot } from "../../game/social"; +import { createTradeSystem } from "../../game/trade"; +import { createUnlocks, type Unlocks } from "../../game/unlocks"; +import type { InventoryLayout, InventorySet } from "../../inventory/inventoryModel"; +import type { StatValueMap } from "../../scene/entityStats"; +import type { EntityStore } from "../../scene/entityStore"; +import { createRoster, type RosterEntry } from "../../scene/roster"; +import { createConnectedPlayers } from "../../game/connectedPlayers"; +import type { SpatialApi } from "../../scene/spatial"; +import { notifyAfter } from "../../store/changeSignal"; +import type { ObservableKeyedStore } from "../../store/observableKeyedStore"; +import type { SnapshotModule } from "../worldSnapshot"; +import type { TurnLoop, TurnLoopConfig, TurnLoopSnapshot } from "../../turn/turnLoop"; +import type { RaceState, RaceStateConfig } from "../../game/race"; +import type { + GameContext, + GameContextCards, + GameContextContent, + GameContextEconomy, + GameContextRace, + GameContextTurn, +} from "../gameContext"; + +/** + * Shared wiring every optional-feature descriptor draws from — the live core subsystems (entities, + * spatial, economy, inventory) plus reactive plumbing (`signalNotify`) and a `feature` reader for the + * few features that reference another (quest reads unlocks). Handed to each {@link FeatureDescriptor}'s + * `create` so a new opt-in subsystem plugs into one registration, never a new `features.x ?` branch. + * @internal + */ +export interface FeatureDeps { + features: GameFeatures; + signalNotify: () => void; + events: GameEvents; + now: () => number; + entities: EntityStore; + spatial: SpatialApi; + store: ObservableKeyedStore; + commandRegistry: CommandRegistry; + economy: GameContextEconomy; + content: GameContextContent; + activeUserId: () => string; + walletOf: (userId: string) => WalletState; + setWallet: (userId: string, state: WalletState) => void; + layouts: Record; + inventoryFor: (userId: string) => InventorySet; + ensureInstanceStats: (instanceId: string) => StatValueMap; + seedUserPool: (userId: string, statId: string, pool: { current: number; max?: number; min?: number }) => void; + sharedSocial: () => Social; + pile: (id: string, config?: CardPileConfig) => CardPile; + loop: (id: string, config?: TurnLoopConfig) => TurnLoop; + cardPiles: ReadonlyMap; + turnLoops: ReadonlyMap; + raceState: (id: string, config?: RaceStateConfig) => RaceState; + feature: (key: keyof GameFeatures) => T | undefined; +} + +/** @internal What a descriptor produces: the `ctx`-facing value plus its optional replication/save modules. */ +export interface FeatureBuild { + value: unknown; + /** Registered into the host→client replication set when present (`ctx.snapshot`/`ctx.hydrate`). */ + replicate?: SnapshotModule; + /** Registered into the whole-world save-only set when present (`ctx.game.save`). */ + save?: SnapshotModule; +} + +/** + * One opt-in subsystem expressed as data: which `features` flag turns it on, how it wires itself from + * {@link FeatureDeps}, and whether it replicates or persists. `createGameContext` iterates the + * descriptor list instead of hand-wiring each `features.x ? create : undefined` branch — the seam a + * new feature extends through. + * @internal + */ +export interface FeatureDescriptor { + readonly key: keyof GameFeatures; + enabled(features: GameFeatures): boolean; + create(deps: FeatureDeps): FeatureBuild; +} + +/** @internal */ +export const featureDescriptors: readonly FeatureDescriptor[] = [ + { + key: "unlocks", + enabled: (f) => f.unlocks === true, + create(d) { + const unlocks = notifyAfter(createUnlocks(), ["grant", "hydrate"], d.signalNotify); + return { + value: unlocks, + save: { + key: "unlocks", + snapshot: () => unlocks.snapshotAll(), + hydrate: (data) => unlocks.hydrateAll(data as Record), + }, + }; + }, + }, + { + key: "social", + enabled: (f) => f.social === true, + create(d) { + const raw = d.sharedSocial(); + const social: Social = { + friends: notifyAfter( + raw.friends, + ["request", "accept", "decline", "remove", "block", "hydrate"], + d.signalNotify, + ), + party: notifyAfter( + raw.party, + ["invite", "accept", "decline", "kick", "leave", "promote"], + d.signalNotify, + ), + presence: raw.presence, + emotes: raw.emotes, + worldInvites: notifyAfter(raw.worldInvites, ["invite", "accept", "decline"], d.signalNotify), + snapshot: raw.snapshot, + hydrate: (data) => { + raw.hydrate(data); + d.signalNotify(); + }, + }; + return { + value: social, + replicate: { + key: "social", + snapshot: () => social.snapshot(), + hydrate: (data) => social.hydrate(data as SocialSnapshot), + }, + }; + }, + }, + { + key: "chat", + enabled: (f) => f.chat === true, + create(d) { + const raw = d.sharedSocial(); + const chat = notifyAfter( + createChat({ + events: d.events, + now: d.now, + party: raw.party, + proximity: { + entities: { get: (id) => d.entities.get(id) }, + spatial: { inRadius: (center, radius, filter) => d.spatial.inRadius(center, radius, filter) }, + }, + blockedBy: (userId) => raw.friends.snapshot(userId).blocked, + }), + ["register", "send", "whisper", "hydrate"], + d.signalNotify, + ); + return { + value: chat, + replicate: { + key: "chat", + snapshot: () => chat.snapshot(), + hydrate: (data) => chat.hydrate(data as ChatSnapshot), + }, + }; + }, + }, + { + key: "leaderboard", + enabled: (f) => f.leaderboard === true, + create(d) { + const leaderboard = notifyAfter(createLeaderboard(), ["increment", "hydrate"], d.signalNotify); + return { + value: leaderboard, + replicate: { + key: "leaderboard", + snapshot: () => leaderboard.snapshot(), + hydrate: (data) => leaderboard.hydrate(data as LeaderboardRow[]), + }, + }; + }, + }, + { + key: "roster", + enabled: (f) => f.roster === true, + create(d) { + const roster = notifyAfter( + createRoster({ now: d.now }), + ["capture", "release", "setEquipped", "hydrate"], + d.signalNotify, + ); + return { + value: roster, + save: { + key: "roster", + snapshot: () => roster.snapshotAll(), + hydrate: (data) => roster.hydrateAll(data as Record), + }, + }; + }, + }, + { + key: "cosmetics", + enabled: (f) => f.cosmetics === true, + create(d) { + const cosmetics = notifyAfter(createCosmetics({ events: d.events }), ["apply", "equip", "hydrate"], d.signalNotify); + return { + value: cosmetics, + save: { + key: "cosmetics", + snapshot: () => cosmetics.snapshotAll(), + hydrate: (data) => cosmetics.hydrateAll(data as Record>), + }, + }; + }, + }, + { + key: "trade", + enabled: (f) => f.trade === true, + create(d) { + return { + value: createTradeSystem({ + resolveTrade: (itemId) => d.content.itemById?.(itemId)?.trade, + wallet: { + canAfford: (costs) => + walletCanAfford(d.walletOf(d.activeUserId()), costs) ? null : "insufficient-funds", + charge(costs) { + const result = walletChargeAll(d.walletOf(d.activeUserId()), costs); + if (result.status === "ok") { + d.setWallet(d.activeUserId(), result.state); + d.signalNotify(); + } + }, + grant(gains) { + for (const [currencyId, amount] of Object.entries(gains)) { + d.economy.grant(d.activeUserId(), currencyId, amount); + } + }, + }, + inventory: { + put(inventoryId, itemId, count) { + if (d.layouts[inventoryId] === undefined) return { reason: `unknown inventory "${inventoryId}"` }; + const result = d.inventoryFor(d.activeUserId()).put(inventoryId, itemId, count); + return result.status === "ok" ? null : { reason: result.reason }; + }, + take(inventoryId, itemId, count) { + if (d.layouts[inventoryId] === undefined) return { reason: `unknown inventory "${inventoryId}"` }; + const result = d.inventoryFor(d.activeUserId()).take(inventoryId, itemId, count); + return result.status === "ok" ? null : { reason: result.reason }; + }, + count: (inventoryId, itemId) => d.inventoryFor(d.activeUserId()).count(inventoryId, itemId), + }, + }), + }; + }, + }, + { + key: "quest", + enabled: (f) => f.quest === true, + create(d) { + const quest = notifyAfter( + createQuestJournal({ + events: d.events, + rewards: { + grantXp(userId, amount) { + const existing = d.ensureInstanceStats(userId)["xp"]; + const current = (existing?.current ?? 0) + amount; + d.seedUserPool(userId, "xp", { current, max: Math.max(existing?.max ?? 0, current) }); + }, + grantEconomy: (userId, currencyId, amount) => d.economy.grant(userId, currencyId, amount), + grantItem(userId, inventoryId, itemId, count) { + if (d.layouts[inventoryId] === undefined) return { reason: `unknown inventory "${inventoryId}"` }; + const result = d.inventoryFor(userId).put(inventoryId, itemId, count); + return result.status === "ok" ? null : { reason: result.reason }; + }, + grantUnlock: (userId, unlockId) => d.feature("unlocks")?.grant(userId, unlockId), + }, + hasUnlock: (userId, id) => d.feature("unlocks")?.has(userId, id) ?? false, + }), + ["accept", "abandon", "progress", "turnIn", "grant", "revoke", "hydrate"], + d.signalNotify, + ); + return { + value: quest, + save: { + key: "quest", + snapshot: () => quest.snapshotAll(), + hydrate: (data) => quest.hydrateAll(data as Record), + }, + }; + }, + }, + { + key: "dialogue", + enabled: (f) => f.dialogue === true, + create(d) { + const dialogue = createGameDialogue(d.store); + d.commandRegistry.define("dialogue.open", { + apply(state, input) { + const id = (input as { id?: string }).id; + if (id !== undefined) state.game.dialogue?.open(id); + }, + }); + d.commandRegistry.define("dialogue.close", { + apply(state) { + state.game.dialogue?.close(); + }, + }); + return { value: dialogue }; + }, + }, + { + key: "players", + enabled: (f) => f.players === true, + create(d) { + return { value: notifyAfter(createConnectedPlayers(), ["join", "leave"], d.signalNotify) }; + }, + }, + { + key: "cards", + enabled: (f) => f.cards === true, + create(d) { + return { + value: { pile: d.pile } satisfies GameContextCards, + save: { + key: "cards", + snapshot: () => { + const out: Record = {}; + for (const [id, cardPile] of d.cardPiles) out[id] = cardPile.state(); + return out; + }, + hydrate: (data) => { + for (const [id, state] of Object.entries(data as Record)) { + d.cardPiles.get(id)?.reset(state); + } + }, + }, + }; + }, + }, + { + key: "turn", + enabled: (f) => f.turn === true, + create(d) { + return { + value: { loop: d.loop } satisfies GameContextTurn, + save: { + key: "turn", + snapshot: () => { + const out: Record = {}; + for (const [id, turnLoop] of d.turnLoops) out[id] = turnLoop.capture(); + return out; + }, + hydrate: (data) => { + for (const [id, state] of Object.entries(data as Record)) { + d.turnLoops.get(id)?.restore(state); + } + }, + }, + }; + }, + }, + { + key: "race", + enabled: (f) => f.race === true, + create(d) { + return { value: { state: d.raceState } satisfies GameContextRace }; + }, + }, +]; diff --git a/packages/core/src/runtime/gameContext.ts b/packages/core/src/runtime/gameContext.ts index 4a19bc919..699cfaafb 100644 --- a/packages/core/src/runtime/gameContext.ts +++ b/packages/core/src/runtime/gameContext.ts @@ -1,4 +1,4 @@ -import { createCardPile, type CardPile, type CardPileConfig, type CardPileState } from "../cards/cardPile"; +import { type CardPile, type CardPileConfig } from "../cards/cardPile"; import { createDeathSystem, deathReasonFromEffect, normalizeOnDeath, type OnDeathSpec } from "../combat/death"; import { createEffectSystem, @@ -12,56 +12,48 @@ import { } from "../combat/effects"; import { createProjectileSystem, type ProjectileSystem } from "../combat/projectiles"; import { - resolveHitReaction, type HitReaction, type HitReactionConfig, type ImpactPresetName, } from "../combat/hitReaction"; import { - pointInTelegraph, - type TelegraphConfig, type TelegraphShape, } from "../combat/telegraph"; import { createCommandRegistry, type CommandDefinition, - type CommandRegistry, type CommandResult, } from "../commands/commandRegistry"; import { balance as walletBalance, - canAfford as walletCanAfford, charge as walletCharge, - chargeAll as walletChargeAll, createEmptyWallet, grant as walletGrant, isOverdrawn as walletIsOverdrawn, type ChargeOptions as WalletChargeOptions, type WalletState, } from "../economy/wallet"; -import { createCosmetics, type Cosmetics } from "../game/cosmetics"; +import { type Cosmetics } from "../game/cosmetics"; import type { GameDefinition, GameFeatures, PersistConfig } from "../game/defineGame"; import { groundFieldFor, type TerrainField } from "../world/terrain"; import { createGameEvents, type GameEventMap, type GameEvents, type VfxKind } from "../game/events"; -import { createGameFeed, type FeedEntry, type GameFeed } from "../game/feed"; +import { createGameFeed, type GameFeed } from "../game/feed"; import { setGamePhase } from "../game/gamePhase"; -import { createLeaderboard, type Leaderboard, type LeaderboardRow } from "../game/leaderboard"; +import { type Leaderboard } from "../game/leaderboard"; import { createLoadouts, type Loadouts } from "../game/loadout"; import { createLootRegistry, grantDrops, type Drop, type LootTableDef } from "../game/lootTable"; -import { createGameDialogue, type GameDialogue } from "../game/dialogue"; -import { createQuestJournal, type QuestJournal, type QuestSnapshotEntry } from "../game/quest"; +import { type GameDialogue } from "../game/dialogue"; +import { type QuestJournal } from "../game/quest"; import { - createWorldItemStore, resolveDeathDrops, DEFAULT_RARITY, - WORLD_ITEM_ENTITY_NAME, type WorldItemRecord, type WorldItemSpawnInput, } from "../game/worldItem"; -import { createChat, type Chat, type ChatSnapshot } from "../game/chat"; -import { createSocial, type Social, type SocialSnapshot } from "../game/social"; -import { createTradeSystem, type TradeField, type TradeSystem } from "../game/trade"; -import { createUnlocks, type Unlocks } from "../game/unlocks"; +import { type Chat } from "../game/chat"; +import { createSocial, type Social } from "../game/social"; +import { type TradeField, type TradeSystem } from "../game/trade"; +import { type Unlocks } from "../game/unlocks"; import { createInventorySet, putItem, @@ -80,16 +72,14 @@ import { type ItemUseResult, } from "../item/use"; import { createWeaponStats, type WeaponStats } from "../item/weapon"; -import { createPoseState, type PoseAllowedStates, type PoseSnapshot, type PoseState } from "../movement/poseState"; +import { createPoseState, type PoseAllowedStates, type PoseState } from "../movement/poseState"; import type { ModelAssetRef } from "../scene/assetCatalog"; import { createBodyBind, type BodyBind } from "../scene/bodyBind"; import { createPaintLayer, type PaintLayer } from "../scene/paintLayer"; import { createEntityStatsApi, - hydrateEntityStats, seedStatValues, setStatValue, - snapshotEntityStats, type EntityStatsApi, type StatCatalog, type StatValueMap, @@ -108,10 +98,10 @@ import { createForms, type Forms } from "../scene/form"; import { scaledEntityColliders, scaledObjectColliders, type EntityColliderSet } from "../scene/colliders"; import { raycastObjects, raycastObjectsAll, type ObjectRaycastHit, type ObjectRaycastInput } from "../scene/objectQuery"; import { createObjectStore, objectVisualScale, type ObjectStore } from "../scene/objectStore"; -import { createRoster, type Roster, type RosterEntry } from "../scene/roster"; +import { type Roster } from "../scene/roster"; import { createSelectionSet, type SelectionSet } from "../scene/selection"; -import { createConnectedPlayers, type ConnectedPlayers } from "../game/connectedPlayers"; -import { createPossession, type Possession, type PossessionSnapshot } from "../scene/possession"; +import { type ConnectedPlayers } from "../game/connectedPlayers"; +import { createPossession, type Possession } from "../scene/possession"; import { createSceneRaycast, type SceneRaycastApi, @@ -141,12 +131,17 @@ import { import { createRuntimeSave, type RuntimeSave, type RuntimeSaveOptions, type RuntimeSaveTarget } from "./runtimeSave"; import { isOffline } from "./adapter"; import { localSaveBackend, memorySaveBackend } from "../game/saveStore"; -import { createSimClock, type ClockSnapshot, type SimClock } from "../time/simClock"; -import { createTurnLoop, type TurnLoop, type TurnLoopConfig, type TurnLoopSnapshot } from "../turn/turnLoop"; -import { RaceState, type RaceEvent, type RaceStateConfig } from "../game/race"; +import { createSimClock, type SimClock } from "../time/simClock"; +import { type TurnLoop, type TurnLoopConfig } from "../turn/turnLoop"; +import { RaceState, type RaceStateConfig } from "../game/race"; import { createCameraDirector, type CameraDirector } from "./cameraDirector"; import { createInputSnapshot, type InputSnapshot } from "./inputSnapshot"; -import { createMotionIntents, type MotionIntentBatch, type MotionIntents } from "./motionIntents"; +import { createMotionIntents, type MotionIntents } from "./motionIntents"; +import { baselineDescriptors, type BaselineDeps } from "./descriptors/baseline"; +import { featureDescriptors, type FeatureDeps } from "./descriptors/features"; +import { createCombatFx } from "./context/combatFx"; +import { createContextRegistries } from "./context/registries"; +import { createWorldItemContext } from "./context/worldItems"; export interface GameContextItemEntry { use?: string; @@ -525,344 +520,6 @@ export interface GameContext { replicatesPerViewer(): boolean; } -/** - * Shared wiring every optional-feature descriptor draws from — the live core subsystems (entities, - * spatial, economy, inventory) plus reactive plumbing (`signalNotify`) and a `feature` reader for the - * few features that reference another (quest reads unlocks). Handed to each {@link FeatureDescriptor}'s - * `create` so a new opt-in subsystem plugs into one registration, never a new `features.x ?` branch. - */ -interface FeatureDeps { - features: GameFeatures; - signalNotify: () => void; - events: GameEvents; - now: () => number; - entities: EntityStore; - spatial: SpatialApi; - store: ObservableKeyedStore; - commandRegistry: CommandRegistry; - economy: GameContextEconomy; - content: GameContextContent; - activeUserId: () => string; - walletOf: (userId: string) => WalletState; - setWallet: (userId: string, state: WalletState) => void; - layouts: Record; - inventoryFor: (userId: string) => InventorySet; - ensureInstanceStats: (instanceId: string) => StatValueMap; - seedUserPool: (userId: string, statId: string, pool: { current: number; max?: number; min?: number }) => void; - sharedSocial: () => Social; - pile: (id: string, config?: CardPileConfig) => CardPile; - loop: (id: string, config?: TurnLoopConfig) => TurnLoop; - cardPiles: ReadonlyMap; - turnLoops: ReadonlyMap; - raceState: (id: string, config?: RaceStateConfig) => RaceState; - feature: (key: keyof GameFeatures) => T | undefined; -} - -/** What a descriptor produces: the `ctx`-facing value plus its optional replication/save modules. */ -interface FeatureBuild { - value: unknown; - /** Registered into the host→client replication set when present (`ctx.snapshot`/`ctx.hydrate`). */ - replicate?: SnapshotModule; - /** Registered into the whole-world save-only set when present (`ctx.game.save`). */ - save?: SnapshotModule; -} - -/** - * One opt-in subsystem expressed as data: which `features` flag turns it on, how it wires itself from - * {@link FeatureDeps}, and whether it replicates or persists. `createGameContext` iterates the - * descriptor list instead of hand-wiring each `features.x ? create : undefined` branch — the seam a - * new feature extends through. - */ -interface FeatureDescriptor { - readonly key: keyof GameFeatures; - enabled(features: GameFeatures): boolean; - create(deps: FeatureDeps): FeatureBuild; -} - -const featureDescriptors: readonly FeatureDescriptor[] = [ - { - key: "unlocks", - enabled: (f) => f.unlocks === true, - create(d) { - const unlocks = notifyAfter(createUnlocks(), ["grant", "hydrate"], d.signalNotify); - return { - value: unlocks, - save: { - key: "unlocks", - snapshot: () => unlocks.snapshotAll(), - hydrate: (data) => unlocks.hydrateAll(data as Record), - }, - }; - }, - }, - { - key: "social", - enabled: (f) => f.social === true, - create(d) { - const raw = d.sharedSocial(); - const social: Social = { - friends: notifyAfter( - raw.friends, - ["request", "accept", "decline", "remove", "block", "hydrate"], - d.signalNotify, - ), - party: notifyAfter( - raw.party, - ["invite", "accept", "decline", "kick", "leave", "promote"], - d.signalNotify, - ), - presence: raw.presence, - emotes: raw.emotes, - worldInvites: notifyAfter(raw.worldInvites, ["invite", "accept", "decline"], d.signalNotify), - snapshot: raw.snapshot, - hydrate: (data) => { - raw.hydrate(data); - d.signalNotify(); - }, - }; - return { - value: social, - replicate: { - key: "social", - snapshot: () => social.snapshot(), - hydrate: (data) => social.hydrate(data as SocialSnapshot), - }, - }; - }, - }, - { - key: "chat", - enabled: (f) => f.chat === true, - create(d) { - const raw = d.sharedSocial(); - const chat = notifyAfter( - createChat({ - events: d.events, - now: d.now, - party: raw.party, - proximity: { - entities: { get: (id) => d.entities.get(id) }, - spatial: { inRadius: (center, radius, filter) => d.spatial.inRadius(center, radius, filter) }, - }, - blockedBy: (userId) => raw.friends.snapshot(userId).blocked, - }), - ["register", "send", "whisper", "hydrate"], - d.signalNotify, - ); - return { - value: chat, - replicate: { - key: "chat", - snapshot: () => chat.snapshot(), - hydrate: (data) => chat.hydrate(data as ChatSnapshot), - }, - }; - }, - }, - { - key: "leaderboard", - enabled: (f) => f.leaderboard === true, - create(d) { - const leaderboard = notifyAfter(createLeaderboard(), ["increment", "hydrate"], d.signalNotify); - return { - value: leaderboard, - replicate: { - key: "leaderboard", - snapshot: () => leaderboard.snapshot(), - hydrate: (data) => leaderboard.hydrate(data as LeaderboardRow[]), - }, - }; - }, - }, - { - key: "roster", - enabled: (f) => f.roster === true, - create(d) { - const roster = notifyAfter( - createRoster({ now: d.now }), - ["capture", "release", "setEquipped", "hydrate"], - d.signalNotify, - ); - return { - value: roster, - save: { - key: "roster", - snapshot: () => roster.snapshotAll(), - hydrate: (data) => roster.hydrateAll(data as Record), - }, - }; - }, - }, - { - key: "cosmetics", - enabled: (f) => f.cosmetics === true, - create(d) { - const cosmetics = notifyAfter(createCosmetics({ events: d.events }), ["apply", "equip", "hydrate"], d.signalNotify); - return { - value: cosmetics, - save: { - key: "cosmetics", - snapshot: () => cosmetics.snapshotAll(), - hydrate: (data) => cosmetics.hydrateAll(data as Record>), - }, - }; - }, - }, - { - key: "trade", - enabled: (f) => f.trade === true, - create(d) { - return { - value: createTradeSystem({ - resolveTrade: (itemId) => d.content.itemById?.(itemId)?.trade, - wallet: { - canAfford: (costs) => - walletCanAfford(d.walletOf(d.activeUserId()), costs) ? null : "insufficient-funds", - charge(costs) { - const result = walletChargeAll(d.walletOf(d.activeUserId()), costs); - if (result.status === "ok") { - d.setWallet(d.activeUserId(), result.state); - d.signalNotify(); - } - }, - grant(gains) { - for (const [currencyId, amount] of Object.entries(gains)) { - d.economy.grant(d.activeUserId(), currencyId, amount); - } - }, - }, - inventory: { - put(inventoryId, itemId, count) { - if (d.layouts[inventoryId] === undefined) return { reason: `unknown inventory "${inventoryId}"` }; - const result = d.inventoryFor(d.activeUserId()).put(inventoryId, itemId, count); - return result.status === "ok" ? null : { reason: result.reason }; - }, - take(inventoryId, itemId, count) { - if (d.layouts[inventoryId] === undefined) return { reason: `unknown inventory "${inventoryId}"` }; - const result = d.inventoryFor(d.activeUserId()).take(inventoryId, itemId, count); - return result.status === "ok" ? null : { reason: result.reason }; - }, - count: (inventoryId, itemId) => d.inventoryFor(d.activeUserId()).count(inventoryId, itemId), - }, - }), - }; - }, - }, - { - key: "quest", - enabled: (f) => f.quest === true, - create(d) { - const quest = notifyAfter( - createQuestJournal({ - events: d.events, - rewards: { - grantXp(userId, amount) { - const existing = d.ensureInstanceStats(userId)["xp"]; - const current = (existing?.current ?? 0) + amount; - d.seedUserPool(userId, "xp", { current, max: Math.max(existing?.max ?? 0, current) }); - }, - grantEconomy: (userId, currencyId, amount) => d.economy.grant(userId, currencyId, amount), - grantItem(userId, inventoryId, itemId, count) { - if (d.layouts[inventoryId] === undefined) return { reason: `unknown inventory "${inventoryId}"` }; - const result = d.inventoryFor(userId).put(inventoryId, itemId, count); - return result.status === "ok" ? null : { reason: result.reason }; - }, - grantUnlock: (userId, unlockId) => d.feature("unlocks")?.grant(userId, unlockId), - }, - hasUnlock: (userId, id) => d.feature("unlocks")?.has(userId, id) ?? false, - }), - ["accept", "abandon", "progress", "turnIn", "grant", "revoke", "hydrate"], - d.signalNotify, - ); - return { - value: quest, - save: { - key: "quest", - snapshot: () => quest.snapshotAll(), - hydrate: (data) => quest.hydrateAll(data as Record), - }, - }; - }, - }, - { - key: "dialogue", - enabled: (f) => f.dialogue === true, - create(d) { - const dialogue = createGameDialogue(d.store); - d.commandRegistry.define("dialogue.open", { - apply(state, input) { - const id = (input as { id?: string }).id; - if (id !== undefined) state.game.dialogue?.open(id); - }, - }); - d.commandRegistry.define("dialogue.close", { - apply(state) { - state.game.dialogue?.close(); - }, - }); - return { value: dialogue }; - }, - }, - { - key: "players", - enabled: (f) => f.players === true, - create(d) { - return { value: notifyAfter(createConnectedPlayers(), ["join", "leave"], d.signalNotify) }; - }, - }, - { - key: "cards", - enabled: (f) => f.cards === true, - create(d) { - return { - value: { pile: d.pile } satisfies GameContextCards, - save: { - key: "cards", - snapshot: () => { - const out: Record = {}; - for (const [id, cardPile] of d.cardPiles) out[id] = cardPile.state(); - return out; - }, - hydrate: (data) => { - for (const [id, state] of Object.entries(data as Record)) { - d.cardPiles.get(id)?.reset(state); - } - }, - }, - }; - }, - }, - { - key: "turn", - enabled: (f) => f.turn === true, - create(d) { - return { - value: { loop: d.loop } satisfies GameContextTurn, - save: { - key: "turn", - snapshot: () => { - const out: Record = {}; - for (const [id, turnLoop] of d.turnLoops) out[id] = turnLoop.capture(); - return out; - }, - hydrate: (data) => { - for (const [id, state] of Object.entries(data as Record)) { - d.turnLoops.get(id)?.restore(state); - } - }, - }, - }; - }, - }, - { - key: "race", - enabled: (f) => f.race === true, - create(d) { - return { value: { state: d.raceState } satisfies GameContextRace }; - }, - }, -]; - export function createGameContext( options: GameContextOptions, ): GameContext { @@ -1237,42 +894,13 @@ export function createGameContext return created; } - const worldItems = notifyAfter( - createWorldItemStore({ - spawnEntity: (position) => entities.spawn(WORLD_ITEM_ENTITY_NAME, { position, role: "prop" }), - despawnEntity, - resolvePosition: (instanceId) => entities.get(instanceId)?.position, - }), - ["spawn", "take", "remove"], - signal.notify, - ); - - function spawnWorldItem(input: WorldItemSpawnInput): WorldItemRecord { - const record = worldItems.spawn(input); - events.emit("worldItem.dropped", { - instanceId: record.instanceId, - itemId: record.itemId, - rarity: record.rarity, - count: record.count, - position: [input.position[0], input.position[1], input.position[2]], - ...(record.source !== undefined ? { source: record.source } : {}), - }); - return record; - } - - function pickupWorldItem(instanceId: string, userId: string): WorldItemPickupResult { - const record = worldItems.take(instanceId); - if (record === null) return { status: "rejected", reason: "not-found" }; - loot.grantToPlayer(userId, [{ item: record.itemId, count: record.count }], "worldItem.pickup"); - events.emit("worldItem.picked_up", { - instanceId, - userId, - itemId: record.itemId, - rarity: record.rarity, - count: record.count, - }); - return { status: "ok", record }; - } + const { worldItems, spawnWorldItem, pickupWorldItem } = createWorldItemContext({ + entities, + events, + despawnEntity, + grantToPlayer: loot.grantToPlayer, + signalNotify: signal.notify, + }); const death = createDeathSystem({ resolveOnDeath: (instanceId) => catalogEntry(instanceId)?.onDeath, @@ -1337,148 +965,12 @@ export function createGameContext signal.notify, ); - function emitFloatText(input: FloatTextInput): void { - const position = - input.position ?? - (input.instanceId === undefined ? undefined : entities.get(input.instanceId)?.position); - if (position === undefined) return; - const text = input.text ?? (input.amount === undefined ? "" : String(Math.round(input.amount))); - const event: GameEventMap["entity.floatText"] = { - position: [position[0], position[1], position[2]], - text, - kind: input.kind ?? "info", - }; - if (input.instanceId !== undefined) event.instanceId = input.instanceId; - if (input.amount !== undefined) event.amount = input.amount; - if (input.hitType !== undefined) event.hitType = input.hitType; - if (input.element !== undefined) event.element = input.element; - if (input.crit !== undefined) event.crit = input.crit; - if (input.scale !== undefined) event.scale = input.scale; - events.emit("entity.floatText", event); - } - - const vfxDefaultDurationMs: Record = { - projectile: 380, - beam: 260, - nova: 520, - glow: 700, - spark: 240, - }; - let vfxSeq = 0; - - function resolveVfxPoint( - ref: string | readonly [number, number, number] | undefined, - ): [number, number, number] | undefined { - if (ref === undefined) return undefined; - if (typeof ref === "string") { - const entity = entities.get(ref); - if (entity === null) return undefined; - return [entity.position[0], entity.position[1], entity.position[2]]; - } - return [ref[0], ref[1], ref[2]]; - } - - function emitVfx(input: VfxInput): void { - const to = resolveVfxPoint(input.to); - const from = resolveVfxPoint(input.from) ?? to; - if (from === undefined) return; - const event: GameEventMap["combat.vfx"] = { - id: vfxSeq++, - kind: input.kind, - color: input.color, - from, - durationMs: input.durationMs ?? vfxDefaultDurationMs[input.kind], - }; - if (to !== undefined) event.to = to; - if (input.radius !== undefined) event.radius = input.radius; - events.emit("combat.vfx", event); - } - - let telegraphSeq = 0; - - function fireTelegraph(input: TelegraphInput): () => void { - const id = telegraphSeq++; - const telegraphEvent: GameEventMap["combat.telegraph"] = { - id, - shape: input.shape, - position: [input.at[0], input.at[1], input.at[2]], - windupMs: input.windupMs, - kind: input.kind ?? "danger", - }; - if (input.dir !== undefined) telegraphEvent.dir = input.dir; - events.emit("combat.telegraph", telegraphEvent); - const cancelVisual = () => events.emit("combat.telegraphCancelled", { id }); - const bound = input.effect; - if (bound === undefined) return cancelVisual; - const config: TelegraphConfig = { shape: input.shape, at: input.at, windupMs: input.windupMs }; - if (input.dir !== undefined) config.dir = input.dir; - const cancelEffect = time.after(input.windupMs / 1000, () => { - const targets = entities.list().filter((entity) => pointInTelegraph(config, entity.position)); - for (const target of targets) { - applyEffectAndFloat({ - from: input.from, - to: target.id, - effect: bound.effect, - ...(bound.via === undefined ? {} : { via: bound.via }), - }); - } - }); - return () => { - cancelEffect(); - cancelVisual(); - }; - } - - function applyHitReaction(input: HitReactionInput): HitReaction | null { - const attacker = entities.get(input.from); - const target = entities.get(input.to); - if (target === null) return null; - const attackerPos = attacker?.position ?? target.position; - const reaction = resolveHitReaction(input.config, { - attackerPos, - targetPos: target.position, - ...(input.power === undefined ? {} : { power: input.power }), - }); - entities.setPose(input.to, { - position: [ - target.position[0] + reaction.impulse[0], - target.position[1] + reaction.impulse[1], - target.position[2] + reaction.impulse[2], - ], - rotationY: target.rotationY, - }); - const reactionEvent: GameEventMap["combat.hitReaction"] = { - instanceId: input.to, - position: [target.position[0], target.position[1], target.position[2]], - hitstopMs: reaction.hitstopMs, - }; - if (reaction.shake !== null) reactionEvent.shake = reaction.shake; - if (reaction.trauma !== null) reactionEvent.trauma = reaction.trauma; - events.emit("combat.hitReaction", reactionEvent); - return reaction; - } - - function applyEffectAndFloat(input: EffectInput): EffectResult[] { - const positionsBefore = new Map(); - for (const entity of entities.list()) positionsBefore.set(entity.id, entity.position); - const results = effects.applyEffect(input); - for (const result of results) { - let total = 0; - for (const delta of result.applied) total += delta.delta; - if (total === 0) continue; - const position = entities.get(result.instanceId)?.position ?? positionsBefore.get(result.instanceId); - if (position === undefined) continue; - const magnitude = Math.abs(total); - emitFloatText({ - instanceId: result.instanceId, - position: [position[0], position[1], position[2]], - text: String(Math.round(magnitude)), - kind: total < 0 ? "damage" : "heal", - amount: magnitude, - }); - } - return results; - } + const { emitFloatText, emitVfx, fireTelegraph, applyHitReaction, applyEffectAndFloat } = createCombatFx({ + entities, + events, + time, + applyEffect: (input) => effects.applyEffect(input), + }); const floatingEffects: EffectSystem = { canReceive: effects.canReceive, @@ -1575,96 +1067,7 @@ export function createGameContext const store = notifyAfter(createObservableKeyedStore(), ["set", "delete", "hydrate"], signal.notify); - const cardPiles = new Map(); - function pile(id: string, config?: CardPileConfig): CardPile { - const existing = cardPiles.get(id); - if (existing !== undefined) return existing; - if (config === undefined) { - throw new Error(`cardPile "${id}" has not been created yet; pass a config on first access`); - } - const created = notifyAfter( - createCardPile(config), - ["shuffle", "draw", "discard", "exhaust", "move", "reset"], - signal.notify, - ); - cardPiles.set(id, created); - return created; - } - - const turnLoops = new Map(); - function loop(id: string, config?: TurnLoopConfig): TurnLoop { - const existing = turnLoops.get(id); - if (existing !== undefined) return existing; - if (config === undefined) { - throw new Error(`turn loop "${id}" has not been created yet; pass a config on first access`); - } - const raw = createTurnLoop(config); - const wrappedCommit = notifyAfter( - raw.commit, - ["submit", "expected", "commit", "discard", "clear"], - signal.notify, - ); - const wrapped: TurnLoop = { - ...notifyAfter( - raw, - [ - "setOrder", - "addParticipant", - "removeParticipant", - "advancePhase", - "advanceTurn", - "advanceRound", - "spend", - "gain", - "refill", - "restore", - ], - signal.notify, - ), - commit: wrappedCommit, - }; - turnLoops.set(id, wrapped); - return wrapped; - } - - class NotifyingRaceState extends RaceState { - override addRacer(racerId: string, startTime?: number): void { - super.addRacer(racerId, startTime); - signal.notify(); - } - override removeRacer(racerId: string): void { - super.removeRacer(racerId); - signal.notify(); - } - override reset(): void { - super.reset(); - signal.notify(); - } - override eliminate(racerId: string): void { - super.eliminate(racerId); - signal.notify(); - } - override update( - now: number, - positions: Record | Map, - ): readonly RaceEvent[] { - const raceEvents = super.update(now, positions); - if (raceEvents.length > 0) signal.notify(); - return raceEvents; - } - } - - const raceStates = new Map(); - function raceState(id: string, config?: RaceStateConfig): RaceState { - const existing = raceStates.get(id); - if (existing !== undefined) return existing; - if (config === undefined) { - throw new Error(`race "${id}" has not been created yet; pass a config on first access`); - } - const created = new NotifyingRaceState(config); - raceStates.set(id, created); - return created; - } + const { pile, loop, raceState, cardPiles, turnLoops } = createContextRegistries(signal.notify); const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic", "setChaseTuning"], signal.notify); const input = createInputSnapshot(); @@ -1714,37 +1117,25 @@ export function createGameContext if (build.save !== undefined) featureSaveModules.push(build.save); } + const baselineDeps: BaselineDeps = { + signalNotify: signal.notify, + entities, + statsByInstance, + store, + feed, + inventoryIds, + inventoryByUser, + inventoryFor, + wallets, + time, + pose, + possession, + motionByUser, + motionFor, + }; + const baselineBuilds = baselineDescriptors.map((descriptor) => descriptor.create(baselineDeps)); const snapshotModules: SnapshotModule[] = [ - { key: "entities", snapshot: () => entities.snapshot(), hydrate: (data) => entities.hydrate(data as SceneEntity[]) }, - { - key: "stats", - snapshot: () => snapshotEntityStats(statsByInstance), - hydrate: (data) => hydrateEntityStats(statsByInstance, data as Record), - }, - { - key: "store", - snapshot: () => store.snapshot(), - hydrate: (data) => store.hydrate(data as readonly (readonly [string, unknown])[]), - }, - { key: "feed", snapshot: () => feed.snapshot(), hydrate: (data) => feed.hydrate(data as Record) }, - { - key: "inventory", - snapshot: () => { - const byUser: Record> = {}; - for (const [userId, set] of inventoryByUser) { - const states: Record = {}; - for (const inventoryId of inventoryIds) states[inventoryId] = set.state(inventoryId); - byUser[userId] = states; - } - return byUser; - }, - hydrate: (data) => { - for (const [userId, states] of Object.entries(data as Record>)) { - const set = inventoryFor(userId); - for (const [inventoryId, state] of Object.entries(states)) set.replaceState(inventoryId, state); - } - }, - }, + ...baselineBuilds.flatMap((build) => (build.replicate === undefined ? [] : [build.replicate])), ...featureReplicateModules, ]; @@ -1791,37 +1182,7 @@ export function createGameContext */ const saveModules: SnapshotModule[] = [ ...snapshotModules, - { - key: "economy", - snapshot: () => Object.fromEntries(wallets), - hydrate: (data) => { - wallets.clear(); - for (const [userId, state] of Object.entries(data as Record)) { - wallets.set(userId, state); - } - signal.notify(); - }, - }, - { key: "time", snapshot: () => time.snapshot(), hydrate: (data) => time.hydrate(data as ClockSnapshot) }, - { key: "pose", snapshot: () => pose.snapshotAll(), hydrate: (data) => pose.hydrateAll(data as PoseSnapshot) }, - { - key: "possession", - snapshot: () => possession.snapshotAll(), - hydrate: (data) => possession.hydrateAll(data as PossessionSnapshot), - }, - { - key: "motion", - snapshot: () => { - const out: Record = {}; - for (const [userId, queue] of motionByUser) out[userId] = queue.snapshot(); - return out; - }, - hydrate: (data) => { - for (const [userId, batch] of Object.entries(data as Record)) { - motionFor(userId).hydrate(batch); - } - }, - }, + ...baselineBuilds.flatMap((build) => (build.save === undefined ? [] : [build.save])), ...featureSaveModules, ]; diff --git a/scripts/api-doc-baseline.json b/scripts/api-doc-baseline.json index 106f0c3f9..668655335 100644 --- a/scripts/api-doc-baseline.json +++ b/scripts/api-doc-baseline.json @@ -221,6 +221,38 @@ "@jgengine/core/cards/modifierPipeline#TraceStep", "@jgengine/core/cards/modifierPipeline#createModifierPipeline", "@jgengine/core/cards/modifierPipeline#runPipeline", + "@jgengine/core/combat#AbilityKit", + "@jgengine/core/combat#AbilitySlotSnapshot", + "@jgengine/core/combat#AbilitySlotState", + "@jgengine/core/combat#AnimationClip", + "@jgengine/core/combat#BuildupProc", + "@jgengine/core/combat#CheckAdvantage", + "@jgengine/core/combat#CheckResult", + "@jgengine/core/combat#ComboStep", + "@jgengine/core/combat#EventMeter", + "@jgengine/core/combat#EventMeterFeedResult", + "@jgengine/core/combat#FrameRange", + "@jgengine/core/combat#HitReactionConfig", + "@jgengine/core/combat#MeterAddResult", + "@jgengine/core/combat#ObjectRaycastHit", + "@jgengine/core/combat#OnDeathSpec", + "@jgengine/core/combat#ProjectileSystemDeps", + "@jgengine/core/combat#RaycastHit", + "@jgengine/core/combat#ReceiveMap", + "@jgengine/core/combat#ResourcePool", + "@jgengine/core/combat#Stats", + "@jgengine/core/combat#TelegraphConfig", + "@jgengine/core/combat#TelegraphShape", + "@jgengine/core/combat#advanceCombo", + "@jgengine/core/combat#attackMeta", + "@jgengine/core/combat#counters", + "@jgengine/core/combat#isBlockable", + "@jgengine/core/combat#isDodgeable", + "@jgengine/core/combat#isParryable", + "@jgengine/core/combat#resistanceScale", + "@jgengine/core/combat#resolveDefense", + "@jgengine/core/combat#resolveResistance", + "@jgengine/core/combat#resolveShot", "@jgengine/core/combat/abilityKit#AbilityCastReason", "@jgengine/core/combat/abilityKit#AbilityCastResult", "@jgengine/core/combat/abilityKit#AbilityCastType", @@ -345,6 +377,7 @@ "@jgengine/core/combat/telegraph#HazardPhase", "@jgengine/core/combat/telegraph#TelegraphConfig", "@jgengine/core/combat/telegraph#TelegraphShape", + "@jgengine/core/commands/commandRegistry#CommandDecodeResult", "@jgengine/core/commands/commandRegistry#CommandDefinition", "@jgengine/core/commands/commandRegistry#CommandRegistry", "@jgengine/core/commands/commandRegistry#CommandRejection", @@ -539,7 +572,6 @@ "@jgengine/core/game/events#CombatTelegraphCancelledEvent", "@jgengine/core/game/events#CombatTelegraphEvent", "@jgengine/core/game/events#CosmeticsChangedEvent", - "@jgengine/core/game/events#DeathReason", "@jgengine/core/game/events#EmotePlayedEvent", "@jgengine/core/game/events#EntityDiedEvent", "@jgengine/core/game/events#EntityFloatTextEvent", @@ -720,6 +752,170 @@ "@jgengine/core/game/worldItem#WorldItemSpawnInput", "@jgengine/core/game/worldItem#WorldItemStore", "@jgengine/core/game/worldItem#WorldItemStoreDeps", + "@jgengine/core/gameplay#ActionCodes", + "@jgengine/core/gameplay#ActionStateTracker", + "@jgengine/core/gameplay#AffixPool", + "@jgengine/core/gameplay#AxisBindingMap", + "@jgengine/core/gameplay#AxisChannelConfig", + "@jgengine/core/gameplay#AxisInput", + "@jgengine/core/gameplay#BehaviourWorld", + "@jgengine/core/gameplay#CAMERA_FRUSTUM_DEFAULTS", + "@jgengine/core/gameplay#CardPile", + "@jgengine/core/gameplay#CardPileState", + "@jgengine/core/gameplay#Cell", + "@jgengine/core/gameplay#CellGrid", + "@jgengine/core/gameplay#Chat", + "@jgengine/core/gameplay#ChatMessage", + "@jgengine/core/gameplay#ChatRateLimit", + "@jgengine/core/gameplay#ChatSendResult", + "@jgengine/core/gameplay#CombatTelegraphEvent", + "@jgengine/core/gameplay#CropDef", + "@jgengine/core/gameplay#CropTileState", + "@jgengine/core/gameplay#Curve", + "@jgengine/core/gameplay#DEFAULT_CHAT_BODY_LENGTH", + "@jgengine/core/gameplay#DEFAULT_CHAT_HISTORY_LIMIT", + "@jgengine/core/gameplay#DEFAULT_CHAT_RATE_LIMIT", + "@jgengine/core/gameplay#DEFAULT_PICKUP_RADIUS", + "@jgengine/core/gameplay#DeliveryEntry", + "@jgengine/core/gameplay#DeliveryQueue", + "@jgengine/core/gameplay#DirectionalLightingConfig", + "@jgengine/core/gameplay#DurabilitySpec", + "@jgengine/core/gameplay#DurabilityState", + "@jgengine/core/gameplay#EntityDiedEvent", + "@jgengine/core/gameplay#EntityFloatTextEvent", + "@jgengine/core/gameplay#EntitySpriteConfig", + "@jgengine/core/gameplay#FeedEntry", + "@jgengine/core/gameplay#FirstPersonCameraConfig", + "@jgengine/core/gameplay#FriendEntry", + "@jgengine/core/gameplay#FriendRequestEntry", + "@jgengine/core/gameplay#Friends", + "@jgengine/core/gameplay#GameCameraConfig", + "@jgengine/core/gameplay#GameEventMap", + "@jgengine/core/gameplay#GameEvents", + "@jgengine/core/gameplay#InstalledPart", + "@jgengine/core/gameplay#InventorySlot", + "@jgengine/core/gameplay#InventoryState", + "@jgengine/core/gameplay#ItemUseHandler", + "@jgengine/core/gameplay#ItemUseInput", + "@jgengine/core/gameplay#LaneRule", + "@jgengine/core/gameplay#LeaderboardRow", + "@jgengine/core/gameplay#LeaderboardScope", + "@jgengine/core/gameplay#LevelProgress", + "@jgengine/core/gameplay#LevelSequence", + "@jgengine/core/gameplay#LevelingConfig", + "@jgengine/core/gameplay#LevelingTrack", + "@jgengine/core/gameplay#LoadoutDef", + "@jgengine/core/gameplay#LootFilterRule", + "@jgengine/core/gameplay#ModelConfig", + "@jgengine/core/gameplay#ModularItemDef", + "@jgengine/core/gameplay#MountSlotDef", + "@jgengine/core/gameplay#NEUTRAL_AXIS", + "@jgengine/core/gameplay#ObjectStyle", + "@jgengine/core/gameplay#PING_FEED_ACTION", + "@jgengine/core/gameplay#PartDef", + "@jgengine/core/gameplay#Party", + "@jgengine/core/gameplay#PartyInviteEntry", + "@jgengine/core/gameplay#PartyMemberEntry", + "@jgengine/core/gameplay#PingCategory", + "@jgengine/core/gameplay#PingSystem", + "@jgengine/core/gameplay#PlayableGame", + "@jgengine/core/gameplay#PointerAxisState", + "@jgengine/core/gameplay#PointerConfig", + "@jgengine/core/gameplay#PointerVec3", + "@jgengine/core/gameplay#PresenceInfo", + "@jgengine/core/gameplay#QuestDef", + "@jgengine/core/gameplay#QuestInstance", + "@jgengine/core/gameplay#QuestRewards", + "@jgengine/core/gameplay#RarityStyle", + "@jgengine/core/gameplay#RecipeDef", + "@jgengine/core/gameplay#RecipeItem", + "@jgengine/core/gameplay#Ring", + "@jgengine/core/gameplay#RingConfig", + "@jgengine/core/gameplay#RingPhase", + "@jgengine/core/gameplay#RoleSpec", + "@jgengine/core/gameplay#Rotation", + "@jgengine/core/gameplay#RoundConfig", + "@jgengine/core/gameplay#RoundSnapshot", + "@jgengine/core/gameplay#RunDraft", + "@jgengine/core/gameplay#RunModifierOffer", + "@jgengine/core/gameplay#ScheduledDelivery", + "@jgengine/core/gameplay#ShapeTable", + "@jgengine/core/gameplay#SlotGrid", + "@jgengine/core/gameplay#Social", + "@jgengine/core/gameplay#SocialDeps", + "@jgengine/core/gameplay#StatLevelUpEvent", + "@jgengine/core/gameplay#TalentNodeDef", + "@jgengine/core/gameplay#TalentTree", + "@jgengine/core/gameplay#TechNodeDef", + "@jgengine/core/gameplay#TouchButton", + "@jgengine/core/gameplay#TouchJoystick", + "@jgengine/core/gameplay#TouchScheme", + "@jgengine/core/gameplay#TurnLoop", + "@jgengine/core/gameplay#UnlockDef", + "@jgengine/core/gameplay#WorldInvite", + "@jgengine/core/gameplay#WorldInviteTarget", + "@jgengine/core/gameplay#WorldItemRecord", + "@jgengine/core/gameplay#WorldItemRenderConfig", + "@jgengine/core/gameplay#advanceTransport", + "@jgengine/core/gameplay#balance", + "@jgengine/core/gameplay#canCraft", + "@jgengine/core/gameplay#chargeAll", + "@jgengine/core/gameplay#clearBindingOverride", + "@jgengine/core/gameplay#computeEffectiveStats", + "@jgengine/core/gameplay#craft", + "@jgengine/core/gameplay#craftSeconds", + "@jgengine/core/gameplay#createAffixRoller", + "@jgengine/core/gameplay#createBehaviourWorld", + "@jgengine/core/gameplay#createCardPile", + "@jgengine/core/gameplay#createCardPileState", + "@jgengine/core/gameplay#createChatRateLimiter", + "@jgengine/core/gameplay#createCommitController", + "@jgengine/core/gameplay#createDeliveryQueue", + "@jgengine/core/gameplay#createDurability", + "@jgengine/core/gameplay#createDurabilityTracker", + "@jgengine/core/gameplay#createGestureSurfaceTracker", + "@jgengine/core/gameplay#createIntentBoard", + "@jgengine/core/gameplay#createModularItem", + "@jgengine/core/gameplay#createRecipeGraph", + "@jgengine/core/gameplay#createRing", + "@jgengine/core/gameplay#createTalentTree", + "@jgengine/core/gameplay#createTouchGestureTracker", + "@jgengine/core/gameplay#createTurnLoop", + "@jgengine/core/gameplay#createUnlocks", + "@jgengine/core/gameplay#curve", + "@jgengine/core/gameplay#drainOutput", + "@jgengine/core/gameplay#draw", + "@jgengine/core/gameplay#durabilityFraction", + "@jgengine/core/gameplay#evalCurve", + "@jgengine/core/gameplay#feedProduction", + "@jgengine/core/gameplay#grant", + "@jgengine/core/gameplay#install", + "@jgengine/core/gameplay#insureLost", + "@jgengine/core/gameplay#isComplete", + "@jgengine/core/gameplay#isDisabled", + "@jgengine/core/gameplay#leveling", + "@jgengine/core/gameplay#loadBindingOverrides", + "@jgengine/core/gameplay#missingRequiredSlots", + "@jgengine/core/gameplay#moveCards", + "@jgengine/core/gameplay#partInSlot", + "@jgengine/core/gameplay#partitionOnDeath", + "@jgengine/core/gameplay#peek", + "@jgengine/core/gameplay#pileRng", + "@jgengine/core/gameplay#playControlsActive", + "@jgengine/core/gameplay#productionBuilding", + "@jgengine/core/gameplay#repairQuote", + "@jgengine/core/gameplay#resolveConsolation", + "@jgengine/core/gameplay#resolvePowerGrid", + "@jgengine/core/gameplay#ringSampleAt", + "@jgengine/core/gameplay#runPipeline", + "@jgengine/core/gameplay#saveBindingOverride", + "@jgengine/core/gameplay#shuffleWithRng", + "@jgengine/core/gameplay#stationSatisfied", + "@jgengine/core/gameplay#tickProduction", + "@jgengine/core/gameplay#touchCode", + "@jgengine/core/gameplay#uninstall", + "@jgengine/core/gameplay#wear", + "@jgengine/core/gameplay#worldHealthBarAllowsRole", "@jgengine/core/input/actionBindings#ActionBindingConfig", "@jgengine/core/input/actionBindings#ActionBindingMap", "@jgengine/core/input/actionBindings#ActionBindingModes", @@ -915,10 +1111,44 @@ "@jgengine/core/movement/poseState#PoseAllowedStates", "@jgengine/core/movement/poseState#PoseHitbox", "@jgengine/core/movement/poseState#PoseRejection", + "@jgengine/core/movement/poseState#PoseSnapshot", "@jgengine/core/movement/poseState#PoseState", "@jgengine/core/movement/voxelController#DEFAULT_VOXEL_DIMS", "@jgengine/core/movement/voxelController#SolidQuery", "@jgengine/core/movement/voxelController#createVoxelPlayerBody", + "@jgengine/core/multiplayer#AuthSession", + "@jgengine/core/multiplayer#BoardSnapshot", + "@jgengine/core/multiplayer#ChatActions", + "@jgengine/core/multiplayer#ChatSendOutcome", + "@jgengine/core/multiplayer#EnsurePresenceResult", + "@jgengine/core/multiplayer#FeedWriteGate", + "@jgengine/core/multiplayer#MatchFilter", + "@jgengine/core/multiplayer#PlayerIdentity", + "@jgengine/core/multiplayer#PlayerPose", + "@jgengine/core/multiplayer#PoseSyncRules", + "@jgengine/core/multiplayer#PoseSyncTuning", + "@jgengine/core/multiplayer#PresenceActions", + "@jgengine/core/multiplayer#PresenceFeeds", + "@jgengine/core/multiplayer#PresencePoseState", + "@jgengine/core/multiplayer#PresenceSession", + "@jgengine/core/multiplayer#PushToTalkMode", + "@jgengine/core/multiplayer#PushToTalkStatus", + "@jgengine/core/multiplayer#SessionListing", + "@jgengine/core/multiplayer#SessionVisibility", + "@jgengine/core/multiplayer#Vec3", + "@jgengine/core/multiplayer#VoiceParticipant", + "@jgengine/core/multiplayer#VoiceRoute", + "@jgengine/core/multiplayer#browseSessions", + "@jgengine/core/multiplayer#createFeedWriteGate", + "@jgengine/core/multiplayer#createLocalVoiceTransport", + "@jgengine/core/multiplayer#createPoseSyncGate", + "@jgengine/core/multiplayer#createPushToTalk", + "@jgengine/core/multiplayer#findByJoinCode", + "@jgengine/core/multiplayer#normalizeJoinCode", + "@jgengine/core/multiplayer#quickMatch", + "@jgengine/core/multiplayer#resolveGuestSession", + "@jgengine/core/multiplayer#sessionPlayer", + "@jgengine/core/multiplayer#validateFeedWrite", "@jgengine/core/multiplayer/chatContract#ChatActions", "@jgengine/core/multiplayer/chatContract#ChatSendArgs", "@jgengine/core/multiplayer/chatContract#ChatSendOutcome", @@ -1077,6 +1307,10 @@ "@jgengine/core/physics/vehicleBody#WheelSpec", "@jgengine/core/physics/vehicleBody#WheelState", "@jgengine/core/physics/vehicleBody#createVehicleBody", + "@jgengine/core/procedural#DecayMeterSet", + "@jgengine/core/procedural#Moodle", + "@jgengine/core/procedural#MoodleStack", + "@jgengine/core/procedural#MultiRegionHealth", "@jgengine/core/puzzle/cellGrid#CellGrid", "@jgengine/core/puzzle/cellGrid#CellRun", "@jgengine/core/puzzle/fallingPiece#FallingPiece", @@ -1092,7 +1326,6 @@ "@jgengine/core/runtime/adapter#MultiplayerTopology", "@jgengine/core/runtime/adapter#ServersPoolConfig", "@jgengine/core/runtime/adapter#adapterOf", - "@jgengine/core/runtime/adapter#convex", "@jgengine/core/runtime/adapter#fly", "@jgengine/core/runtime/adapter#lan", "@jgengine/core/runtime/adapter#multiplayerAdapterKind", @@ -1100,7 +1333,6 @@ "@jgengine/core/runtime/adapter#p2p", "@jgengine/core/runtime/adapter#servers", "@jgengine/core/runtime/adapter#socketIo", - "@jgengine/core/runtime/adapter#ws", "@jgengine/core/runtime/cameraDirector#CameraDirector", "@jgengine/core/runtime/commandRunner#CommandDef", "@jgengine/core/runtime/commandRunner#CommandValidationError", @@ -1136,6 +1368,8 @@ "@jgengine/core/runtime/gameRuntime#RuntimeLoopContext", "@jgengine/core/runtime/gameRuntime#RuntimeWorldContext", "@jgengine/core/runtime/gameRuntime#ServerLoopHooks", + "@jgengine/core/runtime/headlessRunner#HeadlessRunnerOptions", + "@jgengine/core/runtime/headlessRunner#createHeadlessRunner", "@jgengine/core/runtime/hostPersistence#FEED_RING_LIMIT", "@jgengine/core/runtime/hostPersistence#GameServerRecord", "@jgengine/core/runtime/hostPersistence#GameServerStatus", @@ -1280,6 +1514,7 @@ "@jgengine/core/scene/possession#PossessionDeps", "@jgengine/core/scene/possession#PossessionEntities", "@jgengine/core/scene/possession#PossessionEvents", + "@jgengine/core/scene/possession#PossessionSnapshot", "@jgengine/core/scene/possession#PossessionSwappedEvent", "@jgengine/core/scene/possession#createPossession", "@jgengine/core/scene/roster#Roster", @@ -1522,6 +1757,27 @@ "@jgengine/core/turn/turnLoop#TurnLoopSnapshot", "@jgengine/core/turn/turnLoop#TurnState", "@jgengine/core/turn/turnLoop#createTurnLoop", + "@jgengine/core/ui#BUILT_IN_SETTING_CATEGORIES", + "@jgengine/core/ui#DEFAULT_GRAPHICS_QUALITY", + "@jgengine/core/ui#DEFAULT_GRAPHICS_SHADOWS", + "@jgengine/core/ui#DEFAULT_MASTER_VOLUME", + "@jgengine/core/ui#GRAPHICS_QUALITY_OPTIONS", + "@jgengine/core/ui#GameSettingsConfig", + "@jgengine/core/ui#GraphicsQuality", + "@jgengine/core/ui#HUD_ANCHOR_FRACTIONS", + "@jgengine/core/ui#HudAnchor", + "@jgengine/core/ui#HudLayoutStore", + "@jgengine/core/ui#HudPlacement", + "@jgengine/core/ui#HudSize", + "@jgengine/core/ui#SETTING_IDS", + "@jgengine/core/ui#SettingKind", + "@jgengine/core/ui#SettingOption", + "@jgengine/core/ui#SettingValue", + "@jgengine/core/ui#SettingsStore", + "@jgengine/core/ui#UI_SCALE_MAX", + "@jgengine/core/ui#UI_SCALE_MIN", + "@jgengine/core/ui#busVolumeSettingId", + "@jgengine/core/ui#resolveHudFit", "@jgengine/core/ui/hudLayout#HUD_ANCHOR_FRACTIONS", "@jgengine/core/ui/hudLayout#HudAnchor", "@jgengine/core/ui/hudLayout#HudLayoutOptions", @@ -1571,6 +1827,218 @@ "@jgengine/core/visibility/visibilitySystem#VisibilitySystem", "@jgengine/core/visibility/visibilitySystem#VisibilitySystemOptions", "@jgengine/core/visibility/visibilitySystem#createVisibilitySystem", + "@jgengine/core/world#Aabb", + "@jgengine/core/world#AddBodyOptions", + "@jgengine/core/world#Aim", + "@jgengine/core/world#AssetCatalog", + "@jgengine/core/world#AudioBusDef", + "@jgengine/core/world#AudioFalloffConfig", + "@jgengine/core/world#AutoTargetPolicy", + "@jgengine/core/world#BallisticSweep", + "@jgengine/core/world#BallisticSweepHit", + "@jgengine/core/world#BehaviorDescriptor", + "@jgengine/core/world#BuildRole", + "@jgengine/core/world#BuildingEnvironmentDescriptor", + "@jgengine/core/world#BuildingIndex", + "@jgengine/core/world#BuildingPaletteOverrides", + "@jgengine/core/world#BuildingStyle", + "@jgengine/core/world#CameraView", + "@jgengine/core/world#Cardinal", + "@jgengine/core/world#ClockSnapshot", + "@jgengine/core/world#CollapseEvent", + "@jgengine/core/world#ColliderPurpose", + "@jgengine/core/world#ConcealmentSensor", + "@jgengine/core/world#ContextMenu", + "@jgengine/core/world#DEFAULT_GRIP_CURVE", + "@jgengine/core/world#DEFAULT_MARKER_KINDS", + "@jgengine/core/world#DEFAULT_REPUTATION_TIERS", + "@jgengine/core/world#EditableTerrain", + "@jgengine/core/world#EnclosedFootprint", + "@jgengine/core/world#EntityColliderSet", + "@jgengine/core/world#EntityPosition", + "@jgengine/core/world#EnvironmentField", + "@jgengine/core/world#EnvironmentWorldFeature", + "@jgengine/core/world#FactionDef", + "@jgengine/core/world#FireGrid", + "@jgengine/core/world#FogCells", + "@jgengine/core/world#FramingConfig", + "@jgengine/core/world#FreezeMonitor", + "@jgengine/core/world#FreezeViolation", + "@jgengine/core/world#Frustum", + "@jgengine/core/world#FrustumProjection", + "@jgengine/core/world#FrustumSample", + "@jgengine/core/world#FrustumSensor", + "@jgengine/core/world#FrustumTarget", + "@jgengine/core/world#GrassEnvironmentDescriptor", + "@jgengine/core/world#GripCurve", + "@jgengine/core/world#HiddenStateSource", + "@jgengine/core/world#Job", + "@jgengine/core/world#JobDef", + "@jgengine/core/world#JobReport", + "@jgengine/core/world#KinematicVehicleStep", + "@jgengine/core/world#KinematicVehicleTuning", + "@jgengine/core/world#MapCellStates", + "@jgengine/core/world#MapMarker", + "@jgengine/core/world#MapRoute", + "@jgengine/core/world#MapZone", + "@jgengine/core/world#MarkerSet", + "@jgengine/core/world#MinimapView", + "@jgengine/core/world#ModelAssetRef", + "@jgengine/core/world#MovementPose", + "@jgengine/core/world#NavGrid", + "@jgengine/core/world#NavPoint", + "@jgengine/core/world#ObjectVisual", + "@jgengine/core/world#OceanEnvironmentDescriptor", + "@jgengine/core/world#POSE_HITBOX", + "@jgengine/core/world#PadEnvironmentDescriptor", + "@jgengine/core/world#PadSize", + "@jgengine/core/world#PaintStroke", + "@jgengine/core/world#PathFollowConfig", + "@jgengine/core/world#PathFollowState", + "@jgengine/core/world#PhysicsStats", + "@jgengine/core/world#PhysicsWorld", + "@jgengine/core/world#PlacedStructure", + "@jgengine/core/world#PlacementCommit", + "@jgengine/core/world#PlacementController", + "@jgengine/core/world#PlacementPreview", + "@jgengine/core/world#PlacementRules", + "@jgengine/core/world#PositionedPrompt", + "@jgengine/core/world#ProximityPrompt", + "@jgengine/core/world#QteStep", + "@jgengine/core/world#RainEnvironmentDescriptor", + "@jgengine/core/world#RecordingBuffer", + "@jgengine/core/world#RecordingBufferOptions", + "@jgengine/core/world#RegionField", + "@jgengine/core/world#ResolvedCollider", + "@jgengine/core/world#ResolvedWeather", + "@jgengine/core/world#RevealHit", + "@jgengine/core/world#RevealQuery", + "@jgengine/core/world#RoofPlan", + "@jgengine/core/world#RosterEntry", + "@jgengine/core/world#SHAPE_BOX", + "@jgengine/core/world#SHAPE_SPHERE", + "@jgengine/core/world#ScatterInstance", + "@jgengine/core/world#ScatterPoint", + "@jgengine/core/world#SceneEntity", + "@jgengine/core/world#SceneObject", + "@jgengine/core/world#SceneRaycastApi", + "@jgengine/core/world#SceneRaycastHit", + "@jgengine/core/world#ScreenRect", + "@jgengine/core/world#SelectionSet", + "@jgengine/core/world#SensorProbeOptions", + "@jgengine/core/world#SensorReading", + "@jgengine/core/world#SimClock", + "@jgengine/core/world#SkillCheckConfig", + "@jgengine/core/world#SkillCheckResult", + "@jgengine/core/world#SkyEnvironmentDescriptor", + "@jgengine/core/world#SnapMode", + "@jgengine/core/world#SnowEnvironmentDescriptor", + "@jgengine/core/world#SoundDef", + "@jgengine/core/world#SpawnDirectorConfig", + "@jgengine/core/world#SpawnDirectorState", + "@jgengine/core/world#SpawnEntry", + "@jgengine/core/world#SpawnRequest", + "@jgengine/core/world#StatCatalog", + "@jgengine/core/world#StatValue", + "@jgengine/core/world#Station", + "@jgengine/core/world#StructureMaterial", + "@jgengine/core/world#SupportResult", + "@jgengine/core/world#TERRAIN_MATERIAL_PALETTES", + "@jgengine/core/world#TerraformSnapshot", + "@jgengine/core/world#TerrainEnvironmentDescriptor", + "@jgengine/core/world#TerrainFlattenMask", + "@jgengine/core/world#TerrainPalette", + "@jgengine/core/world#ThreatTable", + "@jgengine/core/world#Vec3", + "@jgengine/core/world#VisibilitySystem", + "@jgengine/core/world#VoxelFace", + "@jgengine/core/world#VoxelMaterial", + "@jgengine/core/world#WaterSurface", + "@jgengine/core/world#WaveManifest", + "@jgengine/core/world#Waypoint", + "@jgengine/core/world#WeatherEnvironmentDescriptor", + "@jgengine/core/world#WeatherModifierTable", + "@jgengine/core/world#WeatherState", + "@jgengine/core/world#WindField", + "@jgengine/core/world#WorldGridCell", + "@jgengine/core/world#WorldXZ", + "@jgengine/core/world#advanceSpawnDirector", + "@jgengine/core/world#advanceWave", + "@jgengine/core/world#bearingToCardinal", + "@jgengine/core/world#buildingIndex", + "@jgengine/core/world#carvableTerrain", + "@jgengine/core/world#command", + "@jgengine/core/world#computeFalloffGain", + "@jgengine/core/world#constrainToNavGrid", + "@jgengine/core/world#createAssetCatalog", + "@jgengine/core/world#createBuoyantBody", + "@jgengine/core/world#createContributionPool", + "@jgengine/core/world#createDamageModel", + "@jgengine/core/world#createEditableTerrain", + "@jgengine/core/world#createFactionGraph", + "@jgengine/core/world#createFactionRoster", + "@jgengine/core/world#createFireGrid", + "@jgengine/core/world#createFogField", + "@jgengine/core/world#createKinematicVehicle", + "@jgengine/core/world#createLodScheduler", + "@jgengine/core/world#createMarkerSet", + "@jgengine/core/world#createMountController", + "@jgengine/core/world#createNavGrid", + "@jgengine/core/world#createPathFollow", + "@jgengine/core/world#createPlacedStructureStore", + "@jgengine/core/world#createPlacementController", + "@jgengine/core/world#createPlotPermissions", + "@jgengine/core/world#createRagdoll", + "@jgengine/core/world#createRegionField", + "@jgengine/core/world#createReputationLedger", + "@jgengine/core/world#createSpawnDirectorState", + "@jgengine/core/world#createStationClaim", + "@jgengine/core/world#createTerraformBrush", + "@jgengine/core/world#createThreatTable", + "@jgengine/core/world#createVehicleBody", + "@jgengine/core/world#createVisibilitySystem", + "@jgengine/core/world#createVoxelField", + "@jgengine/core/world#distance", + "@jgengine/core/world#distance3", + "@jgengine/core/world#effectiveRelation", + "@jgengine/core/world#evaluateSkillCheck", + "@jgengine/core/world#gauge", + "@jgengine/core/world#getCurrentGameTimestamp", + "@jgengine/core/world#isRegionField", + "@jgengine/core/world#keybind", + "@jgengine/core/world#label", + "@jgengine/core/world#mapLayerColor", + "@jgengine/core/world#markerKindStyle", + "@jgengine/core/world#objectVisualScale", + "@jgengine/core/world#pad", + "@jgengine/core/world#pathFromNav", + "@jgengine/core/world#patrol", + "@jgengine/core/world#pendingQteStep", + "@jgengine/core/world#player", + "@jgengine/core/world#proximityPrompt", + "@jgengine/core/world#quarterTurnsToRotationY", + "@jgengine/core/world#raiseAlert", + "@jgengine/core/world#resolveEmitterGain", + "@jgengine/core/world#resolveGridInstances", + "@jgengine/core/world#resolveStructureBuildings", + "@jgengine/core/world#resolveWeather", + "@jgengine/core/world#sanitizeGameTimeScale", + "@jgengine/core/world#scatter", + "@jgengine/core/world#selectAutoTarget", + "@jgengine/core/world#sky", + "@jgengine/core/world#snapToNearest", + "@jgengine/core/world#socketWorldPosition", + "@jgengine/core/world#socketsCompatible", + "@jgengine/core/world#solveSupport", + "@jgengine/core/world#summarizeEnvironment", + "@jgengine/core/world#talkable", + "@jgengine/core/world#toDebrisBodies", + "@jgengine/core/world#validatePlacement", + "@jgengine/core/world#wander", + "@jgengine/core/world#waterSurface", + "@jgengine/core/world#waterSurfaceFromDescriptor", + "@jgengine/core/world#windField", + "@jgengine/core/world#worldSockets", "@jgengine/core/world/buildPermissions#BuildActor", "@jgengine/core/world/buildPermissions#BuildRole", "@jgengine/core/world/buildPermissions#ContributionGoal", @@ -1855,6 +2323,11 @@ "@jgengine/core/world/windZones#WindZoneState", "@jgengine/core/world/windZones#WindZonesConfig", "@jgengine/core/world/windZones#createWindZones", + "@jgengine/editor/chromeFields#NumberField", + "@jgengine/editor/chromeFields#SliderRow", + "@jgengine/editor/chromeStyles#BTN", + "@jgengine/editor/chromeStyles#INPUT", + "@jgengine/editor/chromeStyles#MICRO", "@jgengine/node#GameSocketIoServer", "@jgengine/node#GameSocketIoServerOptions", "@jgengine/node#GameWsServer", @@ -2141,6 +2614,7 @@ "@jgengine/react/voice#VoiceState", "@jgengine/shell/GameHost#GameHost", "@jgengine/shell/GameHost#GameHostProps", + "@jgengine/shell/GamePhaseStamp#GamePhaseStamp", "@jgengine/shell/GamePlayer#GamePlayer", "@jgengine/shell/GamePlayer#GamePlayerProps", "@jgengine/shell/GamePlayerShell#GamePlayerShell", @@ -2259,6 +2733,12 @@ "@jgengine/shell/cartridge/validate#validateCartridge", "@jgengine/shell/defineGame#GameConfig", "@jgengine/shell/defineGame#defineGame", + "@jgengine/shell/devtools/ColPanel#ColPanel", + "@jgengine/shell/devtools/KeysPanel#KeysPanel", + "@jgengine/shell/devtools/LogsPanel#LogsPanel", + "@jgengine/shell/devtools/NetPanel#NetPanel", + "@jgengine/shell/devtools/PerfPanel#PerfPanel", + "@jgengine/shell/devtools/TunePanel#TunePanel", "@jgengine/shell/devtools/collisionDebug#AimProbeConfig", "@jgengine/shell/devtools/collisionDebug#COLLISION_DEBUG_LAYERS", "@jgengine/shell/devtools/collisionDebug#CollisionDebugController", @@ -2280,15 +2760,36 @@ "@jgengine/shell/devtools/collisionDebugMath#DebugShapeEntry", "@jgengine/shell/devtools/collisionDebugMath#HITBOX_WIRE_COLOR", "@jgengine/shell/devtools/collisionDebugMath#PROJECTILE_PATH_COLOR", + "@jgengine/shell/devtools/devtoolsOverrides#readStoredOverrides", + "@jgengine/shell/devtools/panelAtoms#SectionLabel", + "@jgengine/shell/devtools/panelAtoms#StatRow", + "@jgengine/shell/devtools/panelAtoms#ms", + "@jgengine/shell/devtools/perfDiagnose#diagnose", + "@jgengine/shell/diagnostics/RuntimeDiagnostics#DiagnosticOverlay", + "@jgengine/shell/diagnostics/RuntimeDiagnostics#GameUiErrorBoundary", + "@jgengine/shell/diagnostics/RuntimeDiagnostics#RuntimeDiagnostic", + "@jgengine/shell/diagnostics/RuntimeDiagnostics#logRuntimeError", + "@jgengine/shell/drivers/FrameDriver#FrameDriver", + "@jgengine/shell/drivers/FrameDriver#POSTER_SETTLE_SECONDS", + "@jgengine/shell/drivers/HudOnlyDriver#HudOnlyDriver", "@jgengine/shell/environment#DaylightCycleConfig", + "@jgengine/shell/environment#DaylightCycleConfig", + "@jgengine/shell/environment#DaylightProps", "@jgengine/shell/environment#DaylightProps", "@jgengine/shell/environment#DaylightState", + "@jgengine/shell/environment#DaylightState", + "@jgengine/shell/environment#EnvironmentScene", "@jgengine/shell/environment#EnvironmentScene", "@jgengine/shell/environment#EnvironmentSceneProps", + "@jgengine/shell/environment#EnvironmentSceneProps", + "@jgengine/shell/environment#SKY_PRESET_DAY_FRACTION", "@jgengine/shell/environment#SKY_PRESET_DAY_FRACTION", "@jgengine/shell/environment#SkyDaylightProps", + "@jgengine/shell/environment#SkyDaylightProps", + "@jgengine/shell/environment#SkyDomeProps", "@jgengine/shell/environment#SkyDomeProps", "@jgengine/shell/environment#TimeOfDayDaylightProps", + "@jgengine/shell/environment#TimeOfDayDaylightProps", "@jgengine/shell/environment/Daylight#DaylightProps", "@jgengine/shell/environment/Daylight#SkyDaylightProps", "@jgengine/shell/environment/Daylight#SkyDomeProps", @@ -2337,6 +2838,11 @@ "@jgengine/shell/registry#RenderEntity", "@jgengine/shell/registry#RenderObject", "@jgengine/shell/registry#resolveGameLoader", + "@jgengine/shell/render/SceneLighting#BackdropFog", + "@jgengine/shell/render/SceneLighting#ConfiguredLighting", + "@jgengine/shell/render/SceneModels#EntityModel", + "@jgengine/shell/render/SceneModels#EntitySprite", + "@jgengine/shell/render/SceneModels#IsolatedEntityModel", "@jgengine/shell/render/modelRender#MaterialCache", "@jgengine/shell/render/modelRender#PAINT_TEXTURE_SIZE", "@jgengine/shell/render/modelRender#PaintCanvas", @@ -2353,13 +2859,22 @@ "@jgengine/shell/settings/settingsController#SettingsControllerInput", "@jgengine/shell/settings/settingsController#useSettingsCategories", "@jgengine/shell/structures#BuildingBlock", + "@jgengine/shell/structures#BuildingBlock", "@jgengine/shell/structures#BuildingBlockProps", + "@jgengine/shell/structures#BuildingBlockProps", + "@jgengine/shell/structures#GeneratedBuilding", "@jgengine/shell/structures#GeneratedBuilding", "@jgengine/shell/structures#GeneratedBuildingProps", + "@jgengine/shell/structures#GeneratedBuildingProps", + "@jgengine/shell/structures#InstancedBuildingPlacement", "@jgengine/shell/structures#InstancedBuildingPlacement", "@jgengine/shell/structures#InstancedBuildings", + "@jgengine/shell/structures#InstancedBuildings", + "@jgengine/shell/structures#InstancedBuildingsProps", "@jgengine/shell/structures#InstancedBuildingsProps", "@jgengine/shell/structures#PlacementGhost", + "@jgengine/shell/structures#PlacementGhost", + "@jgengine/shell/structures#PlacementGhostProps", "@jgengine/shell/structures#PlacementGhostProps", "@jgengine/shell/structures/GeneratedBuilding#BuildingBlock", "@jgengine/shell/structures/GeneratedBuilding#BuildingBlockProps", @@ -2377,30 +2892,55 @@ "@jgengine/shell/structures/PlacementGhost#PlacementGhost", "@jgengine/shell/structures/PlacementGhost#PlacementGhostProps", "@jgengine/shell/terrain#CarvedTerrainProps", + "@jgengine/shell/terrain#CarvedTerrainProps", + "@jgengine/shell/terrain#DEFAULT_GRASS_WIND", "@jgengine/shell/terrain#DEFAULT_GRASS_WIND", "@jgengine/shell/terrain#EditableGround", + "@jgengine/shell/terrain#EditableGround", + "@jgengine/shell/terrain#EditableGroundProps", "@jgengine/shell/terrain#EditableGroundProps", "@jgengine/shell/terrain#FieldGroundOptions", + "@jgengine/shell/terrain#FieldGroundOptions", + "@jgengine/shell/terrain#GrassBladeGeometryOptions", "@jgengine/shell/terrain#GrassBladeGeometryOptions", "@jgengine/shell/terrain#GrassField", + "@jgengine/shell/terrain#GrassField", + "@jgengine/shell/terrain#GrassFieldProps", "@jgengine/shell/terrain#GrassFieldProps", "@jgengine/shell/terrain#GrassMaterialHandle", + "@jgengine/shell/terrain#GrassMaterialHandle", + "@jgengine/shell/terrain#GrassMaterialOptions", "@jgengine/shell/terrain#GrassMaterialOptions", "@jgengine/shell/terrain#GrassRange", + "@jgengine/shell/terrain#GrassRange", + "@jgengine/shell/terrain#GrassShaderUniforms", "@jgengine/shell/terrain#GrassShaderUniforms", "@jgengine/shell/terrain#GrassWindOptions", + "@jgengine/shell/terrain#GrassWindOptions", + "@jgengine/shell/terrain#ProceduralGround", "@jgengine/shell/terrain#ProceduralGround", "@jgengine/shell/terrain#ProceduralGroundProps", + "@jgengine/shell/terrain#ProceduralGroundProps", + "@jgengine/shell/terrain#ProceduralTerrainConfig", "@jgengine/shell/terrain#ProceduralTerrainConfig", "@jgengine/shell/terrain#ResolvedGrassBladeGeometryOptions", + "@jgengine/shell/terrain#ResolvedGrassBladeGeometryOptions", + "@jgengine/shell/terrain#ResolvedTerrainSegments", "@jgengine/shell/terrain#ResolvedTerrainSegments", "@jgengine/shell/terrain#ResolvedTerrainSize", + "@jgengine/shell/terrain#ResolvedTerrainSize", + "@jgengine/shell/terrain#TerraformBrushCursor", "@jgengine/shell/terrain#TerraformBrushCursor", "@jgengine/shell/terrain#TerraformBrushCursorProps", + "@jgengine/shell/terrain#TerraformBrushCursorProps", + "@jgengine/shell/terrain#TerrainArea", "@jgengine/shell/terrain#TerrainArea", "@jgengine/shell/terrain#TerrainHeightSampler", + "@jgengine/shell/terrain#TerrainHeightSampler", + "@jgengine/shell/terrain#TerrainSeed", "@jgengine/shell/terrain#TerrainSeed", "@jgengine/shell/terrain#TerrainVertexColorOptions", + "@jgengine/shell/terrain#TerrainVertexColorOptions", "@jgengine/shell/terrain/CarvedTerrain#CarvedTerrainProps", "@jgengine/shell/terrain/EditableGround#EditableGround", "@jgengine/shell/terrain/EditableGround#EditableGroundProps", @@ -2441,25 +2981,45 @@ "@jgengine/shell/vision/RevealVision#RevealVisionOptions", "@jgengine/shell/vision/frustumSampleEqual#frustumSampleDisplayEqual", "@jgengine/shell/water#DEFAULT_OCEAN_CONFIG", + "@jgengine/shell/water#DEFAULT_OCEAN_CONFIG", "@jgengine/shell/water#MAX_OCEAN_WAVES", + "@jgengine/shell/water#MAX_OCEAN_WAVES", + "@jgengine/shell/water#OCEAN_QUALITY_PRESETS", "@jgengine/shell/water#OCEAN_QUALITY_PRESETS", "@jgengine/shell/water#Ocean", + "@jgengine/shell/water#Ocean", + "@jgengine/shell/water#OceanColorConfig", "@jgengine/shell/water#OceanColorConfig", "@jgengine/shell/water#OceanConfig", + "@jgengine/shell/water#OceanConfig", + "@jgengine/shell/water#OceanDirectionVector", "@jgengine/shell/water#OceanDirectionVector", "@jgengine/shell/water#OceanFoamConfig", + "@jgengine/shell/water#OceanFoamConfig", + "@jgengine/shell/water#OceanMaterialUniforms", "@jgengine/shell/water#OceanMaterialUniforms", "@jgengine/shell/water#OceanProps", + "@jgengine/shell/water#OceanProps", + "@jgengine/shell/water#OceanQualityPreset", "@jgengine/shell/water#OceanQualityPreset", "@jgengine/shell/water#OceanShaderMaterial", + "@jgengine/shell/water#OceanShaderMaterial", + "@jgengine/shell/water#OceanWaveConfig", "@jgengine/shell/water#OceanWaveConfig", "@jgengine/shell/water#OceanWaveDirection", + "@jgengine/shell/water#OceanWaveDirection", + "@jgengine/shell/water#ResolvedOceanColorConfig", "@jgengine/shell/water#ResolvedOceanColorConfig", "@jgengine/shell/water#ResolvedOceanConfig", + "@jgengine/shell/water#ResolvedOceanConfig", + "@jgengine/shell/water#ResolvedOceanFoamConfig", "@jgengine/shell/water#ResolvedOceanFoamConfig", "@jgengine/shell/water#ResolvedOceanWaveConfig", + "@jgengine/shell/water#ResolvedOceanWaveConfig", + "@jgengine/shell/water#createOceanMaterial", "@jgengine/shell/water#createOceanMaterial", "@jgengine/shell/water#syncOceanMaterial", + "@jgengine/shell/water#syncOceanMaterial", "@jgengine/shell/water/Ocean#Ocean", "@jgengine/shell/water/Ocean#OceanProps", "@jgengine/shell/water/OceanConfig#DEFAULT_OCEAN_CONFIG", @@ -2528,6 +3088,8 @@ "@jgengine/shell/world/WorldHud#WorldEntityBars", "@jgengine/shell/world/WorldHud#WorldFloatText", "@jgengine/shell/world/WorldHud#WorldTelegraphs", + "@jgengine/shell/world/WorldScene#RemotePlayers", + "@jgengine/shell/world/WorldScene#WorldView", "@jgengine/shell/world/entityPose#PoseSource", "@jgengine/shell/world/entityPose#PoseWritable", "@jgengine/shell/world/floatTextStyle#FloatTextInfo", diff --git a/scripts/export-manifest.json b/scripts/export-manifest.json index 1ddba955c..d662519ac 100644 --- a/scripts/export-manifest.json +++ b/scripts/export-manifest.json @@ -194,6 +194,11 @@ "./runtime/adapter", "./runtime/cameraDirector", "./runtime/commandRunner", + "./runtime/context/combatFx", + "./runtime/context/registries", + "./runtime/context/worldItems", + "./runtime/descriptors/baseline", + "./runtime/descriptors/features", "./runtime/gameContext", "./runtime/gameRuntime", "./runtime/headlessRunner", diff --git a/scripts/skill-size-baseline.json b/scripts/skill-size-baseline.json index 61015f256..4554f9cc7 100644 --- a/scripts/skill-size-baseline.json +++ b/scripts/skill-size-baseline.json @@ -6,14 +6,14 @@ "harvest": 32, "harvest-full-game": 32, "harvest-game": 28, - "jgengine": 827, + "jgengine": 841, "jgengine-assets": 68, - "jgengine-combat": 48, - "jgengine-editor": 379, - "jgengine-gameplay": 463, - "jgengine-multiplayer": 94, - "jgengine-procedural": 14, - "jgengine-ui": 136, + "jgengine-combat": 50, + "jgengine-editor": 388, + "jgengine-gameplay": 468, + "jgengine-multiplayer": 112, + "jgengine-procedural": 16, + "jgengine-ui": 138, "jgengine-verify": 140, - "jgengine-world": 169 + "jgengine-world": 171 }