diff --git a/.claude/skills/jgengine-editor/api.md b/.claude/skills/jgengine-editor/api.md index a363a2712..44bd27da3 100644 --- a/.claude/skills/jgengine-editor/api.md +++ b/.claude/skills/jgengine-editor/api.md @@ -8,6 +8,10 @@ - `DocumentLiveEvent` (interface): interface DocumentLiveEvent — Event emitted when the authoritative document changes on a {@link DocumentLiveSync}. - `DocumentLiveSync` (interface): interface DocumentLiveSync — Two-way live-sync bus: document patches out, runtime state deltas back. - `DocumentPatch` (type): type DocumentPatch = | { type: "snapshot"; revision?: number; baseRevision: number; document: EditorDocument; } | { type: "commands"; revision?: number; baseRevision: number; commands: readonly EditorCommand[]; } — One versioned document mutation on the live-sync stream. `snapshot` replaces the whole document; `commands` replays structural editor commands onto the current document. `baseRevision` must match the receiver's current revision unless `force` is set (document authority from the editor). +- `EditorCatalogData` (interface): interface EditorCatalogData — Persisted values for one gameplay data catalog (weapons, waves, economy, …). Schemas are not stored here — they come from the game's `editorCatalogs` export and drive SchemaInspector. +- `EditorCatalogDefinition` (interface): interface EditorCatalogDefinition — Game-exported catalog definition: a `ParamSchema` plus default entries. Schemas stay in code; entry values merge into `document.catalogs` and are what the editor/RPC edits and saves. +- `EditorCatalogEntry` (interface): interface EditorCatalogEntry — One row in a gameplay data catalog — id + optional label + a meta bag matching the catalog's `ParamSchema`. Values persist on the scene document; the schema lives in the game export. +- `EditorCatalogsInput` (type): type EditorCatalogsInput = | readonly EditorCatalogDefinition[] | (() => readonly EditorCatalogDefinition[]) — Accepted shape for a game's `editorCatalogs` export: definitions, or a factory. - `EditorCollection` (interface): interface EditorCollection — A named, persisted list of object ids — a selection bookmark (restore, add-to) that can also double as a production group: `locked` blocks `translate`/`setTransform`/`remove`/`removeMany` on its members, `color`/`visible` are UI-only hints for the collections panel. - `EditorCommand` (type): type EditorCommand = | { type: "select"; ids: readonly string[] } | { type: "clearSelection" } | { type: "setTransform"; id: string; position?: EditorVec3; rotationY?: number } | { type: "translate"; ids: readonly string[]; delta: EditorVec3 } | { type: "setParent"; ids: readonly string[]; parentId:… — A single editor mutation — select, move, add, remove, undo/redo — dispatched to a session. - `EditorDispatchOptions` (interface): interface EditorDispatchOptions — Per-dispatch options; `coalesce` merges consecutive same-key edits into one undo step. @@ -29,6 +33,13 @@ - `EditorVolumeShape` (type): type EditorVolumeShape = "sphere" | "cylinder" | "box" — Collision shape a volume is rendered and tested as. - `HudPanelTypeDef` (interface): interface HudPanelTypeDef — Declared panel type: growable axes, size limits, optional ParamSchema for the editor. - `HudResizeAxes` (type): type HudResizeAxes = "none" | "x" | "y" | "both" — Which axes a panel type may grow when resized in canvas mode. Resize is semantic — content reflows (longer track, more rows) — never a CSS scale of the whole panel. +- `RuntimeEntityState` (interface): interface RuntimeEntityState — One live entity row the runtime may stream to the editor (play-mode inspector feed). +- `RuntimeInspectorGetResult` (interface): interface RuntimeInspectorGetResult — One resolved runtime field returned by `runtime_get`. +- `RuntimeInspectorSetPlan` (interface): interface RuntimeInspectorSetPlan — Desired mutation from `runtime_set` before the host applies it. +- `RuntimeInspectorSummary` (interface): interface RuntimeInspectorSummary — Compact reverse-channel view for the play-mode inspector and `runtime_summary` RPC. +- `RuntimePlayControl` (interface): interface RuntimePlayControl — Play-mode sim gate held by the editor host — pause freezes ticks; step runs N frames then re-pauses. +- `RuntimeStateDelta` (interface): interface RuntimeStateDelta — Incremental runtime state for the reverse channel. Entity rows upsert by id; `removeIds` drop rows; `tunables` shallow-merge. Ephemeral unless written back as a document patch. +- `RuntimeStateSnapshot` (interface): interface RuntimeStateSnapshot — Full ephemeral runtime view held on the reverse channel — never mutates the document. - `WELL_KNOWN_MARKER_KINDS` (const): const WELL_KNOWN_MARKER_KINDS: readonly ["player_spawn", "mob", "boss", "vendor", "chest", "travel", "npc", "poi", "prop", "goal", "branch"] — Standard marker kinds recognized with default colors and behavior. - `WELL_KNOWN_PATH_KINDS` (const): const WELL_KNOWN_PATH_KINDS: readonly ["road", "corridor", "branch", "route"] — Standard path kinds recognized with default colors and behavior. - `WELL_KNOWN_VOLUME_KINDS` (const): const WELL_KNOWN_VOLUME_KINDS: readonly ["zone", "flatten", "cluster", "aggro", "leash", "discover", "capture", "prompt", "poi", "respawn_skip"] — Standard volume kinds recognized with default colors and behavior. @@ -37,12 +48,15 @@ - `consumeRuntimePlayStep` (function): function consumeRuntimePlayStep(play: RuntimePlayControl): { runFrame: boolean; next: RuntimePlayControl; } — Advances the play-control step counter: when paused with pending steps, consumes one and reports whether the sim should run this frame. When not paused, always runs. - `createDocumentLiveSync` (function): function createDocumentLiveSync(initial: EditorDocument): DocumentLiveSync — Creates an in-memory two-way live-sync bus seeded from an initial document. Document is authoritative; runtime overrides are ephemeral until {@link DocumentLiveSync.writeBackOverride}. - `createRuntimePlayControl` (function): function createRuntimePlayControl(paused = false): RuntimePlayControl — Default play-control state when entering play mode (running). +- `findEditorCatalog` (function): function findEditorCatalog(doc: EditorDocument, id: string): EditorCatalogData | undefined — Looks up a gameplay data catalog by id on the scene document. +- `findEditorCatalogEntry` (function): function findEditorCatalogEntry(doc: EditorDocument, catalogId: string, entryId: string): EditorCatalogEntry | undefined — Looks up one entry inside a gameplay data catalog. - `getDocumentLiveSync` (function): function getDocumentLiveSync(): DocumentLiveSync | null — Returns the globally installed live-sync bus, or null when none is mounted. - `getRuntimeInspectorValue` (function): function getRuntimeInspectorValue(snapshot: RuntimeStateSnapshot, overrides: Readonly>, id: string, path?: string): RuntimeInspectorGetResult — Resolves one entity, entity field, or tunable from the reverse-channel snapshot (with overrides layered on top for entity rows). - `installDocumentLiveSync` (function): function installDocumentLiveSync(sync: DocumentLiveSync): () => void — Publishes a live-sync bus globally so AuthoredScene / games can subscribe without prop drilling. - `planRuntimeInspectorSet` (function): function planRuntimeInspectorSet(document: EditorDocument, input: { id: string; path?: string; value?: unknown; position?: { x: number; y: number; z: number }; rotationY?: number; values?: Record; writeBack?: boolean; }): RuntimeInspectorSetPlan — Plans a play-mode poke: builds the ephemeral entity/tunable override and, when `writeBack` is true, the undoable document commands that promote it into the scene document. - `runtimeEntityMetaWriteBackCommand` (function): function runtimeEntityMetaWriteBackCommand(document: EditorDocument, entity: RuntimeEntityState): EditorCommand | null — Promotes ephemeral runtime `values` into an undoable meta patch on a document-linked object. Returns null when the id is not in the document or there are no values. - `runtimeEntityWriteBackCommand` (function): function runtimeEntityWriteBackCommand(document: EditorDocument, entity: RuntimeEntityState): EditorCommand | null — Builds an undoable document command from an ephemeral runtime entity row (write-back). Returns null when there is nothing to promote. Does not mutate document or clear the override — the caller dispatches the command and then clears the override. +- `seedEditorCatalogs` (function): function seedEditorCatalogs(doc: EditorDocument, definitions: readonly EditorCatalogDefinition[]): EditorDocument — Seeds default catalog rows from game-exported definitions into a document: missing catalogs and missing entries are filled from the definition; existing document values win (overlay already applied). - `subscribeDocumentLiveSyncInstall` (function): function subscribeDocumentLiveSyncInstall(listener: () => void): () => void — Subscribe to install/uninstall of the global live-sync bus (AuthoredScene re-attaches when the editor host mounts over a running game). - `summarizeRuntimeInspector` (function): function summarizeRuntimeInspector(snapshot: RuntimeStateSnapshot, overrides: Readonly>, play: RuntimePlayControl): RuntimeInspectorSummary — Builds the compact reverse-channel summary used by the play-mode inspector panel and the `runtime_summary` bridge RPC. @@ -53,6 +67,12 @@ - `EditorSession` (interface): interface EditorSession — Stateful, undoable handle for driving scene edits from UI or an MCP agent. - `EditorSessionState` (interface): interface EditorSessionState — The document plus current selection at a point in editor history. +## @jgengine/core/editor/document + +- `findEditorCatalog` (function): function findEditorCatalog(doc: EditorDocument, id: string): EditorCatalogData | undefined — Looks up a gameplay data catalog by id on the scene document. +- `findEditorCatalogEntry` (function): function findEditorCatalogEntry(doc: EditorDocument, catalogId: string, entryId: string): EditorCatalogEntry | undefined — Looks up one entry inside a gameplay data catalog. +- `seedEditorCatalogs` (function): function seedEditorCatalogs(doc: EditorDocument, definitions: readonly EditorCatalogDefinition[]): EditorDocument — Seeds default catalog rows from game-exported definitions into a document: missing catalogs and missing entries are filled from the definition; existing document values win (overlay already applied). + ## @jgengine/core/editor/liveSync - `ApplyDocumentPatchResult` (type): type ApplyDocumentPatchResult = | { ok: true; document: EditorDocument; revision: number; patch: DocumentPatch } | { ok: false; error: string } — Result of applying a {@link DocumentPatch} to a document + revision pair. @@ -174,15 +194,13 @@ - `blankWorld` (function): function blankWorld(seed = "standalone"): EnvironmentWorldFeature — The default flat-ground world the standalone editor opens on when the host supplies none. - `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. - `createDefaultAgentEndpoint` (function): function createDefaultAgentEndpoint(config: AgentEndpointConfig = resolveAgentEndpointConfig()): AgentEndpoint — Picks HTTP endpoint when `JGENGINE_EDITOR_AGENT_URL` (or config.url) is set, otherwise the offline local agent. -- `createEditorHost` (function): function createEditorHost(options: { gameId: string; layers: EditorLayersInput | undefined; assets?: readonly EditorAssetInfo[]; onFocus?: (target: { x: number; y: number; z: number } | null) => void; }): { session: EditorSession; api: EditorHostApi; dispose: () => void; } — Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. - `createEditorUiStore` (function): function createEditorUiStore(): EditorUiStore — Creates the shared UI store the editor chrome and viewport both drive. - `createHttpAgentEndpoint` (function): function createHttpAgentEndpoint(config: { url: string; apiKey?: string; fetchImpl?: typeof fetch; }): AgentEndpoint — HTTP POST agent endpoint: `{ messages, context, tools }` → `{ message?, toolCalls? }`. Bearer auth from `apiKey` when provided (`JGENGINE_EDITOR_AGENT_KEY` / `ANTHROPIC_API_KEY`). - `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. -- `getEditorHost` (function): function getEditorHost(): EditorHostApi | null — Retrieves the globally installed editor host, or null if none is mounted. - `installEditorHost` (function): function installEditorHost(api: EditorHostApi): () => void — Publishes an editor host globally so devtools and MCP agents can reach it; returns a cleanup fn. - `newPlacementId` (function): function newPlacementId(prefix: string): string — Generates a fresh scene-object id for a placement tool click. - `packAgentContext` (function): function packAgentContext(api: EditorHostApi): AgentEditorContext — Packs the live host's selection, mode, focus, and document counts for agent prompts. Injected into every embedded-panel turn so the agent shares the human's current view. -- `resolveAgentEndpointConfig` (function): function resolveAgentEndpointConfig(env: Record = typeof process !== "undefined" ? process.env : {}): AgentEndpointConfig — Reads agent endpoint config from an env map. Prefer `JGENGINE_EDITOR_AGENT_URL` + `JGENGINE_EDITOR_AGENT_KEY` (falls back to `ANTHROPIC_API_KEY`). Empty URL → local offline agent; set the URL for a remote model/tool-call backend. +- `resolveAgentEndpointConfig` (function): function resolveAgentEndpointConfig(env: Record = readProcessEnv()): AgentEndpointConfig — Reads agent endpoint config from an env map. Prefer `JGENGINE_EDITOR_AGENT_URL` + `JGENGINE_EDITOR_AGENT_KEY` (falls back to `ANTHROPIC_API_KEY`). Empty URL → local offline agent; set the URL for a remote model/tool-call backend. - `routeToolCall` (function): function routeToolCall(api: EditorHostApi, call: AgentToolCall): AgentToolResult — Routes one agent tool call through the same editor RPC surface humans use. Mutating calls share the session undo stack — no parallel history. Tool name maps 1:1 onto `EditorBridgeRequest.method` (same verbs as MCP/CLI). - `runAgentTurn` (function): function runAgentTurn(options: { api: EditorHostApi; endpoint: AgentEndpoint; history: readonly AgentChatMessage[]; userMessage: string; maxRounds?: number; now?: () => number; }): Promise — Runs one user message against an agent endpoint: injects live editor context via `packAgentContext`, executes tool calls through `routeToolCall` (shared undo), and returns transcript + patch entries the human can reverse with `undoAgentPatch`. - `shallowArrayEqual` (function): function shallowArrayEqual(a: readonly T[], b: readonly T[]): boolean — Shallow array equality — for selectors that return id lists (`selection`) or small tuples. @@ -267,7 +285,7 @@ - `EDITOR_AGENT_URL_ENV` (const): const EDITOR_AGENT_URL_ENV: "JGENGINE_EDITOR_AGENT_URL" — Env var name for the remote agent HTTP URL (`JGENGINE_EDITOR_AGENT_URL`). - `createDefaultAgentEndpoint` (function): function createDefaultAgentEndpoint(config: AgentEndpointConfig = resolveAgentEndpointConfig()): AgentEndpoint — Picks HTTP endpoint when `JGENGINE_EDITOR_AGENT_URL` (or config.url) is set, otherwise the offline local agent. - `createHttpAgentEndpoint` (function): function createHttpAgentEndpoint(config: { url: string; apiKey?: string; fetchImpl?: typeof fetch; }): AgentEndpoint — HTTP POST agent endpoint: `{ messages, context, tools }` → `{ message?, toolCalls? }`. Bearer auth from `apiKey` when provided (`JGENGINE_EDITOR_AGENT_KEY` / `ANTHROPIC_API_KEY`). -- `resolveAgentEndpointConfig` (function): function resolveAgentEndpointConfig(env: Record = typeof process !== "undefined" ? process.env : {}): AgentEndpointConfig — Reads agent endpoint config from an env map. Prefer `JGENGINE_EDITOR_AGENT_URL` + `JGENGINE_EDITOR_AGENT_KEY` (falls back to `ANTHROPIC_API_KEY`). Empty URL → local offline agent; set the URL for a remote model/tool-call backend. +- `resolveAgentEndpointConfig` (function): function resolveAgentEndpointConfig(env: Record = readProcessEnv()): AgentEndpointConfig — Reads agent endpoint config from an env map. Prefer `JGENGINE_EDITOR_AGENT_URL` + `JGENGINE_EDITOR_AGENT_KEY` (falls back to `ANTHROPIC_API_KEY`). Empty URL → local offline agent; set the URL for a remote model/tool-call backend. ## @jgengine/editor/agent/toolBridge @@ -303,6 +321,10 @@ - `startEditorBridgeServerNode` (function): function startEditorBridgeServerNode(options: EditorBridgeServerOptions): EditorBridgeServer — Starts a Node HTTP server exposing the editor host over POST /rpc and GET /health. +## @jgengine/editor/mcp/cli + +- `EditorCliOptions` (type): type EditorCliOptions = { gameId: string; port: number; rpcSource: RpcPayloadSource | null; serve: boolean; stdio: boolean; } — Parsed CLI flags for the headless editor control plane. + ## @jgengine/editor/mcp/loadGameCatalogs - `LoadGameCatalogsResult` (type): type LoadGameCatalogsResult = | { ok: true; catalogs: readonly EditorCatalogDefinition[] } | { ok: false; errors: { path: string; message: string }[] } — Result of {@link loadGameCatalogs}: validated definitions, or diagnostics when the export is malformed. @@ -313,6 +335,11 @@ - `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/rpcPayload + +- `RpcPayloadResult` (type): type RpcPayloadResult = | { ok: true; value: unknown; raw: string; sourceLabel: string } | { ok: false; error: string } — Result of reading or JSON-decoding an RPC payload for the CLI. +- `RpcPayloadSource` (type): type RpcPayloadSource = | { kind: "inline"; raw: string } | { kind: "file"; path: string } | { kind: "stdin" } — Where an RPC JSON body is read from for the headless editor CLI. + ## @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. @@ -354,8 +381,6 @@ - `EditorRunMode` (type): type EditorRunMode = "edit" | "walk" | "play" — How the editor hosts the game: frozen placement view, roamable world, or the real game. - `EditorSession` (interface): interface EditorSession — Stateful, undoable handle for driving scene edits from UI or an MCP agent. - `EditorSessionState` (interface): interface EditorSessionState — The document plus current selection at a point in editor history. -- `createEditorHost` (function): function createEditorHost(options: { gameId: string; layers: EditorLayersInput | undefined; assets?: readonly EditorAssetInfo[]; onFocus?: (target: { x: number; y: number; z: number } | null) => void; }): { session: EditorSession; api: EditorHostApi; dispose: () => void; } — Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. -- `getEditorHost` (function): function getEditorHost(): EditorHostApi | null — Retrieves the globally installed editor host, or null if none is mounted. - `installEditorHost` (function): function installEditorHost(api: EditorHostApi): () => void — Publishes an editor host globally so devtools and MCP agents can reach it; returns a cleanup fn. ## @jgengine/editor/uiStore diff --git a/.claude/skills/jgengine-editor/capabilities.md b/.claude/skills/jgengine-editor/capabilities.md index 43c66ec99..972854fa1 100644 --- a/.claude/skills/jgengine-editor/capabilities.md +++ b/.claude/skills/jgengine-editor/capabilities.md @@ -4,6 +4,15 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*. +## editor-catalogs — Persist gameplay tuning rows on the scene document. + +- `EditorCatalogData` (interface) · `import { EditorCatalogData } from "@jgengine/core/editor"` +- `EditorCatalogDefinition` (interface) · `import { EditorCatalogDefinition } from "@jgengine/core/editor"` +- `EditorCatalogEntry` (interface) · `import { EditorCatalogEntry } from "@jgengine/core/editor"` +- `findEditorCatalog` (function) · `import { findEditorCatalog } from "@jgengine/core/editor"` +- `findEditorCatalogEntry` (function) · `import { findEditorCatalogEntry } from "@jgengine/core/editor"` +- `seedEditorCatalogs` (function) · `import { seedEditorCatalogs } from "@jgengine/core/editor"` + ## editor-live-sync — apply a versioned document patch onto a live scene document - `applyDocumentPatch` (function) · `import { applyDocumentPatch } from "@jgengine/core/editor"` diff --git a/.claude/skills/jgengine-gameplay/SKILL.md b/.claude/skills/jgengine-gameplay/SKILL.md index 5187dc070..99a2b784b 100644 --- a/.claude/skills/jgengine-gameplay/SKILL.md +++ b/.claude/skills/jgengine-gameplay/SKILL.md @@ -5,9 +5,7 @@ description: Gameplay systems: items, quests, economy, crafting, turns, objectiv # jgengine-gameplay -**Import from the curated barrel** `@jgengine/core/gameplay` (stable, re-exports this domain's public API) — deep paths `@jgengine/core//` still work for anything not re-exported. - -**Composable systems** — `defineSystem` / `composeGameLoop` / `compileSystemSchedule` / `DEFAULT_FIXED_STAGES` / `DEFAULT_FRAME_STAGES` / `SystemDefinition` / `SystemTick` / `SystemEventHandlers` / `CompiledSystemSchedule`: list capabilities in `defineGame({ systems })` instead of a manual `onTick` fan-out. Full contract: [reference-systems.md](reference-systems.md). +**Import from the curated barrel** `@jgengine/core/gameplay` (stable, re-exports this domain's public API) — deep paths `@jgengine/core//` still work for anything not re-exported. **Composable systems** — `defineSystem` / `composeGameLoop` / `compileSystemSchedule` / `DEFAULT_FIXED_STAGES` / `DEFAULT_FRAME_STAGES` / `SystemDefinition` / `SystemTick` / `SystemEventHandlers` / `CompiledSystemSchedule`: list capabilities in `defineGame({ systems })` instead of a manual `onTick` fan-out. Full contract: [reference-systems.md](reference-systems.md). ## Content catalogs diff --git a/.claude/skills/jgengine-gameplay/api.md b/.claude/skills/jgengine-gameplay/api.md index 002e435e9..15fef5a42 100644 --- a/.claude/skills/jgengine-gameplay/api.md +++ b/.claude/skills/jgengine-gameplay/api.md @@ -697,7 +697,7 @@ - `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. +- `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 / editor rig (#207.7, #866): middle-drag pan, right-drag orbit, 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 @@ -710,12 +710,15 @@ - `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). +- `CompiledSystemSchedule` (interface): interface CompiledSystemSchedule — Deterministic compiled schedule: stage buckets, multi-subscribe channels, dependency validation. Order never depends on import order — only stage tables + explicit before/after constraints. - `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_FIXED_STAGES` (const): const DEFAULT_FIXED_STAGES: readonly ["input", "movement", "combat", "ai", "activities", "cleanup"] — Default fixed-sim stage order — systems pick a stage; most need only this. +- `DEFAULT_FRAME_STAGES` (const): const DEFAULT_FRAME_STAGES: readonly ["input", "movement", "combat", "ai", "activities", "cleanup", "animation", "camera", "effects"] — Default frame stage order. Gameplay stages mirror the fixed table so once-per-frame systems (`type: "frame"`) can pick `combat`/`ai`/… without falling into the unknown-stage bucket; presentation stages follow. - `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. @@ -817,6 +820,9 @@ - `Social` (interface): interface Social — ⚠ undocumented - `SocialDeps` (interface): interface SocialDeps — ⚠ undocumented - `StatLevelUpEvent` (interface): interface StatLevelUpEvent — ⚠ undocumented +- `SystemDefinition` (interface): interface SystemDefinition — A reusable game capability — lifecycle, timing, events, and optional save / replication / reset / disposal. Pass instances via `defineGame({ systems })`. Prefer one system per meaningful capability (`combat`, `quests`), not per micro-tick. +- `SystemEventHandlers` (type): type SystemEventHandlers = { readonly [eventName: string]: (ctx: GameContext, event: unknown) => void; } — Event name → handler. Payload is the engine event shape for that name. +- `SystemTick` (type): type SystemTick = | { type: "fixed"; /** Steps per game-second. Default 60. */ rate?: number; stage?: string; after?: string | readonly string[]; before?: string | readonly string[]; } | { type: "frame"; stage?: string; after?: string | readonly string[]; before?: string | readonly string[]; } | { t… — How a system is scheduled. Omit `tick` (or use only `events`) for event-driven systems. Multiple systems may share the same channel; order within a channel is deterministic by stage then optional `before`/`after` constraints — never import order. - `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 @@ -849,6 +855,8 @@ - `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 +- `compileSystemSchedule` (function): function compileSystemSchedule(systems: readonly SystemDefinition[], options?: CompileSystemScheduleOptions): CompiledSystemSchedule — Compile system definitions into a deterministic schedule. Validates unique ids, `dependsOn`, and before/after cycles. +- `composeGameLoop` (function): function composeGameLoop(systems: readonly SystemDefinition[] | undefined, loop: GameLoop | undefined, options?: ComposeGameLoopOptions): GameLoop — Merge a system list with an optional classic `GameLoop` into one loop the shell/runners drive. Systems install on first `onInit`; classic hooks still run for incremental migration. - `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 @@ -901,7 +909,8 @@ - `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`. +- `defineGame` (function): function defineGame(config: GameDefinitionConfig): GameDefinition — Task-first entry point for authoring a game: fills in `scene` and default `assets`, validates `name`, OR-merges `features` from installed systems, and composes `loop` from `systems` + any classic hooks. +- `defineSystem` (function): function defineSystem(definition: SystemDefinition): SystemDefinition — Declare a composable game system. Pure data + hooks — the engine compiles the schedule and installs lifecycle when the game boots. - `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 diff --git a/.claude/skills/jgengine-gameplay/capabilities.md b/.claude/skills/jgengine-gameplay/capabilities.md index 34728652b..40d251f0e 100644 --- a/.claude/skills/jgengine-gameplay/capabilities.md +++ b/.claude/skills/jgengine-gameplay/capabilities.md @@ -8,6 +8,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createRecordBook` (function) · `import { createRecordBook } from "@jgengine/core/gameplay"` +## compose-game-loop — fold composable systems into the game loop without a manual tick fan-out + +- `composeGameLoop` (function) · `import { composeGameLoop } from "@jgengine/core/gameplay"` + ## consumables — use/consume items with cooldowns and effects - `createItemUse` (function) · `import { createItemUse } from "@jgengine/core/gameplay"` @@ -20,6 +24,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createDecayMeterSet` (function) · `import { createDecayMeterSet } from "@jgengine/core/gameplay"` +## define-game — single public game-authoring path — compose systems, world, and loop in one definition + +- `defineGame` (function) · `import { defineGame } from "@jgengine/core/gameplay"` + ## dialogue-bridge — open/close the talkable→DialogueBox flow with no per-game store or command glue - `createGameDialogue` (function) · `import { createGameDialogue } from "@jgengine/core/gameplay"` @@ -40,6 +48,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createSaveStore` (function) · `import { createSaveStore } from "@jgengine/core/gameplay"` +## game-system — declare a composable capability with its own schedule and lifecycle + +- `SystemDefinition` (interface) · `import { SystemDefinition } from "@jgengine/core/gameplay"` + ## item-instance-registry — a runtime store for procedurally generated item instances - `createItemInstanceRegistry` (function) · `import { createItemInstanceRegistry } from "@jgengine/core/gameplay"` @@ -137,6 +149,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createSpawnPoints` (function) · `import { createSpawnPoints } from "@jgengine/core/gameplay"` +## system-schedule — compile fixed/frame/interval system ticks into deterministic ordered stages + +- `compileSystemSchedule` (function) · `import { compileSystemSchedule } from "@jgengine/core/gameplay"` + ## toast-feed — queue of transient self-expiring on-screen messages (toasts, announcer, kill-feed) - `appendToast` (function) · `import { appendToast } from "@jgengine/core/gameplay"` diff --git a/.claude/skills/jgengine-ui/api.md b/.claude/skills/jgengine-ui/api.md index fb5fcc0b4..0e9dc4b44 100644 --- a/.claude/skills/jgengine-ui/api.md +++ b/.claude/skills/jgengine-ui/api.md @@ -80,6 +80,8 @@ - `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. +- `EditorUiDocument` (interface): interface EditorUiDocument — Scene-document HUD section: panel id → layout. Single source of truth for placement. +- `EditorUiPanelLayout` (interface): interface EditorUiPanelLayout — Authored layout for one HUD panel inside `editor.scene.json` → `ui.panels`. - `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. @@ -94,6 +96,7 @@ - `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. +- `HudResizeAxes` (type): type HudResizeAxes = "none" | "x" | "y" | "both" — Which axes a panel type may grow when resized in canvas mode. Resize is semantic — content reflows (longer track, more rows) — never a CSS scale of the whole panel. - `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). @@ -128,11 +131,15 @@ - `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. +- `listHudPanelTypes` (function): function listHudPanelTypes(): HudPanelTypeDef[] — Every registered panel type, sorted by id — editor palette / agent listing. - `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. +- `registerHudPanelType` (function): function registerHudPanelType(def: HudPanelTypeDef): void — Register a HUD panel type so the editor can list/add/configure it and canvas resize knows axes. +- `resizePanelSize` (function): function resizePanelSize(current: HudSize, delta: { dw: number; dh: number }, axes: HudResizeAxes, limits?: { minWidth?: number; maxWidth?: number; minHeight?: number; maxHeight?: number; }): HudSize — Semantic resize: apply pixel deltas only on growable axes, clamped to min/max. Never scales content — callers reflow layout size (track length, list rows). - `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 +- `resolveHudPanelLayout` (function): function resolveHudPanelLayout(docPanel: EditorUiPanelLayout | undefined, fallback: HudPanelFallback): ResolvedHudPanelLayout — Resolve a panel's layout: document entry wins field-by-field over TSX fallback. TSX props are fallback-only — the scene document is the source of truth once authored. - `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. @@ -1204,7 +1211,7 @@ - `AuthoredObjectsProps` (interface): interface AuthoredObjectsProps — Props for {@link AuthoredObjects}: document, ground field, and optional lift / onExisting. - `AuthoredPaths` (function): function AuthoredPaths({ document, field, kinds }: AuthoredPathsProps): React.JSX.Element — Renders a document's non-scatter paths (roads, routes, corridors) as ground-draped ribbons — the editor authors the polyline, the engine drapes it over the live terrain at runtime. Width comes from `path.width`, color from `path.meta.color`/`path.color`. A game never hand-rolls path meshes. - `AuthoredPathsProps` (interface): interface AuthoredPathsProps — Props for {@link AuthoredPaths}: the document, the ground field to drape over, and a kind filter. -- `AuthoredScene` (function): function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, }: AuthoredSceneProps): React.JSX.Element — Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor: drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke render code. When a live-sync bus is installed (editor host), document patches stream in and re-render automatically — document is authoritative; runtime overrides stay ephemeral unless written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`); place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to real catalog GLBs; unmapped items keep the stylized proxy. +- `AuthoredScene` (function): function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, placeObjects, }: AuthoredSceneProps): React.JSX.Element — Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor: drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke render code. When a live-sync bus is installed (editor host), document patches stream in and re-render automatically — document is authoritative; runtime overrides stay ephemeral unless written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`); place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to real catalog GLBs; unmapped items keep the stylized proxy. - `AuthoredSceneProps` (interface): interface AuthoredSceneProps — Props for {@link AuthoredScene}: the document to render and the ground field to drape/ground on. ## @jgengine/shell/scene/AuthoredScene @@ -1213,7 +1220,7 @@ - `AuthoredObjectsProps` (interface): interface AuthoredObjectsProps — Props for {@link AuthoredObjects}: document, ground field, and optional lift / onExisting. - `AuthoredPaths` (function): function AuthoredPaths({ document, field, kinds }: AuthoredPathsProps): React.JSX.Element — Renders a document's non-scatter paths (roads, routes, corridors) as ground-draped ribbons — the editor authors the polyline, the engine drapes it over the live terrain at runtime. Width comes from `path.width`, color from `path.meta.color`/`path.color`. A game never hand-rolls path meshes. - `AuthoredPathsProps` (interface): interface AuthoredPathsProps — Props for {@link AuthoredPaths}: the document, the ground field to drape over, and a kind filter. -- `AuthoredScene` (function): function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, }: AuthoredSceneProps): React.JSX.Element — Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor: drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke render code. When a live-sync bus is installed (editor host), document patches stream in and re-render automatically — document is authoritative; runtime overrides stay ephemeral unless written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`); place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to real catalog GLBs; unmapped items keep the stylized proxy. +- `AuthoredScene` (function): function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, placeObjects, }: AuthoredSceneProps): React.JSX.Element — Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor: drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke render code. When a live-sync bus is installed (editor host), document patches stream in and re-render automatically — document is authoritative; runtime overrides stay ephemeral unless written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`); place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to real catalog GLBs; unmapped items keep the stylized proxy. - `AuthoredSceneProps` (interface): interface AuthoredSceneProps — Props for {@link AuthoredScene}: the document to render and the ground field to drape/ground on. ## @jgengine/shell/scene/GeneratedAssetRenderer diff --git a/.claude/skills/jgengine-ui/capabilities.md b/.claude/skills/jgengine-ui/capabilities.md index 2d7e4b637..6c432ee87 100644 --- a/.claude/skills/jgengine-ui/capabilities.md +++ b/.claude/skills/jgengine-ui/capabilities.md @@ -57,6 +57,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `formatOrdinal` (function) · `import { formatOrdinal } from "@jgengine/core/ui"` +## placement-ghost — render valid/invalid placement preview mesh + +- `PlacementGhost` (function) · `import { PlacementGhost } from "@jgengine/shell/structures"` + ## resolve-game-look — expand a look preset into concrete lighting/backdrop/post knobs - `resolveGameLook` (function) · `import { resolveGameLook } from "@jgengine/core/ui"` diff --git a/.claude/skills/jgengine-world/api.md b/.claude/skills/jgengine-world/api.md index 8e0cf6abe..d24cbfcdc 100644 --- a/.claude/skills/jgengine-world/api.md +++ b/.claude/skills/jgengine-world/api.md @@ -516,10 +516,10 @@ ## @jgengine/core/procedural -- `DecayMeterSet` (interface): interface DecayMeterSet — ⚠ undocumented -- `Moodle` (interface): interface Moodle — ⚠ undocumented -- `MoodleStack` (interface): interface MoodleStack — ⚠ undocumented -- `MultiRegionHealth` (interface): interface MultiRegionHealth — ⚠ undocumented +- `DecayMeterSet` (interface): interface DecayMeterSet — Set of named survival meters (hunger/thirst/…) that drain and refill over game time. +- `Moodle` (interface): interface Moodle — One survival moodle (status icon) — severity, source, and label for HUD chips. +- `MoodleStack` (interface): interface MoodleStack — Ordered stack of active moodles derived from meters/ailments/buffs. +- `MultiRegionHealth` (interface): interface MultiRegionHealth — Per-limb / per-region health track with treat/damage/heal APIs. - `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. @@ -1041,6 +1041,8 @@ - `AssetCatalog` (interface): interface AssetCatalog — ⚠ undocumented - `AudioBusDef` (interface): interface AudioBusDef — ⚠ undocumented - `AudioFalloffConfig` (interface): interface AudioFalloffConfig — ⚠ undocumented +- `AuthoredTrigger` (interface): interface AuthoredTrigger — One resolved trigger binding from a document object. +- `AuthoredTriggerRuntime` (interface): interface AuthoredTriggerRuntime — Runtime handle that watches authored triggers against moving actors each tick. - `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 @@ -1146,6 +1148,7 @@ - `PathFollowState` (interface): interface PathFollowState — ⚠ undocumented - `PhysicsStats` (interface): interface PhysicsStats — ⚠ undocumented - `PhysicsWorld` (class): class PhysicsWorld — ⚠ undocumented +- `PlaceAssetResult` (interface): interface PlaceAssetResult — Shared place-asset verb: one resolved payload for editor `place_asset` and in-game build-mode commits. Convert with {@link toStructureInput} / {@link toEditorMarker}. - `PlacedStructure` (interface): interface PlacedStructure — ⚠ undocumented - `PlacementCommit` (interface): interface PlacementCommit — ⚠ undocumented - `PlacementController` (interface): interface PlacementController — ⚠ undocumented @@ -1231,6 +1234,11 @@ - `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`. +- `TriggerActionDefinition` (interface): interface TriggerActionDefinition — A game-declared action the editor can assign to a volume/marker trigger. Schema drives the inspector params; `targets`/`events` optionally narrow where it appears. +- `TriggerDispatchEvent` (interface): interface TriggerDispatchEvent — Fired when a watched actor trips an authored trigger edge. +- `TriggerEvent` (type): type TriggerEvent = "enter" | "exit" | "interact" — Event edge that can fire an authored trigger. +- `TriggerHandlers` (type): type TriggerHandlers = Readonly void>> — Handler map keyed by action id — unknown actions are skipped unless `onDispatch` is set. +- `TriggerSourceKind` (type): type TriggerSourceKind = "marker" | "volume" — Document collection a trigger source lives on. - `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`. @@ -1275,6 +1283,7 @@ - `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. +- `collectAuthoredTriggers` (function): function collectAuthoredTriggers(document: SceneDocumentLike): AuthoredTrigger[] — Collect every authored trigger on a document's markers and volumes. Pure — no runtime state. Action params use the live {@link registerTriggerAction} registry when present. - `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. @@ -1283,6 +1292,7 @@ - `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 +- `createAuthoredTriggerRuntime` (function): function createAuthoredTriggerRuntime(options: { document: SceneDocumentLike; handlers?: TriggerHandlers; /** Invoked for every dispatch after the matching handler (if any). */ onDispatch?: (event: TriggerDispatchEvent) => void; /** Override the collected trigger list (tests / hot-reload). Default: … — Build a runtime that watches a document's authored triggers against moving actors and dispatches to per-action handlers (and optional catch-all). Pure membership math; the game supplies actors each tick from its own player/entity poses. - `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 @@ -1305,7 +1315,7 @@ - `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 +- `createPlacementController` (function): function createPlacementController(config: PlacementControllerConfig): PlacementController — Headless placement ghost: hover → valid/invalid preview, rotate, grid/free/surface snap, commit. Pair with `@jgengine/shell/structures` `PlacementGhost` and {@link placeAssetFromCommit}. - `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 @@ -1339,6 +1349,7 @@ - `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 +- `getTriggerAction` (function): function getTriggerAction(id: string): TriggerActionDefinition | undefined — Registered definition for an action id, or undefined when the game never declared it. - `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. @@ -1350,7 +1361,9 @@ - `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. +- `listTriggerActions` (function): function listTriggerActions(target?: TriggerSourceKind): TriggerActionDefinition[] — Every registered action, optionally filtered by target collection. - `mapLayerColor` (function): function mapLayerColor(tone: MapLayerTone | undefined): string — ⚠ undocumented +- `markerCatalogId` (function): function markerCatalogId(marker: AuthoredObjectMarkerLike): string | null — Catalog id for a marker: first-class `catalogId` field, else `meta.catalogId` migration alias. Returns null when the marker is not an authored catalog prop (spawn, mob, generator, …). - `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). @@ -1368,15 +1381,19 @@ - `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. +- `placeAssetFromCommit` (function): function placeAssetFromCommit(commit: PlacementCommit, assetId: string, options: PlaceAssetFromCommitOptions = {}): PlaceAssetResult — Bridge a {@link PlacementCommit} into the shared place-asset verb. +- `placeAuthoredObjects` (function): function placeAuthoredObjects(store: AuthoredObjectPlaceTarget, objects: readonly AuthoredObject[], sampleHeight: (x: number, z: number) => number, options: PlaceAuthoredObjectsOptions = {}): string[] — Places resolved authored objects into an object store, grounding each on `sampleHeight(x,z)` plus per-object and options vertical offsets. Returns the instance ids that were placed (or kept). +- `placeAuthoredObjectsFromDocument` (function): function placeAuthoredObjectsFromDocument(store: AuthoredObjectPlaceTarget, document: AuthoredObjectsDocumentLike, sampleHeight: (x: number, z: number) => number, options: PlaceAuthoredObjectsOptions = {}): string[] — Convenience: resolve a document then place every authored catalog prop. - `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. +- `pointInVolume` (function): function pointInVolume(volume: SceneVolumeLike, point: { x: number; y: number; z: number }): boolean — True when `point` is inside an editor volume (sphere / cylinder / box). Cylinder height defaults to diameter when omitted; sphere ignores y for the common ground-plane case only when the volume radius covers the full vertical span — here y is tested for sphere and box too. - `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 +- `quarterTurnsToRotationY` (function): function quarterTurnsToRotationY(quarterTurns: number): number — Maps 0–3 quarter turns onto radians for ghost/commit rotation. - `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. @@ -1384,10 +1401,13 @@ - `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. +- `registerTriggerAction` (function): function registerTriggerAction(definition: TriggerActionDefinition): void — Declare a game action the editor can assign to volume/marker triggers. Idempotent per `id` (last registration wins). Call at module load next to catalogs. - `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. +- `resolveAuthoredObjects` (function): function resolveAuthoredObjects(document: AuthoredObjectsDocumentLike): AuthoredObject[] — Every marker carrying a catalog id, as placeable props — pure, no terrain sample. Parallel to {@link resolveScatter}: games and headless tests read the same list `` places. - `resolveEmitterGain` (function): function resolveEmitterGain(distance: number, sound: Pick, busGain: number): number — ⚠ undocumented - `resolveGridInstances` (function): function resolveGridInstances(config: WorldGridConfig | GridWorldFeature): readonly GridInstanceTransform[] — ⚠ undocumented +- `resolvePlaceAsset` (function): function resolvePlaceAsset(input: ResolvePlaceAssetInput): PlaceAssetResult — Resolve a place-asset intent into a shared payload (editor + games, one verb). - `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. @@ -1429,8 +1449,10 @@ - `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 +- `toEditorMarker` (function): function toEditorMarker(result: PlaceAssetResult): { id: string; kind: string; position: PlaceAssetVec3; rotationY: number; label: string; color: string; meta: Record; } — Scene-document form: feed editor `addMarker` / `place_asset` path. +- `toStructureInput` (function): function toStructureInput(result: PlaceAssetResult): AddStructureInput — Game-state form: feed {@link createPlacedStructureStore}.add. - `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 +- `validatePlacement` (function): function validatePlacement(request: PlacementRequest, rules: PlacementRules = {}): PlacementResult — Footprint validity: bounds + obstacle overlap after optional grid snap. - `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 @@ -1439,6 +1461,18 @@ - `windField` (function): function windField(config: WindFieldConfig = {}): WindField — ⚠ undocumented - `worldSockets` (function): function worldSockets(def: ConnectorPieceDef, piece: PlacedPiece): WorldSocket[] — ⚠ undocumented +## @jgengine/core/world/authoredObjects + +- `AuthoredObject` (interface): interface AuthoredObject — One authored catalog prop resolved from an editor marker — grounded at `x`/`z` with yaw, ready for `ctx.scene.object.place` or {@link placeAuthoredObjects}. +- `AuthoredObjectMarkerLike` (interface): interface AuthoredObjectMarkerLike extends SceneMarkerLike — Minimal marker shape {@link resolveAuthoredObjects} reads; any `EditorMarker` satisfies it. +- `AuthoredObjectPlaceTarget` (interface): interface AuthoredObjectPlaceTarget — Structural place target — any `ObjectStore` satisfies it. +- `AuthoredObjectsDocumentLike` (interface): interface AuthoredObjectsDocumentLike — Minimal document shape {@link resolveAuthoredObjects} walks; any `EditorDocument` satisfies it. +- `PlaceAuthoredObjectsOptions` (interface): interface PlaceAuthoredObjectsOptions — Options for {@link placeAuthoredObjects}. +- `markerCatalogId` (function): function markerCatalogId(marker: AuthoredObjectMarkerLike): string | null — Catalog id for a marker: first-class `catalogId` field, else `meta.catalogId` migration alias. Returns null when the marker is not an authored catalog prop (spawn, mob, generator, …). +- `placeAuthoredObjects` (function): function placeAuthoredObjects(store: AuthoredObjectPlaceTarget, objects: readonly AuthoredObject[], sampleHeight: (x: number, z: number) => number, options: PlaceAuthoredObjectsOptions = {}): string[] — Places resolved authored objects into an object store, grounding each on `sampleHeight(x,z)` plus per-object and options vertical offsets. Returns the instance ids that were placed (or kept). +- `placeAuthoredObjectsFromDocument` (function): function placeAuthoredObjectsFromDocument(store: AuthoredObjectPlaceTarget, document: AuthoredObjectsDocumentLike, sampleHeight: (x: number, z: number) => number, options: PlaceAuthoredObjectsOptions = {}): string[] — Convenience: resolve a document then place every authored catalog prop. +- `resolveAuthoredObjects` (function): function resolveAuthoredObjects(document: AuthoredObjectsDocumentLike): AuthoredObject[] — Every marker carrying a catalog id, as placeable props — pure, no terrain sample. Parallel to {@link resolveScatter}: games and headless tests read the same list `` places. + ## @jgengine/core/world/buildPermissions - `BuildActor` (interface): interface BuildActor — ⚠ undocumented diff --git a/.claude/skills/jgengine-world/capabilities.md b/.claude/skills/jgengine-world/capabilities.md index 7e7a23c86..3d91264f3 100644 --- a/.claude/skills/jgengine-world/capabilities.md +++ b/.claude/skills/jgengine-world/capabilities.md @@ -6,9 +6,15 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p ## authored-objects — place catalog mesh props from an editor document -- `markerCatalogId` (function) · `import { markerCatalogId } from "@jgengine/core/world/authoredObjects"` -- `placeAuthoredObjects` (function) · `import { placeAuthoredObjects } from "@jgengine/core/world/authoredObjects"` -- `resolveAuthoredObjects` (function) · `import { resolveAuthoredObjects } from "@jgengine/core/world/authoredObjects"` +- `markerCatalogId` (function) · `import { markerCatalogId } from "@jgengine/core/world"` +- `placeAuthoredObjects` (function) · `import { placeAuthoredObjects } from "@jgengine/core/world"` +- `resolveAuthoredObjects` (function) · `import { resolveAuthoredObjects } from "@jgengine/core/world"` + +## authored-triggers — schema'd on/action vocabulary on volumes and markers with runtime dispatch + +- `collectAuthoredTriggers` (function) · `import { collectAuthoredTriggers } from "@jgengine/core/world"` +- `createAuthoredTriggerRuntime` (function) · `import { createAuthoredTriggerRuntime } from "@jgengine/core/world"` +- `registerTriggerAction` (function) · `import { registerTriggerAction } from "@jgengine/core/world"` ## behavior-tick — auto-advance patrol/wander behaviors on spawned entities, no per-game route loop @@ -26,6 +32,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createPoseState` (function) · `import { createPoseState } from "@jgengine/core/world"` +## decay-meter — survival meters that drain/refill over game time (hunger, water, oxygen, stamina) + +- `createDecayMeterSet` (function) · `import { createDecayMeterSet } from "@jgengine/core/procedural"` + ## entity-meta — cast-free narrow of SceneEntity.meta via a type guard - `entityMetaOf` (function) · `import { entityMetaOf } from "@jgengine/core/world"` @@ -50,6 +60,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `createGrappleSwing` (function) · `import { createGrappleSwing } from "@jgengine/core/world"` +## limb-health — per-body-part/region health tracked separately + +- `createMultiRegionHealth` (function) · `import { createMultiRegionHealth } from "@jgengine/core/procedural"` + ## lockpick — a solvable grid depth-puzzle with fog-of-war, gates, and hidden traps - `generateLock` (function) · `import { generateLock } from "@jgengine/core/world"` @@ -58,6 +72,20 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p - `ModelNode` (interface) · `import { ModelNode } from "@jgengine/core/world"` +## place-asset — resolve a placement commit into a shared asset placement payload + +- `PlaceAssetResult` (interface) · `import { PlaceAssetResult } from "@jgengine/core/world"` +- `placeAssetFromCommit` (function) · `import { placeAssetFromCommit } from "@jgengine/core/world"` +- `resolvePlaceAsset` (function) · `import { resolvePlaceAsset } from "@jgengine/core/world"` + +## placement-controller — interactive build-mode ghost preview and commit + +- `createPlacementController` (function) · `import { createPlacementController } from "@jgengine/core/world"` + +## placement-math — grid/surface footprint validity for build mode + +- `validatePlacement` (function) · `import { validatePlacement } from "@jgengine/core/world"` + ## proximity-prompt — a "press E" contextual prompt shown near an interactable - `resolveActivePrompt` (function) · `import { resolveActivePrompt } from "@jgengine/core/world"` diff --git a/.claude/skills/jgengine/api.md b/.claude/skills/jgengine/api.md index 3ec72d07b..38e6952ae 100644 --- a/.claude/skills/jgengine/api.md +++ b/.claude/skills/jgengine/api.md @@ -138,17 +138,6 @@ - `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/procedural - -- `DecayMeterSet` (interface): interface DecayMeterSet — Set of named survival meters (hunger/thirst/…) that drain and refill over game time. -- `Moodle` (interface): interface Moodle — One survival moodle (status icon) — severity, source, and label for HUD chips. -- `MoodleStack` (interface): interface MoodleStack — Ordered stack of active moodles derived from meters/ailments/buffs. -- `MultiRegionHealth` (interface): interface MultiRegionHealth — Per-limb / per-region health track with treat/damage/heal APIs. -- `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 diff --git a/.claude/skills/jgengine/capabilities.md b/.claude/skills/jgengine/capabilities.md index 92d68aaf3..cc453f2ef 100644 --- a/.claude/skills/jgengine/capabilities.md +++ b/.claude/skills/jgengine/capabilities.md @@ -4,18 +4,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*. -## decay-meter — survival meters that drain/refill over game time (hunger, water, oxygen, stamina) +## define-game — single public game-authoring path — compose systems, world, and loop in one definition -- `createDecayMeterSet` (function) · `import { createDecayMeterSet } from "@jgengine/core/procedural"` +- `defineGame` (function) · `import { defineGame } from "@jgengine/core/authoring"` ## 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"` -## limb-health — per-body-part/region health tracked separately - -- `createMultiRegionHealth` (function) · `import { createMultiRegionHealth } from "@jgengine/core/procedural"` - ## loot-table — validate a loot table definition for use with the registry - `lootTable` (function) · `import { lootTable } from "@jgengine/core/authoring"` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf51c82aa..7911b27e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,7 @@ jobs: - run: bun run check-stage-skills - run: bun run check-skills - run: git fetch --no-tags origin main + - run: bun run build - run: bun run check-skill-api - run: bun run check-orphan-ratchet @@ -94,7 +95,19 @@ jobs: if: always() runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + issues: write steps: - run: | ok() { test "$1" = "success" || test "$1" = "skipped"; } test "${{ needs.quick.result }}" = "success" && ok "${{ needs.checks.result }}" && ok "${{ needs.web-build.result }}" && ok "${{ needs.smoke.result }}" + - name: Report red main as an issue + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + title="CI red on main" + body="[Run ${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) failed on \`${{ github.sha }}\`. Fix forward from origin/main on a fresh branch, then close this." + n=$(gh issue list --state open --search "\"$title\" in:title" --json number --jq '.[0].number' || true) + if [ -n "$n" ]; then gh issue comment "$n" --body "$body"; else gh issue create --title "$title" --body "$body"; fi diff --git a/AGENTS.md b/AGENTS.md index eed384dfa..7de13b7e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,8 +7,8 @@ Genre-agnostic pure-TypeScript game engine SDK plus its agent skills. Published - **Editor-first for scene, placement, and asset work.** Building or changing what's in a world — placing spawns/objects/props, laying paths/roads/zones, painting terrain/materials, scattering foliage, authoring assets — is done **through the scene editor** (its RPC/CLI `bun packages/editor/src/mcp/cli.ts` or the GUI) and saved into the scene document (`editor.scene.json`), consumed at runtime by engine primitives (`` etc.). The builder should make this easy and look good with zero tuning. When the editor *can't* do something you need, **file a `[FEATURE]` issue for the missing editor/engine capability first**, then fall back to code and note the gap in the PR. Never hardcode level geometry, waypoint arrays, or bespoke per-game placement/render code when the editor could own it — that's the smell (see Design principles → "Author scenes in the editor"). - **Every session is its own ephemeral cloud container.** The **main session** works directly on its assigned `claude/...` branch — no worktrees, no branch juggling. Commit and push early: `git push -u origin ` on its own line (never piped through a filter — a non-zero grep silently drops the push). `warn-unpushed` Stop hook catches strandings. Exception, and only here: parallel **shipping subagents** each run in their own isolated git worktree (`Agent({ isolation: "worktree" })`) so N tasks ship N PRs at once without stomping the shared tree — see `fan-out`. Main never juggles worktrees; the subagents do, and they auto-clean. - **Ship = push → PR → subscribe → stop. Never merge.** When work is done and clean: push, open the PR (`create_pull_request`, ready for review), `subscribe_pr_activity`, report the link, **end the turn** — no waiting or polling. **One PR per branch, ever**: before creating, check none exists (`list_pull_requests` with `head`); if it does, the push already updated it — never open a second. -- **Silence is green.** The subscription delivers CI failures as chat events (PRs run only the ~30s `quick` job; the local gate proved the rest). Failure event → fix on the **same branch**, push, end turn. Never `merge_pull_request`/`enable_pr_auto_merge` unless the user asked this session. Never arm `send_later`/triggers/remote sessions to babysit CI. -- **The user owns merging.** PRs sit parked until they say so in chat. When asked to merge: squash-merge, report, done — no post-merge babysitting of `main` (a red `main` surfaces on the next PR; fix forward from `origin/main` on a fresh branch). One exit before green: a red run whose fix lives outside the repo — report it, hand off as a browser-agent prompt (see Communication), stop. +- **Silence is green.** The subscription delivers CI failures as chat events (PRs run only the ~1min `quick` job incl. `check-skill-api`; the local gate proved the rest). Failure event → fix on the **same branch**, push, end turn. Never `merge_pull_request`/`enable_pr_auto_merge` unless the user asked this session. Never arm `send_later`/triggers/remote sessions to babysit CI. +- **The user owns merging.** PRs sit parked until they say so in chat. When asked to merge: squash-merge, report, done — no post-merge babysitting of `main` (a red `main` auto-files/updates a "CI red on main" issue; fix forward from `origin/main` on a fresh branch and close it). One exit before green: a red run whose fix lives outside the repo — report it, hand off as a browser-agent prompt (see Communication), stop. - **One task, one PR — never chunk.** A task ships as a single PR, however big. Never slice one request into separately-PR'd parts "to keep them small." Keep working the same branch until the whole task is done, then ship once. - **New task in the same session = fresh branch off `origin/main`, new PR.** The previous branch stays parked under its PR — never add to it, never reset it. `git fetch origin main && git checkout -b claude/ origin/main`. Many small parked PRs are the steady state. - **Git ceremony happens once per task, at the end.** No mid-task branch resets, restarts, or checkouts. Never stack new work on merged history — that's where conflicts came from; the session-start hook restarts a clean branch from `origin/main` automatically. The only mid-task restart is a fix-forward after a red merge, and even that is one restart. @@ -72,6 +72,7 @@ Delegation policy lives in the **`fan-out`** skill (`.claude/skills/fan-out`) ## Communication **Telegraph style everywhere** — chat, statuses, quips, briefs. Fragments beat sentences; cut courtesies, hedges, recaps, transitions, play-by-play. Target ~20% of polite prose. If a word gives the reader nothing, cut it. +- **Plain words for the root thing, always — code-level included.** Every explanation — an issue, a bug, a status, a next step, a PR description, even a walk through the actual code — leads with what's actually broken/true in ordinary language. "The engine doesn't auto-place props from the scene file" beats "AuthoredScene can't place catalog objects from editor doc." Naming the file/function/type (the "where") is fine and often needed; convoluted phrasing never is. If a sentence needs re-reading to parse, rewrite it, jargon or not. - **Hard cap: a normal reply fits one phone screen (~8 lines).** The user reads on mobile. One reply per turn — no interleaved narration between tool calls. If it doesn't change what the user does next, don't write it. - Result + decision only. Reasoning stays internal unless asked. diff --git a/CLAUDE.md b/CLAUDE.md index 532baf814..7de13b7e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,8 +7,8 @@ Genre-agnostic pure-TypeScript game engine SDK plus its agent skills. Published - **Editor-first for scene, placement, and asset work.** Building or changing what's in a world — placing spawns/objects/props, laying paths/roads/zones, painting terrain/materials, scattering foliage, authoring assets — is done **through the scene editor** (its RPC/CLI `bun packages/editor/src/mcp/cli.ts` or the GUI) and saved into the scene document (`editor.scene.json`), consumed at runtime by engine primitives (`` etc.). The builder should make this easy and look good with zero tuning. When the editor *can't* do something you need, **file a `[FEATURE]` issue for the missing editor/engine capability first**, then fall back to code and note the gap in the PR. Never hardcode level geometry, waypoint arrays, or bespoke per-game placement/render code when the editor could own it — that's the smell (see Design principles → "Author scenes in the editor"). - **Every session is its own ephemeral cloud container.** The **main session** works directly on its assigned `claude/...` branch — no worktrees, no branch juggling. Commit and push early: `git push -u origin ` on its own line (never piped through a filter — a non-zero grep silently drops the push). `warn-unpushed` Stop hook catches strandings. Exception, and only here: parallel **shipping subagents** each run in their own isolated git worktree (`Agent({ isolation: "worktree" })`) so N tasks ship N PRs at once without stomping the shared tree — see `fan-out`. Main never juggles worktrees; the subagents do, and they auto-clean. - **Ship = push → PR → subscribe → stop. Never merge.** When work is done and clean: push, open the PR (`create_pull_request`, ready for review), `subscribe_pr_activity`, report the link, **end the turn** — no waiting or polling. **One PR per branch, ever**: before creating, check none exists (`list_pull_requests` with `head`); if it does, the push already updated it — never open a second. -- **Silence is green.** The subscription delivers CI failures as chat events (PRs run only the ~30s `quick` job; the local gate proved the rest). Failure event → fix on the **same branch**, push, end turn. Never `merge_pull_request`/`enable_pr_auto_merge` unless the user asked this session. Never arm `send_later`/triggers/remote sessions to babysit CI. -- **The user owns merging.** PRs sit parked until they say so in chat. When asked to merge: squash-merge, report, done — no post-merge babysitting of `main` (a red `main` surfaces on the next PR; fix forward from `origin/main` on a fresh branch). One exit before green: a red run whose fix lives outside the repo — report it, hand off as a browser-agent prompt (see Communication), stop. +- **Silence is green.** The subscription delivers CI failures as chat events (PRs run only the ~1min `quick` job incl. `check-skill-api`; the local gate proved the rest). Failure event → fix on the **same branch**, push, end turn. Never `merge_pull_request`/`enable_pr_auto_merge` unless the user asked this session. Never arm `send_later`/triggers/remote sessions to babysit CI. +- **The user owns merging.** PRs sit parked until they say so in chat. When asked to merge: squash-merge, report, done — no post-merge babysitting of `main` (a red `main` auto-files/updates a "CI red on main" issue; fix forward from `origin/main` on a fresh branch and close it). One exit before green: a red run whose fix lives outside the repo — report it, hand off as a browser-agent prompt (see Communication), stop. - **One task, one PR — never chunk.** A task ships as a single PR, however big. Never slice one request into separately-PR'd parts "to keep them small." Keep working the same branch until the whole task is done, then ship once. - **New task in the same session = fresh branch off `origin/main`, new PR.** The previous branch stays parked under its PR — never add to it, never reset it. `git fetch origin main && git checkout -b claude/ origin/main`. Many small parked PRs are the steady state. - **Git ceremony happens once per task, at the end.** No mid-task branch resets, restarts, or checkouts. Never stack new work on merged history — that's where conflicts came from; the session-start hook restarts a clean branch from `origin/main` automatically. The only mid-task restart is a fix-forward after a red merge, and even that is one restart. diff --git a/Games/claudecraft/src/game/gameplay.test.ts b/Games/claudecraft/src/game/gameplay.test.ts index 5db92a6f0..956c3adc0 100644 --- a/Games/claudecraft/src/game/gameplay.test.ts +++ b/Games/claudecraft/src/game/gameplay.test.ts @@ -3,7 +3,8 @@ import { createGameContext, type GameContext } from "@jgengine/core/runtime/game import { evaluateSkillCheck } from "@jgengine/core/interaction/skillCheck"; import { game } from "../game.config"; -import { loop } from "../loop"; + +const loop = game.loop; import type { AuctionView } from "./auction/systems"; import { classById } from "./classes/catalog"; import { applyMobCc, isMobInstance, mobCount, mobRuntimeOf } from "./ai/mobs"; diff --git a/Games/the-robots/src/editorLayers.ts b/Games/the-robots/src/editorLayers.ts index b05b38881..021ed0954 100644 --- a/Games/the-robots/src/editorLayers.ts +++ b/Games/the-robots/src/editorLayers.ts @@ -294,6 +294,7 @@ export function buildTheRobotsEditorLayers(): EditorDocument { annotations: [], prefabs: [], collections: [], + catalogs: [], }; } diff --git a/apps/dev/src/demo/builderDemo.tsx b/apps/dev/src/demo/builderDemo.tsx index df70260c6..602cbf70c 100644 --- a/apps/dev/src/demo/builderDemo.tsx +++ b/apps/dev/src/demo/builderDemo.tsx @@ -297,7 +297,7 @@ function BuilderUI() { export const builderDemoGame: PlayableGame = { game, content: {}, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: BuilderUI, environment: BuilderScene, camera: { diff --git a/apps/dev/src/demo/demoGame.tsx b/apps/dev/src/demo/demoGame.tsx index 39981b201..c153779d5 100644 --- a/apps/dev/src/demo/demoGame.tsx +++ b/apps/dev/src/demo/demoGame.tsx @@ -310,6 +310,6 @@ export const demoGame: PlayableGame = { itemById: (itemId) => itemCatalog[itemId] ?? null, entityById: (catalogId) => entityCatalog[catalogId] ?? null, }, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: DemoGameUI, }; diff --git a/apps/dev/src/demo/mapDemo.tsx b/apps/dev/src/demo/mapDemo.tsx index 1ddac0402..ee5eda3df 100644 --- a/apps/dev/src/demo/mapDemo.tsx +++ b/apps/dev/src/demo/mapDemo.tsx @@ -285,7 +285,7 @@ export const mapDemoGame: PlayableGame = { entityById: (catalogId) => entityCatalog[catalogId] ?? null, objectById: (catalogId) => objectCatalog[catalogId] ?? null, }, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: MapUI, environment: () => , WorldOverlay: () => , diff --git a/apps/dev/src/demo/pointerDemo.tsx b/apps/dev/src/demo/pointerDemo.tsx index 6a48f16d0..17e643f61 100644 --- a/apps/dev/src/demo/pointerDemo.tsx +++ b/apps/dev/src/demo/pointerDemo.tsx @@ -191,7 +191,7 @@ export const pointerDemoGame: PlayableGame = { entityById: (catalogId) => entityCatalog[catalogId] ?? null, objectById: (catalogId) => objectCatalog[catalogId] ?? null, }, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: CommanderUI, pointer: { select: true, diff --git a/apps/dev/src/demo/sensorShowcase.tsx b/apps/dev/src/demo/sensorShowcase.tsx index 2a9628a93..b3ca09d99 100644 --- a/apps/dev/src/demo/sensorShowcase.tsx +++ b/apps/dev/src/demo/sensorShowcase.tsx @@ -181,7 +181,7 @@ export const sensorShowcaseGame: PlayableGame = { content: { entityById: (catalogId) => entityCatalog[catalogId] ?? null, }, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: SensorHud, WorldOverlay: SensorWorldOverlay, camera: { diff --git a/apps/dev/src/demo/survivalDemo.tsx b/apps/dev/src/demo/survivalDemo.tsx index e9feb2e44..65a1b4f18 100644 --- a/apps/dev/src/demo/survivalDemo.tsx +++ b/apps/dev/src/demo/survivalDemo.tsx @@ -457,7 +457,7 @@ export const survivalDemoGame: PlayableGame = { entityById: (catalogId) => entityCatalog[catalogId] ?? null, itemById: (itemId) => (itemId in itemCatalog ? itemCatalog[itemId as keyof typeof itemCatalog] : null), }, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: SurvivalGameUI, environment: SurvivalWorld, camera: { minDistance: 8, maxDistance: 40, initialDistance: 26, targetHeight: 1.4 }, diff --git a/packages/assets/tsconfig.build.json b/packages/assets/tsconfig.build.json index 9ee6ad87c..d821eeed6 100644 --- a/packages/assets/tsconfig.build.json +++ b/packages/assets/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] } diff --git a/packages/convex/tsconfig.build.json b/packages/convex/tsconfig.build.json index 3fa3c21a7..620f51669 100644 --- a/packages/convex/tsconfig.build.json +++ b/packages/convex/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/testFixtures.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts"] } diff --git a/packages/core/src/editor/commands.ts b/packages/core/src/editor/commands.ts index be2d2e159..af1ebcdc5 100644 --- a/packages/core/src/editor/commands.ts +++ b/packages/core/src/editor/commands.ts @@ -528,6 +528,7 @@ function applyMutating(state: EditorSessionState, command: EditorCommand): Edito annotations: state.document.annotations, prefabs: state.document.prefabs, collections: state.document.collections, + catalogs: state.document.catalogs, ...(state.document.ui === undefined ? {} : { ui: state.document.ui }), }; return { ...state, document: nextDoc }; diff --git a/packages/core/src/editor/document.ts b/packages/core/src/editor/document.ts index 532430fdc..c6febb3b1 100644 --- a/packages/core/src/editor/document.ts +++ b/packages/core/src/editor/document.ts @@ -22,12 +22,14 @@ import type { export function editorDocumentExtras(doc: EditorDocument): { prefabs: EditorPrefab[]; collections: EditorCollection[]; + catalogs: EditorCatalogData[]; terrain?: EditorTerrain; ui?: EditorDocument["ui"]; } { return { prefabs: doc.prefabs, collections: doc.collections, + catalogs: doc.catalogs, ...(doc.terrain === undefined ? {} : { terrain: doc.terrain }), ...(doc.ui === undefined ? {} : { ui: doc.ui }), }; @@ -782,6 +784,7 @@ export function applyEditorDocumentOverlay( annotations: upsertById(base.annotations, overlay.annotations), prefabs: upsertById(base.prefabs, overlay.prefabs), collections: upsertById(base.collections, overlay.collections), + catalogs: upsertCatalogs(base.catalogs, overlay.catalogs), ...(terrain === undefined ? {} : { terrain }), ...(ui === undefined ? {} : { ui }), }; diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts index 9989f92fb..2f610472e 100644 --- a/packages/core/src/editor/types.ts +++ b/packages/core/src/editor/types.ts @@ -1,3 +1,4 @@ +import type { ParamSchema } from "../scene/sceneKinds"; import type { EditorUiDocument } from "../ui/hudDocument"; import type { TerraformSnapshot } from "../world/terraform"; @@ -159,6 +160,8 @@ export interface EditorDocument { prefabs: EditorPrefab[]; /** Named selection sets / production groups — restore, add-to, lock, color, visibility. */ collections: EditorCollection[]; + /** Persisted gameplay catalog values; schemas come from the game's `editorCatalogs` export. */ + catalogs: EditorCatalogData[]; /** * HUD layout owned by the scene document — panel id → anchor/offset/size/visibility. * Canvas mode (F2+C) and `canvas_move_panel` / `canvas_resize_panel` write here; HudPanel reads it. diff --git a/packages/core/src/gameplay.ts b/packages/core/src/gameplay.ts index ba69bb726..c8ba253bb 100644 --- a/packages/core/src/gameplay.ts +++ b/packages/core/src/gameplay.ts @@ -55,19 +55,7 @@ export { type LifecycleConfig, type PhysicsConfig, } from "./game/defineGame"; -export { - defineSystem, - type SystemDefinition, - type SystemEventHandlers, - type SystemTick, -} from "./game/defineSystem"; -export { - DEFAULT_FIXED_STAGES, - DEFAULT_FRAME_STAGES, - compileSystemSchedule, - type CompiledSystemSchedule, -} from "./game/systemSchedule"; -export { composeGameLoop } from "./game/systemRuntime"; +export { defineSystem, type SystemDefinition, type SystemEventHandlers, type SystemTick } from "./game/defineSystem"; export { createGameDialogue, dialogueSlot } from "./game/dialogue"; export { createGameEvents, @@ -180,6 +168,13 @@ export { type WorldInviteTarget, } from "./game/social"; export { createSpawnPoints } from "./game/spawnPoints"; +export { composeGameLoop } from "./game/systemRuntime"; +export { + DEFAULT_FIXED_STAGES, + DEFAULT_FRAME_STAGES, + compileSystemSchedule, + type CompiledSystemSchedule, +} from "./game/systemSchedule"; export { createTalentTree, type TalentNodeDef, type TalentTree } from "./game/talents"; export { appendToast, createToastQueue, pruneToasts, type Toast } from "./game/toasts"; export { createUnlockCatalog, createUnlocks, type UnlockDef } from "./game/unlocks"; diff --git a/packages/core/src/ui.ts b/packages/core/src/ui.ts index a320c38b0..57f2588b4 100644 --- a/packages/core/src/ui.ts +++ b/packages/core/src/ui.ts @@ -45,6 +45,15 @@ export { type LayoutRegion, type MobileHudBehavior, } from "./ui/gameLayout"; +export { + listHudPanelTypes, + registerHudPanelType, + resizePanelSize, + resolveHudPanelLayout, + type EditorUiDocument, + type EditorUiPanelLayout, + type HudResizeAxes, +} from "./ui/hudDocument"; export { HUD_ANCHOR_FRACTIONS, type HudAnchor, @@ -52,13 +61,6 @@ export { type HudPlacement, type HudSize, } from "./ui/hudLayout"; -export type { - EditorUiDocument, - EditorUiPanelLayout, - HudPanelTypeDef, - HudResizeAxes, - ResolvedHudPanelLayout, -} from "./ui/hudDocument"; export { hudScaleForViewport, overflowingPanels, diff --git a/packages/core/src/world.ts b/packages/core/src/world.ts index c9ee77279..619ed6837 100644 --- a/packages/core/src/world.ts +++ b/packages/core/src/world.ts @@ -119,6 +119,21 @@ export { Glide, Grapple } from "./physics/traversal"; export { DEFAULT_GRIP_CURVE, createVehicleBody, sampleGripCurve, type GripCurve } from "./physics/vehicleBody"; export { createAssetCatalog, type AssetCatalog, type ModelAssetRef, type ModelDims } from "./scene/assetCatalog"; export { partsBounds, registerAssetGenerator, type GeneratedAsset, type GeneratedPart } from "./scene/assetGenerator"; +export { + collectAuthoredTriggers, + createAuthoredTriggerRuntime, + getTriggerAction, + listTriggerActions, + pointInVolume, + registerTriggerAction, + type AuthoredTrigger, + type AuthoredTriggerRuntime, + type TriggerActionDefinition, + type TriggerDispatchEvent, + type TriggerEvent, + type TriggerHandlers, + type TriggerSourceKind, +} from "./scene/authoredTriggers"; export { selectAutoTarget, type AutoTargetPolicy } from "./scene/autoTarget"; export { advanceBehaviors } from "./scene/behaviorRuntime"; export { patrol, player, talkable, wander, type BehaviorDescriptor } from "./scene/behaviors"; @@ -174,6 +189,12 @@ export { type VisibilityConfig } from "./visibility/config"; export { distance } from "./visibility/distance"; export { type CameraView, type Frustum } from "./visibility/frustum"; export { createVisibilitySystem, type Renderable, type VisibilitySystem } from "./visibility/visibilitySystem"; +export { + markerCatalogId, + placeAuthoredObjects, + placeAuthoredObjectsFromDocument, + resolveAuthoredObjects, +} from "./world/authoredObjects"; export { createContributionPool, createPlotPermissions, type BuildRole } from "./world/buildPermissions"; export { buildingIndex, type BuildingIndex } from "./world/buildingIndex"; export { type BuildingPaletteOverrides, type BuildingStyle } from "./world/buildings"; @@ -251,6 +272,13 @@ export { type WorldXZ, } from "./world/minimap"; export { placeAlongPath } from "./world/pathInstances"; +export { + placeAssetFromCommit, + resolvePlaceAsset, + toEditorMarker, + toStructureInput, + type PlaceAssetResult, +} from "./world/placeAsset"; export { createPlacedStructureStore, type PlacedStructure } from "./world/placedStructureStore"; export { validatePlacement, type PlacementRules } from "./world/placement"; export { @@ -261,16 +289,6 @@ export { type PlacementPreview, type SnapMode, } from "./world/placementController"; -export { - placeAssetFromCommit, - resolvePlaceAsset, - toEditorMarker, - toStructureInput, - type PlaceAssetFromCommitOptions, - type PlaceAssetResult, - type PlaceAssetVec3, - type ResolvePlaceAssetInput, -} from "./world/placeAsset"; export { composeRealm } from "./world/realm"; export { createRegionField, isRegionField, type RegionField } from "./world/regions"; export { buildRoadRibbon, dashSegments } from "./world/roads"; diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json index 9fa850010..4c71f4c49 100644 --- a/packages/core/tsconfig.build.json +++ b/packages/core/tsconfig.build.json @@ -10,7 +10,7 @@ "src" ], "exclude": [ - "src/**/*.test.ts", + "src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts" ] } \ No newline at end of file diff --git a/packages/editor/src/EditorApp.tsx b/packages/editor/src/EditorApp.tsx index 7a036c5e9..d7d0266a4 100644 --- a/packages/editor/src/EditorApp.tsx +++ b/packages/editor/src/EditorApp.tsx @@ -350,6 +350,8 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip } // Placement/walk views freeze combat/AI so the frame isn't burned on sim. }, onPlayerLeave: playable.loop.onPlayerLeave, + onReset: playable.loop.onReset, + onDispose: playable.loop.onDispose, }; if (mode === "play") { diff --git a/packages/editor/src/EditorChrome.tsx b/packages/editor/src/EditorChrome.tsx index 457016110..764de2f82 100644 --- a/packages/editor/src/EditorChrome.tsx +++ b/packages/editor/src/EditorChrome.tsx @@ -14,6 +14,7 @@ import { listSceneKinds } from "@jgengine/core/scene/sceneKinds"; import { AssetBrowser, type EditorAssetEntry } from "./AssetBrowser"; import { AgentPanel } from "./agent/AgentPanel"; +import { CatalogsPanel } from "./CatalogsPanel"; import { CollectionsPanel } from "./CollectionsPanel"; import { EditorContextMenu } from "./EditorContextMenu"; import { OutlinerPanel } from "./OutlinerPanel"; diff --git a/packages/editor/src/mcp/cli.ts b/packages/editor/src/mcp/cli.ts index efe9aa957..02c95e61e 100644 --- a/packages/editor/src/mcp/cli.ts +++ b/packages/editor/src/mcp/cli.ts @@ -43,6 +43,7 @@ export type EditorCliOptions = { /** * Parses argv into editor-mcp flags. `--rpc -` and `--rpc-file` both set a non-inline * {@link RpcPayloadSource} so large documents never ride a shell argument. + * @internal */ export function parseEditorCliArgs(argv: string[]): EditorCliOptions { let gameId = "the-robots"; @@ -90,25 +91,7 @@ async function main(argv: string[]): Promise { return 0; } - let gameId = "the-robots"; - let port = 17373; - const rpcRaws: string[] = []; - let serve = true; - let stdio = false; - - for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i]!; - if (arg === "--game") gameId = argv[++i] ?? gameId; - else if (arg === "--port") port = Number(argv[++i] ?? port); - else if (arg === "--rpc") { - const raw = argv[++i]; - if (raw !== undefined) rpcRaws.push(raw); - serve = false; - } else if (arg === "--stdio") { - stdio = true; - serve = false; - } else if (arg === "--serve") serve = true; - } + const { gameId, port, rpcSource, serve, stdio } = parseEditorCliArgs(argv); if (stdio) { await runEditorMcpStdio({ gameId }); @@ -118,7 +101,7 @@ async function main(argv: string[]): Promise { const [layers, catalogs] = await Promise.all([loadGameLayers(gameId), loadGameCatalogs(gameId)]); if (!layers.ok) { console.error( - `invalid editorLayers for ${options.gameId}: ${layers.errors.map((e) => `${e.path} ${e.message}`).join("; ")}`, + `invalid editorLayers for ${gameId}: ${layers.errors.map((e) => `${e.path} ${e.message}`).join("; ")}`, ); return 1; } @@ -127,40 +110,39 @@ async function main(argv: string[]): Promise { return 1; } const { api, dispose } = createEditorHost({ - gameId: options.gameId, + gameId: gameId, layers: layers.document, catalogs: catalogs.catalogs, }); - if (rpcRaws.length > 0) { - let allOk = true; - for (const rpcRaw of rpcRaws) { - const decoded = decodeEditorBridgeRequest(JSON.parse(rpcRaw)); - if (!decoded.ok) { - console.log( - JSON.stringify( - { ok: false, error: decoded.errors.map((e) => `${e.path} ${e.message}`).join("; ") }, - null, - 2, - ), - ); - allOk = false; - break; - } - const response = api.handle(decoded.request); - console.log(JSON.stringify(response, null, 2)); - if (!response.ok) { - allOk = false; - break; - } + if (rpcSource !== null) { + const payload = await loadRpcPayload(rpcSource); + if (!payload.ok) { + console.error(payload.error); + dispose(); + return 1; + } + const decoded = decodeEditorBridgeRequest(payload.value); + if (!decoded.ok) { + console.log( + JSON.stringify( + { ok: false, error: decoded.errors.map((e) => `${e.path} ${e.message}`).join("; ") }, + null, + 2, + ), + ); + dispose(); + return 1; } + const response = api.handle(decoded.request); + console.log(JSON.stringify(response, null, 2)); dispose(); - return allOk ? 0 : 1; + return response.ok ? 0 : 1; } if (options.serve) { const server = startEditorBridgeServerNode({ host: api, port: options.port }); - console.log(`editor bridge for ${options.gameId} at ${server.url}`); + console.log(`editor bridge for ${gameId} at ${server.url}`); console.log(`POST ${server.url}/rpc body: {"method":"scene_summary"}`); console.log("tools:", EDITOR_MCP_TOOLS.map((tool) => tool.name).join(", ")); await new Promise(() => undefined); diff --git a/packages/editor/src/mcp/loadGameCatalogs.ts b/packages/editor/src/mcp/loadGameCatalogs.ts index d742b15f7..8f4b5fc3a 100644 --- a/packages/editor/src/mcp/loadGameCatalogs.ts +++ b/packages/editor/src/mcp/loadGameCatalogs.ts @@ -1,4 +1,4 @@ -import type { EditorCatalogDefinition, EditorCatalogsInput } from "@jgengine/core/editor/index"; +import type { EditorCatalogDefinition, EditorCatalogEntry, EditorCatalogsInput } from "@jgengine/core/editor/index"; import type { ParamSchema } from "@jgengine/core/scene/sceneKinds"; /** Result of {@link loadGameCatalogs}: validated definitions, or diagnostics when the export is malformed. */ @@ -55,7 +55,7 @@ export function decodeGameCatalogs(resolved: unknown): LoadGameCatalogsResult { return; } if (typeof item.id !== "string" || typeof item.label !== "string" || schema === null) return; - const entries: EditorCatalogDefinition["entries"] = []; + const entries: EditorCatalogEntry[] = []; item.entries.forEach((entry, entryIndex) => { const entryPath = `${path}.entries[${entryIndex}]`; if (!isPlainObject(entry) || typeof entry.id !== "string") { diff --git a/packages/editor/src/mcp/rpcPayload.ts b/packages/editor/src/mcp/rpcPayload.ts index 2236ac7b3..a8b5ac1ef 100644 --- a/packages/editor/src/mcp/rpcPayload.ts +++ b/packages/editor/src/mcp/rpcPayload.ts @@ -11,14 +11,14 @@ export type RpcPayloadResult = | { ok: true; value: unknown; raw: string; sourceLabel: string } | { ok: false; error: string }; -/** Human-readable label for error messages (and tests). */ +/** Human-readable label for error messages (and tests). @internal */ export function rpcSourceLabel(source: RpcPayloadSource): string { if (source.kind === "inline") return "inline --rpc"; if (source.kind === "file") return `--rpc-file ${source.path}`; return "stdin (--rpc -)"; } -/** True when braces/brackets/quotes look cut off — common when a shell truncates a long --rpc arg. */ +/** True when braces/brackets/quotes look cut off — common when a shell truncates a long --rpc arg. @internal */ export function looksTruncatedJson(raw: string): boolean { let depth = 0; let inString = false; @@ -49,6 +49,7 @@ export function looksTruncatedJson(raw: string): boolean { /** * Builds a clear diagnostic when JSON.parse fails on an RPC body — names the source, size, and * (for inline args) points agents at `--rpc-file` / `--rpc -` instead of a bare SyntaxError. + * @internal */ export function formatRpcParseError(raw: string, error: unknown, source: RpcPayloadSource): string { const detail = error instanceof Error ? error.message : String(error); @@ -67,7 +68,7 @@ export function formatRpcParseError(raw: string, error: unknown, source: RpcPayl return parts.join(". "); } -/** JSON.parse with a source-aware diagnostic (never throws). */ +/** JSON.parse with a source-aware diagnostic (never throws). @internal */ export function parseRpcJson(raw: string, source: RpcPayloadSource): RpcPayloadResult { if (raw.length === 0) { return { @@ -82,7 +83,7 @@ export function parseRpcJson(raw: string, source: RpcPayloadSource): RpcPayloadR } } -/** Reads the raw RPC text from an inline arg, file path, or stdin. */ +/** Reads the raw RPC text from an inline arg, file path, or stdin. @internal */ export async function readRpcText( source: RpcPayloadSource, readStdin: () => Promise = defaultReadStdin, @@ -106,7 +107,7 @@ export async function readRpcText( } } -/** Load + parse an RPC payload from the resolved CLI source. */ +/** Load + parse an RPC payload from the resolved CLI source. @internal */ export async function loadRpcPayload( source: RpcPayloadSource, readStdin?: () => Promise, diff --git a/packages/editor/src/session.ts b/packages/editor/src/session.ts index 95781f121..4df0f06c0 100644 --- a/packages/editor/src/session.ts +++ b/packages/editor/src/session.ts @@ -17,10 +17,12 @@ import { planRuntimeInspectorSet, runtimeEntityMetaWriteBackCommand, runtimeEntityWriteBackCommand, + seedEditorCatalogs, summarizeEditorSession, summarizeRuntimeInspector, type DocumentLiveSync, type DocumentPatch, + type EditorCatalogDefinition, type EditorCommand, type EditorDocument, type EditorKindVisibility, @@ -281,13 +283,13 @@ export function installEditorHost(api: EditorHostApi): () => void { }; } -/** Retrieves the globally installed editor host, or null if none is mounted. */ +/** Retrieves the globally installed editor host, or null if none is mounted. @internal */ export function getEditorHost(): EditorHostApi | null { const root = globalThis as typeof globalThis & { [GLOBAL_KEY]?: EditorHostApi }; return root[GLOBAL_KEY] ?? null; } -/** Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. */ +/** Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. @internal */ export function createEditorHost(options: { gameId: string; layers: EditorLayersInput | undefined; @@ -301,6 +303,7 @@ export function createEditorHost(options: { dispose: () => void; } { const catalogDefinitions = options.catalogs ?? []; + const catalogById = new Map(catalogDefinitions.map((definition) => [definition.id, definition])); const document = seedEditorCatalogs(normalizeEditorLayers(options.layers), catalogDefinitions); const session = createEditorSession(document); const liveSync = createDocumentLiveSync(document); diff --git a/packages/editor/tsconfig.build.json b/packages/editor/tsconfig.build.json index 62483ff73..077851fa0 100644 --- a/packages/editor/tsconfig.build.json +++ b/packages/editor/tsconfig.build.json @@ -11,7 +11,7 @@ }, "include": ["src"], "exclude": [ - "src/**/*.test.ts", + "src/**/*.test.ts", "src/**/*.test.tsx", "src/mcp/cli.ts", "src/mcp/rpcPayload.ts", "src/mcp/bridgeServer.node.ts", diff --git a/packages/github/tsconfig.build.json b/packages/github/tsconfig.build.json index 91a4a7c9d..ec0832c63 100644 --- a/packages/github/tsconfig.build.json +++ b/packages/github/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] } diff --git a/packages/jgengine/tsconfig.build.json b/packages/jgengine/tsconfig.build.json index eb871faba..fb5726cbe 100644 --- a/packages/jgengine/tsconfig.build.json +++ b/packages/jgengine/tsconfig.build.json @@ -7,5 +7,5 @@ "rootDir": "src" }, "include": ["src"], - "exclude": ["src/**/*.test.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] } diff --git a/packages/node/tsconfig.build.json b/packages/node/tsconfig.build.json index 8a839cc22..978b94534 100644 --- a/packages/node/tsconfig.build.json +++ b/packages/node/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/testFixtures.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts"] } diff --git a/packages/react/tsconfig.build.json b/packages/react/tsconfig.build.json index a16565534..53f9e4fe4 100644 --- a/packages/react/tsconfig.build.json +++ b/packages/react/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/testFixtures.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts"] } diff --git a/packages/shell/src/defineGame.tsx b/packages/shell/src/defineGame.tsx index d8d790e55..159c2ecd1 100644 --- a/packages/shell/src/defineGame.tsx +++ b/packages/shell/src/defineGame.tsx @@ -106,8 +106,8 @@ export function defineGame( onNewPlayer: withPhaseSync(composed?.onNewPlayer), onTick: withPhaseSync(composed?.onTick), onPlayerLeave: composed?.onPlayerLeave ?? noop, - onReset: composed?.onReset, - onDispose: composed?.onDispose, + onReset: composed?.onReset?.bind(composed) ?? noop, + onDispose: composed?.onDispose?.bind(composed) ?? noop, }, GameUI: GameUI ?? emptyUi, environment: diff --git a/packages/shell/src/scene/AuthoredScene.tsx b/packages/shell/src/scene/AuthoredScene.tsx index 1efa8a1ff..f113a3258 100644 --- a/packages/shell/src/scene/AuthoredScene.tsx +++ b/packages/shell/src/scene/AuthoredScene.tsx @@ -262,6 +262,12 @@ export interface AuthoredSceneProps { * the `document` prop (tests, one-shot previews). */ live?: boolean; + /** + * Place the document's catalog-id markers into the object + * store — WorldScene renders them via the game's `objectModels` seam. Omit when the game places + * props itself in onInit with `placeAuthoredObjects`. + */ + placeObjects?: boolean | { verticalOffset?: number }; } /** @@ -281,6 +287,7 @@ export function AuthoredScene({ scatterModels, assets, live = true, + placeObjects, }: AuthoredSceneProps) { const liveDocument = useLiveEditorDocument(document, live); const instances = useMemo(() => resolveScatter(liveDocument, field), [liveDocument, field]); @@ -299,6 +306,9 @@ export function AuthoredScene({ context={{ document: liveDocument, field, ...(assets === undefined ? {} : { assets }) }} /> + {shouldPlaceObjects ? ( + + ) : null} ); } diff --git a/packages/shell/tsconfig.build.json b/packages/shell/tsconfig.build.json index 8b6b48b64..7572c95dd 100644 --- a/packages/shell/tsconfig.build.json +++ b/packages/shell/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/cartridge/testkit.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/cartridge/testkit.ts"] } diff --git a/packages/sql/tsconfig.build.json b/packages/sql/tsconfig.build.json index ed459ae9b..b8d88e557 100644 --- a/packages/sql/tsconfig.build.json +++ b/packages/sql/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/testFixtures.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts"] } diff --git a/packages/ws/tsconfig.build.json b/packages/ws/tsconfig.build.json index 8a5f5a472..d4cad0062 100644 --- a/packages/ws/tsconfig.build.json +++ b/packages/ws/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/testFixtures.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts"] } diff --git a/scripts/api-doc-baseline.json b/scripts/api-doc-baseline.json index 3e72d8074..7215c4fba 100644 --- a/scripts/api-doc-baseline.json +++ b/scripts/api-doc-baseline.json @@ -2840,7 +2840,6 @@ "@jgengine/shell/structures#InstancedBuildingPlacement", "@jgengine/shell/structures#InstancedBuildings", "@jgengine/shell/structures#InstancedBuildingsProps", - "@jgengine/shell/structures#PlacementGhost", "@jgengine/shell/structures#PlacementGhostProps", "@jgengine/shell/structures/GeneratedBuilding#BuildingBlock", "@jgengine/shell/structures/GeneratedBuilding#BuildingBlockProps", diff --git a/scripts/api-orphan-baseline.json b/scripts/api-orphan-baseline.json index 2a3b33d23..e6a1604ce 100644 --- a/scripts/api-orphan-baseline.json +++ b/scripts/api-orphan-baseline.json @@ -27,7 +27,6 @@ "@jgengine/assets/verify#verifyData", "@jgengine/assets/verify#verifyManifest", "@jgengine/convex#createConvexChatSync", - "@jgengine/convex#createConvexChatTransport", "@jgengine/convex#createConvexFeedWrites", "@jgengine/convex#createConvexGameFeeds", "@jgengine/convex#createConvexGameTransport", @@ -37,7 +36,6 @@ "@jgengine/convex#defaultConvexGameApi", "@jgengine/convex#randomConvexPlayerId", "@jgengine/convex#watchConvexQuery", - "@jgengine/convex/convexChatTransport#createConvexChatTransport", "@jgengine/convex/convexPresenceTransport#createConvexPresenceTransport", "@jgengine/convex/createConvexGameTransport#createConvexChatSync", "@jgengine/convex/createConvexGameTransport#createConvexFeedWrites", @@ -50,19 +48,12 @@ "@jgengine/convex/resolveConvexMultiplayer#randomConvexPlayerId", "@jgengine/core/audio/synth#patchDuration", "@jgengine/core/cards/cardPile#countIn", - "@jgengine/core/cards/cardPile#shuffleZone", "@jgengine/core/cards/cardPile#zoneOf", - "@jgengine/core/cards/modifierPipeline#createModifierPipeline", "@jgengine/core/combat/attackTags#hasAnyTag", - "@jgengine/core/combat/attackTags#hasTag", "@jgengine/core/combat/comboString#stepById", "@jgengine/core/combat/death#deathReasonFromEffect", "@jgengine/core/combat/death#normalizeOnDeath", - "@jgengine/core/combat/defensiveWindow#iframeActiveAt", "@jgengine/core/combat/defensiveWindow#totalWindowMs", - "@jgengine/core/combat/defensiveWindow#windowActiveAt", - "@jgengine/core/combat/effects#resolveAreaTargets", - "@jgengine/core/combat/hitReaction#applyImpulse", "@jgengine/core/combat/resistance#UnknownResistanceCategoryError", "@jgengine/core/combat/resistance#UnknownResistancePropertyError", "@jgengine/core/combat/shotOrigin#aimDirection", @@ -70,22 +61,9 @@ "@jgengine/core/commands/commandRegistry#createCommandRegistry", "@jgengine/core/crafting/production#acceptsInput", "@jgengine/core/crafting/recipe#hasRecipeInputs", - "@jgengine/core/crafting/recipe#missingInputs", - "@jgengine/core/data/dataSource#createDataSource", - "@jgengine/core/data/devProxy#parseDevProxyTable", - "@jgengine/core/data/devProxy#proxiedUrl", - "@jgengine/core/data/fetchJson#HttpStatusError", - "@jgengine/core/data/fetchJson#JsonParseError", - "@jgengine/core/data/fetchJson#fetchJson", - "@jgengine/core/data/jsonDataSource#createJsonDataSource", "@jgengine/core/devtools/devtools#formatLogMessage", "@jgengine/core/devtools/devtools#measureProfile", - "@jgengine/core/economy/wallet#canAfford", "@jgengine/core/format/duration#padNumber", - "@jgengine/core/game/chat#createChat", - "@jgengine/core/game/chat#whisperChannelId", - "@jgengine/core/game/chatFilter#createChatFilter", - "@jgengine/core/game/chatFilter#normalizeChatText", "@jgengine/core/game/connectedPlayers#createConnectedPlayers", "@jgengine/core/game/controlGate#setPlayControlsActive", "@jgengine/core/game/feed#appendFeedEntry", @@ -94,31 +72,19 @@ "@jgengine/core/game/keyValueStore#defaultKeyValueStorage", "@jgengine/core/game/lootTable#grantDrops", "@jgengine/core/game/objectives#evaluateObjectives", - "@jgengine/core/game/ping#classifyPing", "@jgengine/core/game/quest#applyQuestRewards", "@jgengine/core/game/quest#createQuestEvaluator", - "@jgengine/core/game/race#everyoneFinishes", - "@jgengine/core/game/race#lastStanding", - "@jgengine/core/game/race#topK", - "@jgengine/core/game/runDraft#createRunModifierStack", "@jgengine/core/game/unlocks#grantUnlock", "@jgengine/core/game/unlocks#hasUnlock", "@jgengine/core/game/unlocks#unlockTree", "@jgengine/core/input/bindingOverrides#bindingOverridesStorageKey", "@jgengine/core/input/bindingOverrides#clearAllBindingOverrides", "@jgengine/core/input/lookChannel#createLookChannel", - "@jgengine/core/input/pointer#createDragCapture", - "@jgengine/core/input/pointer#groundOf", - "@jgengine/core/input/pointer#moveTargetFromHit", "@jgengine/core/input/pointerAxis#pointerAxisValue", - "@jgengine/core/input/touchScheme#touchActionLabel", "@jgengine/core/input/touchScheme#touchButtonKind", "@jgengine/core/interaction/proximityPrompt#positionedPromptsEqual", "@jgengine/core/interaction/proximityPrompt#promptCommandsEqual", "@jgengine/core/interaction/proximityPrompt#promptDisplaysEqual", - "@jgengine/core/interaction/qte#qteProgress", - "@jgengine/core/interaction/skillCheck#skillCheckMarkerPosition", - "@jgengine/core/inventory/storageTier#tierOf", "@jgengine/core/item/durability#canRepairAt", "@jgengine/core/item/durability#isBroken", "@jgengine/core/item/durability#wearAmount", @@ -129,24 +95,17 @@ "@jgengine/core/movement/avatarGait#gaitSwayAngle", "@jgengine/core/movement/playerMovement#forgetPlayerMovement", "@jgengine/core/movement/playerMovement#playerMovementHeading", - "@jgengine/core/movement/playerMovement#resolvePhysicsTuning", "@jgengine/core/movement/steering#steerToward", "@jgengine/core/movement/steering#yawForward", "@jgengine/core/movement/steering#yawRight", "@jgengine/core/movement/voxelController#advanceVoxelPlayer", "@jgengine/core/movement/voxelController#createVoxelPlayerBody", - "@jgengine/core/multiplayer/chatContract#createLocalChatTransport", - "@jgengine/core/multiplayer/matchmaking#generateJoinCode", "@jgengine/core/multiplayer/matchmaking#hasSpace", - "@jgengine/core/multiplayer/matchmaking#matchesFilter", "@jgengine/core/multiplayer/presenceContract#createLocalPresenceTransport", "@jgengine/core/nav/corridors#createCorridorField", - "@jgengine/core/nav/navGrid#smoothPath", "@jgengine/core/nav/railGraph#createRailGraph", "@jgengine/core/nav/railGraph#createRailRider", "@jgengine/core/nav/timetable#createRouteTimetable", - "@jgengine/core/physics/buoyancy#BuoyantBody", - "@jgengine/core/physics/damageZones#DamageModel", "@jgengine/core/physics/flowTube#combineFlowVelocity", "@jgengine/core/physics/flowTube#createFlowTube", "@jgengine/core/physics/forceVolume#applyVolumeForce", @@ -154,42 +113,19 @@ "@jgengine/core/physics/physicsWorld#cellCoord", "@jgengine/core/physics/physicsWorld#cellIndex", "@jgengine/core/physics/radialImpulse#radialImpulse", - "@jgengine/core/physics/ragdoll#Ragdoll", - "@jgengine/core/physics/vehicleBody#VehicleBody", - "@jgengine/core/random/nameGen#fillTemplate", - "@jgengine/core/random/nameGen#pickFrom", "@jgengine/core/random/rng#hashString", "@jgengine/core/random/rng#randomSeedFrom", "@jgengine/core/random/rng#stepRandomSeed", "@jgengine/core/scene/assetPreload#createSceneAssetPreloader", - "@jgengine/core/scene/autoTarget#createAutoTargeter", - "@jgengine/core/scene/behaviors#promptable", - "@jgengine/core/scene/captureCheck#captureChance", - "@jgengine/core/scene/captureCheck#rollCapture", - "@jgengine/core/scene/entityStore#createEntityStore", - "@jgengine/core/scene/entityStore#movedWhileFrozen", - "@jgengine/core/scene/form#createForms", - "@jgengine/core/scene/movementSpeed#applyStatDrivenSpeed", "@jgengine/core/scene/movementSpeed#deriveWalkSpeed", "@jgengine/core/scene/objectQuery#intersectAabb", "@jgengine/core/scene/objectQuery#normalizeDirection", - "@jgengine/core/scene/objectStore#createObjectStore", - "@jgengine/core/scene/paintLayer#createPaintLayer", - "@jgengine/core/scene/possession#createPossession", - "@jgengine/core/scene/roster#createRoster", "@jgengine/core/scene/sceneRaycast#createSceneRaycast", - "@jgengine/core/scene/selection#rectContainsPoint", - "@jgengine/core/scene/spatial#createSpatialApi", - "@jgengine/core/scene/spatial#distanceBetween", - "@jgengine/core/scene/stationClaim#StationClaim", "@jgengine/core/scene/targeting#createTargeting", "@jgengine/core/settings/settingsModel#loadSettingValue", "@jgengine/core/settings/settingsModel#saveSettingValue", "@jgengine/core/settings/settingsModel#settingStorageKey", - "@jgengine/core/time/gameClock#computeGameDay", - "@jgengine/core/time/gameClock#getScaledElapsedMs", "@jgengine/core/time/serverTick#planServerTick", - "@jgengine/core/time/simClock#createSimClock", "@jgengine/core/time/stateSchedule#createStateSchedule", "@jgengine/core/time/stateSchedule#nextClearWindow", "@jgengine/core/ui/hudScale#rectOverflow", @@ -209,23 +145,13 @@ "@jgengine/core/visibility/spatialIndex#createSpatialIndex", "@jgengine/core/world/cellStates#createCellStateGrid", "@jgengine/core/world/connectors#collectWorldSockets", - "@jgengine/core/world/features#island", "@jgengine/core/world/features#padFlattenMasks", "@jgengine/core/world/gridInstances#resolveGridCells", - "@jgengine/core/world/interiors#createInteriors", "@jgengine/core/world/mapLayers#pointInMapZone", "@jgengine/core/world/massing#composeMassing", "@jgengine/core/world/massing#massingFloorCount", - "@jgengine/core/world/placement#footprintObstacle", - "@jgengine/core/world/roads#isOnRoad", - "@jgengine/core/world/roads#nearestOnPath", - "@jgengine/core/world/roads#pathLength", - "@jgengine/core/world/scatter#scatterAabb", "@jgengine/core/world/segment#circleVsSegment", "@jgengine/core/world/segment#closestPointOnSegment", - "@jgengine/core/world/streets#sidewalkPaths", - "@jgengine/core/world/terraform#brushWeight", - "@jgengine/core/world/water#synthesizeWaves", "@jgengine/core/world/windZones#createWindZones", "@jgengine/editor#AssetBrowser", "@jgengine/editor#EditorLayerOverlays", @@ -258,16 +184,10 @@ "@jgengine/node/testFixtures#createTestRuntime", "@jgengine/node/webHandler#toWebRequest", "@jgengine/shell/GamePhaseStamp#GamePhaseStamp", - "@jgengine/shell/audio/AudioComponents#AudioListener", - "@jgengine/shell/audio/AudioComponents#EntityAudioEmitters", - "@jgengine/shell/audio/AudioComponents#ObjectAudioEmitters", "@jgengine/shell/audio/audioEngine#createAudioEngine", - "@jgengine/shell/audio/musicDirector#MusicDirector", "@jgengine/shell/audio/musicVoices#playMusicNote", "@jgengine/shell/audio/synthEngine#createNoiseBuffer", "@jgengine/shell/audio/synthEngine#realizeSynthPatch", - "@jgengine/shell/behaviour#attachObject3D", - "@jgengine/shell/behaviourAttach#attachObject3D", "@jgengine/shell/camera#GameCameraRig", "@jgengine/shell/camera#GameFirstPersonCamera", "@jgengine/shell/camera#GameInspectionCamera", @@ -301,12 +221,10 @@ "@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#logRuntimeError", - "@jgengine/shell/drivers/FrameDriver#FrameDriver", "@jgengine/shell/drivers/HudOnlyDriver#HudOnlyDriver", "@jgengine/shell/environment/GroundPad#GroundPad", "@jgengine/shell/environment/RoadRibbons#RoadRibbons", @@ -317,7 +235,6 @@ "@jgengine/shell/pointer/PointerProbe#PointerProbe", "@jgengine/shell/postfx/PostProcessing#PostProcessing", "@jgengine/shell/postfx/gradeShader#createGradePass", - "@jgengine/shell/registry#resolveGameLoader", "@jgengine/shell/render/SceneLighting#BackdropFog", "@jgengine/shell/render/SceneLighting#ConfiguredLighting", "@jgengine/shell/render/SceneModels#EntityModel", @@ -332,46 +249,32 @@ "@jgengine/shell/settings/appliedSettings#useSettingsRevision", "@jgengine/shell/settings/settingsController#useSettingsCategories", "@jgengine/shell/structures#BuildingBlock", - "@jgengine/shell/structures#GeneratedBuilding", "@jgengine/shell/structures#InstancedBuildings", "@jgengine/shell/structures/GeneratedBuilding#BuildingBlock", - "@jgengine/shell/structures/GeneratedBuilding#GeneratedBuilding", "@jgengine/shell/structures/GeneratedBuilding#InstancedBuildings", "@jgengine/shell/touch/OrientationHint#OrientationHint", "@jgengine/shell/touch/TouchControlsOverlay#TouchControlsDock", "@jgengine/shell/touch/TouchControlsOverlay#primaryButtonOffsets", "@jgengine/shell/touch/TouchControlsOverlay#touchDockClearance", "@jgengine/shell/useShellMultiplayerSync#useShellMultiplayerSync", - "@jgengine/shell/visibility/CullingProvider#CullingProvider", "@jgengine/shell/visibility/CullingProvider#useRenderVisibility", "@jgengine/shell/vision/FrustumSensorHud#frustumSampleDisplayEqual", "@jgengine/shell/vision/FrustumSensorHud#useFrustumSensor", "@jgengine/shell/vision/RevealVision#useRevealHits", "@jgengine/shell/vision/frustumSampleEqual#frustumSampleDisplayEqual", - "@jgengine/shell/water#Ocean", "@jgengine/shell/water#createOceanMaterial", "@jgengine/shell/water#syncOceanMaterial", - "@jgengine/shell/water/Ocean#Ocean", "@jgengine/shell/water/OceanMaterial#createOceanMaterial", "@jgengine/shell/water/OceanMaterial#syncOceanMaterial", "@jgengine/shell/weather#LightningStrike", - "@jgengine/shell/weather#RainField", - "@jgengine/shell/weather#SnowField", "@jgengine/shell/weather#WeatherLayer", "@jgengine/shell/weather/LightningStrike#LightningStrike", - "@jgengine/shell/weather/RainField#RainField", - "@jgengine/shell/weather/SnowField#SnowField", "@jgengine/shell/weather/WeatherLayer#WeatherLayer", "@jgengine/shell/world/DataObjects#DataObjects", "@jgengine/shell/world/GridWorldScene#GridWorldScene", - "@jgengine/shell/world/InstancedBodies#InstancedBodies", - "@jgengine/shell/world/InstancedJoints#InstancedJoints", - "@jgengine/shell/world/SpriteBatch#SpriteBatch", "@jgengine/shell/world/WorldHud#ProjectileTracers", "@jgengine/shell/world/WorldHud#WorldFloatText", "@jgengine/shell/world/WorldHud#WorldTelegraphs", - "@jgengine/shell/world/WorldItems#WorldItems", - "@jgengine/shell/world/WorldScene#RemotePlayers", "@jgengine/shell/world/WorldScene#WorldView", "@jgengine/ws#computeVoiceGain", "@jgengine/ws/voiceChannel#computeVoiceGain" diff --git a/scripts/check-game-shape.ts b/scripts/check-game-shape.ts index deef525bc..aebbd2e65 100644 --- a/scripts/check-game-shape.ts +++ b/scripts/check-game-shape.ts @@ -11,6 +11,8 @@ const SKELETON_FILES = new Set([ "index.css", "style.css", "editorLayers.ts", + "editorCatalogs.ts", + "editorCatalogs.test.ts", "editorLayers.test.ts", "editor.scene.json", ]); diff --git a/scripts/export-manifest.json b/scripts/export-manifest.json index 295e1ee5d..88a4c5095 100644 --- a/scripts/export-manifest.json +++ b/scripts/export-manifest.json @@ -67,6 +67,8 @@ "./editor/commands", "./editor/document", "./editor/index", + "./editor/liveSync", + "./editor/runtimeInspector", "./editor/types", "./faction/factions", "./faction/reputation", @@ -79,6 +81,7 @@ "./game/controlGate", "./game/cosmetics", "./game/defineGame", + "./game/defineSystem", "./game/dialogue", "./game/events", "./game/feed", @@ -102,6 +105,8 @@ "./game/snapshotHistory", "./game/social", "./game/spawnPoints", + "./game/systemRuntime", + "./game/systemSchedule", "./game/talents", "./game/toasts", "./game/trade", @@ -222,6 +227,7 @@ "./scene/assetCatalog", "./scene/assetGenerator", "./scene/assetPreload", + "./scene/authoredTriggers", "./scene/autoTarget", "./scene/behaviorRuntime", "./scene/behaviors", @@ -290,6 +296,7 @@ "./turn/turnLoop", "./ui", "./ui/gameLayout", + "./ui/hudDocument", "./ui/hudLayout", "./ui/hudScale", "./ui/orientation", @@ -307,6 +314,7 @@ "./visibility/spatialIndex", "./visibility/visibilitySystem", "./world", + "./world/authoredObjects", "./world/buildPermissions", "./world/buildingGenerator", "./world/buildingIndex", @@ -330,6 +338,7 @@ "./world/massing", "./world/minimap", "./world/pathInstances", + "./world/placeAsset", "./world/placedStructureStore", "./world/placement", "./world/placementController", @@ -551,6 +560,7 @@ "./structures", "./structures/GeneratedBuilding", "./structures/PlacementGhost", + "./structures/TransformGizmo", "./structures/index", "./terrain", "./terrain/CarvedTerrain", @@ -610,26 +620,36 @@ "@jgengine/editor": [ ".", "./AssetBrowser", + "./CatalogsPanel", "./CollectionsPanel", "./DebugDraw", "./EditorApp", "./EditorCameraDriver", "./EditorChrome", + "./EditorContextMenu", "./InspectorPanel", "./MaterialDropZone", "./OutlinerPanel", "./PerfProbe", "./PrefabsPanel", + "./RuntimePlayBridge", "./ScatterPreview", "./SchemaInspector", "./SelectionGizmo", "./StandaloneEditor", "./TerrainPanel", "./TerrainSculpt", + "./TriggerInspector", + "./agent/AgentPanel", + "./agent/context", + "./agent/endpoint", + "./agent/toolBridge", + "./agent/turn", "./chromeFields", "./chromeStyles", "./index", "./mcp/bridgeServer", + "./mcp/loadGameCatalogs", "./mcp/rpcRequest", "./mcp/tools", "./outlinerModel", @@ -637,7 +657,8 @@ "./session", "./uiStore", "./useF2Chord", - "./useStoreSelector" + "./useStoreSelector", + "./viewportContextMenu" ], "@jgengine/assets": [ ".", diff --git a/scripts/gen-skill-api-safe.ts b/scripts/gen-skill-api-safe.ts index 14e8de911..7008275b0 100644 --- a/scripts/gen-skill-api-safe.ts +++ b/scripts/gen-skill-api-safe.ts @@ -35,7 +35,8 @@ function restore(): void { for (const [path, content] of before) writeFileSync(path, content); } -if (result.status !== 0) { +const generationCompleted = result.status === 0 || result.status === 2; +if (!generationCompleted) { restore(); process.exit(result.status ?? 1); } @@ -65,3 +66,8 @@ try { console.error(`skill-api generation validation failed: ${String(error)}`); process.exit(1); } + +if (result.status === 2) { + console.error("skill-api: generated files kept; gate failures above still need fixing"); + process.exit(2); +} diff --git a/scripts/gen-skill-api.ts b/scripts/gen-skill-api.ts index 557f4520b..3665a0ef0 100644 --- a/scripts/gen-skill-api.ts +++ b/scripts/gen-skill-api.ts @@ -75,9 +75,10 @@ const ORPHAN_GATED_KINDS = new Set(["function", "class"]); function collectOrphans(root: string, skills: SkillModules): string[] { const adoption = collectAdoption(root); + const tokens = new Set(); + for (const skill of SKILL_DIRS) for (const t of collectSkillTokens(root, skill)) tokens.add(t); const orphans: string[] = []; - for (const [skill, refs] of skills) { - const tokens = collectSkillTokens(root, skill); + for (const refs of skills.values()) { for (const ref of refs) { if (adoption.namespaceModules.has(ref.importPath)) continue; for (const e of ref.exports) { @@ -96,6 +97,15 @@ function main(): void { const root = fileURLToPath(new URL("..", import.meta.url)); const failures: string[] = []; + const extractedPackages = ["core", ...Object.keys(PACKAGE_SKILLS)]; + const missingDist = extractedPackages.filter((pkg) => !existsSync(join(root, "packages", pkg, "dist"))); + if (missingDist.length > 0) { + console.error( + `skill-api refused: missing dist for ${missingDist.join(", ")} — run \`bun run build\` first (extraction silently drops modules that resolve through unbuilt packages)`, + ); + process.exit(1); + } + const { skills, undocumented } = collectSkillModules(root); const undocumentedSet = new Set(undocumented); const baseline = readBaseline(root, BASELINE_PATH); @@ -176,7 +186,7 @@ function main(): void { if (failures.length > 0) { console.error(`\ncheck-skill-api failed:\n${failures.map((f) => ` ${f}`).join("\n")}\n`); - process.exit(1); + process.exit(check ? 1 : 2); } const total = [...skills.values()].reduce((n, m) => n + m.length, 0); console.log(