@jgengine/convex 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ const session = resolveConvexMultiplayer({
19
19
  <GamePlayerShell playable={playable} multiplayer={session} />;
20
20
  ```
21
21
 
22
- `resolveShellMultiplayer` (`@jgengine/shell/multiplayer`) is the ws counterpart — a dev host can try both in sequence (`resolveConvexMultiplayer(...) ?? resolveShellMultiplayer(...)`, see `apps/dev/src/main.tsx`) and pass whichever resolves to the shell.
22
+ `resolveShellMultiplayer` (`@jgengine/core/multiplayer/multiplayer.draw`) is the ws counterpart — a dev host can try both in sequence (`resolveConvexMultiplayer(...) ?? resolveShellMultiplayer(...)`, see `apps/dev/src/main.tsx`) and pass whichever resolves to the shell.
23
23
 
24
24
  Once a session resolves, `GamePlayerShell` wires it up with no game code changes: pose presence (`RemotePlayers`), `feedActions` (default `entity.died`) bridged both ways with echo suppression, and `global`-kind chat channels relayed through `chatSyncFor` (`whisper`/`party`/`proximity` stay local). Everything else in `GameContext` stays client-local unless the game also registers a server-side `GameRuntime`.
25
25
 
@@ -1,6 +1,6 @@
1
1
  import type { FunctionReference } from "convex/server";
2
- import type { ChatMessage } from "@jgengine/core/game/chat";
3
- import type { ChatTransport } from "@jgengine/core/multiplayer/chatContract";
2
+ import type { ChatMessage } from "@jgengine/core/chat/chat";
3
+ import type { ChatTransport } from "@jgengine/core/chat/chatContract";
4
4
  export interface ConvexChatFunctions {
5
5
  messages: FunctionReference<"query">;
6
6
  sendMessage: FunctionReference<"mutation">;
@@ -1,10 +1,10 @@
1
1
  import type { ConvexReactClient } from "convex/react";
2
2
  import type { DefaultFunctionArgs, FunctionReference } from "convex/server";
3
3
  import type { GameRuntimeFeeds, JoinServerOutcome, GameRuntimeTransport, PresencePoseRow, PresenceSync } from "@jgengine/core/runtime/transport";
4
- import type { ChatMessage } from "@jgengine/core/game/chat";
5
- import type { ChatSendOutcome, ChatSync } from "@jgengine/core/multiplayer/chatContract";
4
+ import type { ChatMessage } from "@jgengine/core/chat/chat";
5
+ import type { ChatSendOutcome, ChatSync } from "@jgengine/core/chat/chatContract";
6
6
  import { type PoseSyncTuning } from "@jgengine/core/multiplayer/poseSyncGate";
7
- import type { LeaderboardScope } from "@jgengine/core/game/leaderboard";
7
+ import type { LeaderboardScope } from "@jgengine/core/leaderboard/leaderboard";
8
8
  /** Structural client seam avoids requiring the engine and game to share a class instance type. */
9
9
  export type ConvexGameClient = Pick<ConvexReactClient, "mutation" | "watchQuery">;
10
10
  export type ConvexGameTransportConfig = {
package/dist/server.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import type { Auth, FunctionReference, DataModelFromSchemaDefinition, DocumentByName, GenericMutationCtx, GenericQueryCtx } from "convex/server";
2
2
  import type { GenericId } from "convex/values";
3
- import { type ChatMessage } from "@jgengine/core/game/chat";
4
- import type { ChatSendOutcome } from "@jgengine/core/multiplayer/chatContract";
3
+ import { type ChatMessage } from "@jgengine/core/chat/chat";
4
+ import type { ChatSendOutcome } from "@jgengine/core/chat/chatContract";
5
5
  import { type PoseSyncRules } from "@jgengine/core/multiplayer/presenceModel";
6
- import type { CommandScope } from "@jgengine/core/runtime/commandRunner";
6
+ import type { CommandScope } from "@jgengine/core/commands/commandRunner";
7
7
  import type { GameRuntime } from "@jgengine/core/runtime/gameRuntime";
8
+ import type { RunCommandArgs } from "@jgengine/core/runtime/transport";
8
9
  import type { LeaderboardIncrement } from "@jgengine/core/runtime/hostPersistence";
9
10
  import { type MatchmakingMode } from "@jgengine/core/runtime/hostPolicy";
10
11
  import type { SaveConfig } from "@jgengine/core/save/saveConfig";
@@ -581,8 +582,6 @@ export declare function resolveActor(ctx: {
581
582
  }, claimedExternalId: string | undefined, mode: JgAuthMode): Promise<string | null>;
582
583
  /** Most live client sessions one shared-topology membership remembers; the oldest drop first. */
583
584
  export declare const MAX_MEMBER_SESSIONS = 16;
584
- /** Longest accepted client `sessionId`. */
585
- export declare const MAX_SESSION_ID_LENGTH = 128;
586
585
  /**
587
586
  * How much of a server to hydrate. Both fields default to "everything", which is a document read per
588
587
  * member plus one per chunk — the cost that makes a large shared world unaffordable per mutation.
@@ -626,10 +625,7 @@ export type RunCommandOutcome = {
626
625
  reason: string;
627
626
  };
628
627
  /** Arguments of the `runCommand` mutation, and of the equivalent {@link GameServerHelpers.runCommand} helper. */
629
- export type RunCommandArgs = {
630
- serverId: string;
631
- command: string;
632
- input: unknown;
628
+ export type ConvexRunCommandArgs = RunCommandArgs & {
633
629
  externalId?: string;
634
630
  /**
635
631
  * How much of the server to hydrate before applying the command. Overrides whatever the
@@ -687,7 +683,7 @@ export type GameServerHelpers = {
687
683
  * Apply a command as the actor. Refuses non-members with "Not a member of this server"; host
688
684
  * mutations acting for a user who might have no live session call {@link ensureJoined} first.
689
685
  */
690
- runCommand: (ctx: JGMutationCtx, args: RunCommandArgs) => Promise<RunCommandOutcome>;
686
+ runCommand: (ctx: JGMutationCtx, args: ConvexRunCommandArgs) => Promise<RunCommandOutcome>;
691
687
  /**
692
688
  * Join a trusted, already-authorized actor through the same path as the `joinServer` mutation.
693
689
  * Idempotent: an existing member of `serverId` returns at once. Pins no client session, so it never
package/dist/server.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { defineSchema, defineTable, internalMutationGeneric, mutationGeneric, queryGeneric, } from "convex/server";
2
2
  import { ConvexError, v } from "convex/values";
3
- import { validateChatMessage } from "@jgengine/core/game/chat";
3
+ import { validateChatMessage } from "@jgengine/core/chat/chat";
4
4
  import { createFeedWriteGate, validateFeedWrite, } from "@jgengine/core/multiplayer/feedWriteGate";
5
5
  import { DEFAULT_POSE_SYNC_RULES, decidePoseSync, isPresenceExpired, pickReusablePresence, resolveActivePresence, spawnPresenceState, } from "@jgengine/core/multiplayer/presenceModel";
6
- import { SERVER_ONLY_COMMAND_REASON, commandScopeEscape, scopeWithActor } from "@jgengine/core/runtime/commandRunner";
6
+ import { SERVER_ONLY_COMMAND_REASON, commandScopeEscape, scopeWithActor } from "@jgengine/core/commands/commandRunner";
7
7
  import { createGameRuntime, initialPlayerState } from "@jgengine/core/runtime/gameRuntime";
8
8
  import { buildHydratePlayers, planServerPersist, shouldAutoSave, trimFeedEntries, } from "@jgengine/core/runtime/hostPersistence";
9
- import { JG_MAX_MEMBERS_PER_SERVER, isListablePublicly, isPrivateJoinBlocked, isServerFull, isServerMember, selectJoinTarget, statusAfterLeave, validateSlotsPerServer, withJoinedMember, withoutMember, } from "@jgengine/core/runtime/hostPolicy";
9
+ import { JG_MAX_MEMBERS_PER_SERVER, isListablePublicly, isPrivateJoinBlocked, isServerFull, isServerMember, selectJoinTarget, statusAfterLeave, validateSlotsPerServer, withJoinedMember, withoutMember, MAX_SESSION_ID_LENGTH, } from "@jgengine/core/runtime/hostPolicy";
10
10
  import { createEmptyServerRow } from "@jgengine/core/runtime/snapshot";
11
11
  import { chunkKeyAt, parseChunkKey, chunkKeysAround, DEFAULT_CHUNK_SIZE } from "@jgengine/core/runtime/worldChunks";
12
12
  import { applyCommandWithOcc, commitIfRevisionMatch } from "./occ.js";
@@ -203,8 +203,6 @@ async function memberRow(ctx, serverId, userId) {
203
203
  }
204
204
  /** Most live client sessions one shared-topology membership remembers; the oldest drop first. */
205
205
  export const MAX_MEMBER_SESSIONS = 16;
206
- /** Longest accepted client `sessionId`. */
207
- export const MAX_SESSION_ID_LENGTH = 128;
208
206
  function withSession(sessionIds, sessionId) {
209
207
  return [...(sessionIds ?? []).filter((id) => id !== sessionId), sessionId].slice(-MAX_MEMBER_SESSIONS);
210
208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/convex",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "Convex adapters for JGengine: game transport, presence transport, and backend wiring over @jgengine/core.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "check-types": "tsgo --noEmit -p tsconfig.json"
31
31
  },
32
32
  "dependencies": {
33
- "@jgengine/core": "^0.45.0"
33
+ "@jgengine/core": "^0.46.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "convex": "^1.31.2",
@@ -40,9 +40,9 @@ Treat the pitch as a unique composition of needs, not a genre to fill in — nev
40
40
 
41
41
  Pick the dimension first: `create` defaults to a 3D world (terrain, sky, player model, editor scene); a grid, card, tile, or other board game with no 3D scene takes `create --template 2d` (a `board` world drawn by GameUI over `@jgengine/core/puzzle/cellGrid`). `create --template 3d|2d|builder` is the one way to start a game, in this monorepo too (`bun run jgengine create "<Title>"` lands in `games/`); never copy `games/*`. The game's `jgengine.json` says what it is and uses (template, editor, `@jgengine` packages, and in-repo its capability answers). Change what a game uses by editing it, then run `jgengine sync`, which also brings a game up to the running engine: its `@jgengine` versions and its template's shared files (wiring, page, tooling scripts, AGENTS.md). A shared file the game rewrote goes in its `owns` list so sync leaves it alone.
42
42
 
43
- For an existing game asked to pick up a new engine release ("jgengine published a new version, grab it"), run `npx jgengine upgrade` in the project first: it diffs installed `@jgengine/*` versions against the latest release and prints every Migrate step and Adopt-worthy addition in between (`--json` for structured output). Bump, migrate oldest-first, then work the Adopt lists instead of reconstructing the changelog by hand.
43
+ For an existing game asked to pick up a new engine release ("jgengine published a new version, grab it"), run `npx jgengine@latest sync` in the project (or at its workspace root): it moves every `@jgengine` pin and catalog entry to that release, refreshes the template's files, and prints every Migrate step and Adopt-worthy addition in between. Install, migrate oldest-first, then work the Adopt lists instead of reconstructing the changelog by hand.
44
44
 
45
- `doctor` and `upgrade` resolve Bun `catalog:` and `catalog:<name>` declarations from the nearest workspace's `workspaces.catalog` or `workspaces.catalogs`, and follow hoisted SDK installs. Missing or malformed catalog entries fail diagnostics. `upgrade` is read-only by default. For catalog-owned versions, run `upgrade --plan --to <published-version>` at the workspace root or in a game, review the whole root catalog plan and migrations, then run `upgrade --apply --to <published-version>`. Apply preserves per-game `catalog:` declarations, unrelated text, CLI/github versions, and a held editor override; it verifies published SDK targets and refuses files changed while planning. Apply changes declarations only: run `bun install` at the root, then `jgengine doctor --workspace --json` once there to check installed SDK identities across declared workspace owners. Rebuild, migrate, and verify game behavior afterward. `--json` includes exact before/after text; an apply prints its plan before writing and reports write status on stderr. A report without installed packages labels its resolved pins as a declared baseline.
45
+ `doctor` resolves Bun `catalog:` and `catalog:<name>` declarations from the nearest workspace's `workspaces.catalog` or `workspaces.catalogs` and follows hoisted SDK installs; missing or malformed catalog entries fail diagnostics. `sync` at a workspace root moves the catalog pins (preserving per-game `catalog:` declarations, CLI/github versions and a held editor override) and changes declarations only: run `bun install` at the root, then `jgengine doctor --workspace --json` once there.
46
46
 
47
47
  When a linked consumer renders differently despite matching root SDK versions, `doctor` checks direct consumers and SDK dependency/peer paths, including private copies with the same version. Review the reported importer and real paths, align the shared catalog and linked-consumer declarations, then reinstall at the root and rerun `doctor --workspace`; a root install may leave private linked-package installs intact. Restart running games after reinstall. Workspace mode checks installed identities only, reports coverage, and refuses unsupported patterns, escaped symlinks or exhausted traversal budgets. The default project audit stays scoped; neither mode covers custom bundler aliases or indirect non-SDK chains.
48
48
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
8
8
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
9
- - `VERSION` (const): const VERSION: "0.45.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.46.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
10
10
 
11
11
  ## @jgengine/core/authoring
12
12
 
@@ -35,7 +35,7 @@
35
35
  - `SpawnPointDistanceBias` (type): type SpawnPointDistanceBias = "near" | "far" | "none" — Preference for picking a spawn point relative to `avoid` positions: closer, farther, or unweighted.
36
36
  - `SpawnPointSelectionOptions` (interface): interface SpawnPointSelectionOptions — Semantic options for selecting a spawn point without exposing weighting internals.
37
37
  - `TransientProvenance` (interface): interface TransientProvenance — An ephemeral simulation object — a spawned agent, projectile, or particle that never persists.
38
- - `WorldFeature` (type): type WorldFeature = | PlaceWorldFeature | ({ kind: "biomes" } & BiomesWorldConfig) | ({ kind: "voxel" } & VoxelWorldConfig) | ({ kind: "plots" } & PlotsWorldConfig) | ({ kind: "tilemap" } & TilemapWorldConfig) | EnvironmentWorldFeature | { kind: "flat" } — A declared world shape passed to `defineGame`. The preferred model is the place feature from `world()` (`@jgengine/core/world/place`): substrate + laws, with all dressing authored in the editor. The remaining members — biomes, voxel grid, plots, tilemap, environment, flat — are the legacy code-declared shapes kept for existing games.
38
+ - `WorldFeature` (type): type WorldFeature = | PlaceWorldFeature | ({ kind: "biomes" } & BiomesWorldConfig) | ({ kind: "voxel" } & VoxelWorldConfig) | ({ kind: "plots" } & PlotsWorldConfig) | ({ kind: "tilemap" } & TilemapWorldConfig) | EnvironmentWorldFeature | { kind: "flat" } — A declared world shape passed to `defineGame`. The preferred model is the place feature from `world()` (`@jgengine/core/placement/place`): substrate + laws, with all dressing authored in the editor. The remaining members — biomes, voxel grid, plots, tilemap, environment, flat — are the legacy code-declared shapes kept for existing games.
39
39
  - `auditManifest` (function): function auditManifest(manifest: SceneOwnershipManifest): OwnershipDiagnostic[] — Audit every declaration in a manifest, keying diagnostics by each object's provenance id. An empty result means the manifest declares all of its content cleanly — every object is authored, generated, bakeable, or a reasoned runtime/transient object.
40
40
  - `biomes` (function): function biomes(config: BiomesWorldConfig): WorldFeature — Declares a biome-painted world — the whole-world alternative to a single `environment()` terrain.
41
41
  - `building` (function): function building(config: BuildingEnvironmentConfig = {}): BuildingEnvironmentDescriptor — Declares a cluster of procedurally-massed buildings for `environment()` — count, footprint, stories, style. Pass `along` to line road frontage instead of gridding around `position`.
@@ -59,6 +59,24 @@
59
59
  - `tilemap` (function): function tilemap(config: TilemapWorldConfig): WorldFeature — Declares a 2D tilemap world from a map string.
60
60
  - `voxel` (function): function voxel(config: VoxelWorldConfig): WorldFeature — Declares a voxel-grid world for block-based games.
61
61
 
62
+ ## @jgengine/core/commands/commandDispatch
63
+
64
+ - `DispatchedCommandResult` (type): type DispatchedCommandResult = CommandResult<GameContext> | Promise<CommandResult<GameContext>> — A UI command completes locally or after its authoritative host acknowledges it.
65
+ - `bindCommandTransport` (function): function bindCommandTransport(ctx: GameContext, route: Route): () => void — Bind UI command dispatch to the host for this context. A missing join rejects; it never mutates the local replica.
66
+ - `dispatchCommand` (function): function dispatchCommand(ctx: GameContext, name: string, input: unknown): DispatchedCommandResult — Dispatch a player's command through the bound host, or locally for an unbound offline context. Await the result before showing success.
67
+
68
+ ## @jgengine/core/commands/commandInput
69
+
70
+ - `QuantityResult` (type): type QuantityResult = { ok: true; quantity: number } | { ok: false; reason: "invalid-quantity" } — Validated integer quantity or a stable input rejection.
71
+ - `isInputRecord` (function): function isInputRecord(value: unknown): value is Record<string, unknown> — Narrow an untrusted value to a plain object, rejecting arrays and null.
72
+ - `readInputArray` (function): function readInputArray<T>(value: unknown, readItem: (item: unknown) => T | null, options: { maxItems?: number } = {}): T[] | null — Read an array of at most `maxItems` entries, each accepted by `readItem`; any rejected entry rejects the whole array.
73
+ - `readInputNumber` (function): function readInputNumber(value: unknown, options: { min?: number; max?: number; integer?: boolean } = {}): number | null — Read a finite number inside `[min, max]`, or `null`. `NaN` and `±Infinity` are always rejected, so a coordinate can never drive an unbounded loop; `integer` also rejects fractions instead of flooring them.
74
+ - `readInputOneOf` (function): function readInputOneOf<const T extends string | number>(value: unknown, allowed: readonly T[]): T | null — Read one of a fixed set of values, or `null`.
75
+ - `readInputPoint2` (function): function readInputPoint2(value: unknown, options: { maxAbs?: number } = {}): { x: number; z: number } | null — Read an `{ x, z }` ground point whose axes are finite and within `maxAbs`, or `null`.
76
+ - `readInputPoint3` (function): function readInputPoint3(value: unknown, options: { maxAbs?: number } = {}): { x: number; y: number; z: number } | null — Read an `{ x, y, z }` point whose axes are finite and within `maxAbs`, or `null`.
77
+ - `readInputString` (function): function readInputString(value: unknown, options: { maxLength?: number } = {}): string | null — Read a non-empty string of at most `maxLength` characters, or `null`.
78
+ - `readQuantity` (function): function readQuantity(value: unknown, options: { min?: number; max?: number } = {}): QuantityResult — Read an untrusted whole-item quantity without coercion, truncation, or clamping.
79
+
62
80
  ## @jgengine/core/commands/commandRegistry
63
81
 
64
82
  - `CommandDecodeResult` (type): type CommandDecodeResult<TInput> = | { ok: true; value: TInput } | { ok: false; reason: string } — ⚠ undocumented
@@ -69,6 +87,87 @@
69
87
  - `CommandResult` (type): type CommandResult<TState> = | { status: "applied"; state: TState } | { status: "rejected"; reason: string; message?: string } | { status: "unknown-command" } — ⚠ undocumented
70
88
  - `createCommandRegistry` (function): function createCommandRegistry<TState>(): CommandRegistry<TState> — ⚠ undocumented
71
89
 
90
+ ## @jgengine/core/commands/commandRunner
91
+
92
+ - `CommandAccess` (type): type CommandAccess = "client" | "server" — Who may send a command: any client through the public transport, or only trusted host code.
93
+ - `CommandDef` (type): type CommandDef<TInput = unknown> = { /** * `"server"` when `input` carries facts only the host may decide (unlock state, prices, multipliers): * public transports then refuse it and only trusted host calls such as `helpers.runCommand` run it. * Defaults to `"client"`, so every field of `input` must… — ⚠ undocumented · used by `defineCommand`: Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and `apply` receive the parsed type.
94
+ - `CommandScope` (type): type CommandScope = { /** Member ids to hydrate; omit for the whole roster. */ players?: readonly string[]; /** Chunk keys to hydrate; omit for every chunk of the server, `[]` for none. */ chunkKeys?: readonly string[]; } — Which slice of a server has to be hydrated before some work runs against it. Both fields default to "everything", which costs a read per member plus one per chunk — the cost that makes a large shared world unaffordable per mutation. Narrow them when the caller knows what it will touch.
95
+ - `CommandScopeDefinition` (type): type CommandScopeDefinition = Omit<CommandScope, "players"> & { players?: readonly string[] | "actor" } — Declarative read scope; actor is resolved before host hydration.
96
+ - `CommandValidationError` (type): type CommandValidationError = { reason: string; message?: string } — `reason` is a stable machine code; `message`, when set, is player-facing text the client shows on refusal.
97
+ - `MALFORMED_COMMAND_INPUT_REASON` (const): const MALFORMED_COMMAND_INPUT_REASON: "Malformed command input" — Reason every host returns when a command's `parse` rejects its input.
98
+ - `RunCommandResult` (type): type RunCommandResult = | { ok: true; snapshot: GameRuntimeSnapshot } | { ok: false; reason: string; message?: string } — ⚠ undocumented
99
+ - `SERVER_ONLY_COMMAND_REASON` (const): const SERVER_ONLY_COMMAND_REASON: "Command is server-only" — Reason a public transport returns for a {@link CommandDef} marked `access: "server"`.
100
+ - `defineCommand` (function): function defineCommand<TInput>(definition: CommandDef<TInput> & { parse: (input: unknown) => TInput | null }): CommandDef — Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and `apply` receive the parsed type. Returns the untyped {@link CommandDef} a runtime's command table holds.
101
+
102
+ ## @jgengine/core/commands/commandSink
103
+
104
+ - `CommandSink` (interface): interface CommandSink — Where a gameplay command goes when the shell dispatches it — run locally, or sent to the authoritative host.
105
+
106
+ ## @jgengine/core/devtools/ColPanel.view
107
+
108
+ - `ColPanel` (function): function ColPanel(): React.JSX.Element — ⚠ undocumented
109
+
110
+ ## @jgengine/core/devtools/CollisionDebugWorld.draw
111
+
112
+ - `CollisionDebugWorld` (function): function CollisionDebugWorld(): React.JSX.Element | null — World-space collision debugger. Performs zero scene scans and zero raycasts when every layer is off. Mount only when shell devtools are enabled.
113
+
114
+ ## @jgengine/core/devtools/FpsCounter.draw
115
+
116
+ - `FrameTally` (interface): interface FrameTally — Mutable frame tally shared between the in-Canvas probe and the DOM readout.
117
+
118
+ ## @jgengine/core/devtools/KeysPanel.view
119
+
120
+ - `KeysPanel` (function): function KeysPanel({ input }: { input: ActionCodesMap | undefined }): React.JSX.Element — ⚠ undocumented
121
+
122
+ ## @jgengine/core/devtools/LogsPanel.view
123
+
124
+ - `LogsPanel` (function): function LogsPanel(): React.JSX.Element — ⚠ undocumented
125
+
126
+ ## @jgengine/core/devtools/NetPanel.draw
127
+
128
+ - `NetPanel` (function): function NetPanel({ multiplayer }: { multiplayer: ShellMultiplayer | null }): React.JSX.Element — ⚠ undocumented
129
+
130
+ ## @jgengine/core/devtools/PerfPanel.view
131
+
132
+ - `PerfPanel` (function): function PerfPanel({ ctx }: { ctx: GameContext }): React.JSX.Element — ⚠ undocumented
133
+
134
+ ## @jgengine/core/devtools/TunePanel.view
135
+
136
+ - `TunePanel` (function): function TunePanel({ gameName }: { gameName: string }): React.JSX.Element — ⚠ undocumented
137
+ - `tuneControlLabels` (function): function tuneControlLabels(name: string): { range: string; number: string } — Accessible names for one tunable's inputs: the number field answers to the control name itself, so `drive --fill "<name>=<value>"` and a screen reader both find it; the range is `<name> slider`.
138
+
139
+ ## @jgengine/core/devtools/agentBridge.draw
140
+
141
+ - `AgentBridgeRequest` (type): type AgentBridgeRequest = { method: string } & Record<string, unknown> — One RPC call into the agent bridge: a verb name plus its verb-specific fields.
142
+ - `AgentBridgeResponse` (type): type AgentBridgeResponse = | { ok: true; [key: string]: unknown } | { ok: false; error: string } — Result of an agent bridge call: `ok: true` with verb payload, or `ok: false` with a reason.
143
+
144
+ ## @jgengine/core/devtools/collisionDebug
145
+
146
+ - `AimProbeConfig` (interface): interface AimProbeConfig { from: string; aim: Aim; originPolicy?: ShotOriginPolicy; maxDistance?: number } — ⚠ undocumented
147
+ - `COLLISION_DEBUG_LAYERS` (const): const COLLISION_DEBUG_LAYERS: readonly CollisionDebugLayer[] — ⚠ undocumented
148
+ - `CollisionDebugController` (interface): interface CollisionDebugController { getState(): CollisionDebugState; subscribe(listener: CollisionDebugListener): () => void; setLayer(layer: CollisionDebugLayer, on: boolean): void; toggleLayer(layer: CollisionDebugLayer): void; setLayers(partial: Partial<CollisionDebugLay… — ⚠ undocumented
149
+ - `CollisionDebugLayer` (type): type CollisionDebugLayer = | "hitboxes" | "bodies" | "projectiles" | "muzzles" | "aimLaser" — ⚠ undocumented
150
+ - `CollisionDebugLayers` (type): type CollisionDebugLayers = Record<CollisionDebugLayer, boolean> — ⚠ undocumented
151
+ - `CollisionDebugListener` (type): type CollisionDebugListener = () => void — ⚠ undocumented
152
+ - `CollisionDebugState` (interface): interface CollisionDebugState { layers: CollisionDebugLayers; aimProbe: AimProbeConfig | null; projectileTraces: readonly ProjectileDebugTrace[]; maxProjectileTraces: number; projectileTraceLifeMs: number } — ⚠ undocumented
153
+ - `ProjectileDebugTrace` (interface): interface ProjectileDebugTrace { id: number; origin: EntityPosition; at: EntityPosition; hit: boolean; bornMs: number } — ⚠ undocumented
154
+ - `collisionDebug` (const): const collisionDebug: CollisionDebugController — ⚠ undocumented
155
+
156
+ ## @jgengine/core/devtools/collisionDebugMath
157
+
158
+ - `AIM_DAMAGE_COLOR` (const): const AIM_DAMAGE_COLOR: "#f87171" — ⚠ undocumented
159
+ - `AIM_LASER_COLOR` (const): const AIM_LASER_COLOR: "#a3e635" — ⚠ undocumented
160
+ - `AIM_MISS_COLOR` (const): const AIM_MISS_COLOR: "#94a3b8" — ⚠ undocumented
161
+ - `AIM_SOLID_COLOR` (const): const AIM_SOLID_COLOR: "#fbbf24" — ⚠ undocumented
162
+ - `AimEndpointKind` (type): type AimEndpointKind = "damage" | "solid" | "miss" — ⚠ undocumented
163
+ - `AimLaserDebug` (interface): interface AimLaserDebug { origin: EntityPosition; direction: EntityPosition; end: EntityPosition; maxDistance: number; kind: AimEndpointKind; nearest: SceneRaycastHit | null; firstImpact: SceneRaycastHit | null; queryCount: number } — ⚠ undocumented
164
+ - `BODY_WIRE_COLOR` (const): const BODY_WIRE_COLOR: "#38bdf8" — ⚠ undocumented
165
+ - `CollectDebugShapesInput` (interface): interface CollectDebugShapesInput { layers: CollisionDebugLayers; entities: readonly {id: string; position: EntityPosition; rotationY: number; name?: string;}[]; objects?: readonly {instanceId: string; catalogId: string; position: EntityPosition; rotationY: number;}[]; enti… — ⚠ undocumented
166
+ - `ComputeAimLaserInput` (interface): interface ComputeAimLaserInput { layers: CollisionDebugLayers; sceneRaycast: SceneRaycastApi; positionOf(instanceId: string): EntityPosition | undefined; rotationYOf?(instanceId: string): number | undefined; collidersOf?(instanceId: string): EntityColliderSet | null | un… — ⚠ undocumented
167
+ - `DebugShapeEntry` (interface): interface DebugShapeEntry { key: string; targetKind: "entity" | "object"; instanceId: string; catalogId?: string; name: string; purpose: ColliderPurpose; damageEligible: boolean; blocks: boolean; position: EntityPosition; rotationY: number; shape: | {kind: "sphere";… — ⚠ undocumented
168
+ - `HITBOX_WIRE_COLOR` (const): const HITBOX_WIRE_COLOR: "#f472b6" — ⚠ undocumented
169
+ - `PROJECTILE_PATH_COLOR` (const): const PROJECTILE_PATH_COLOR: "#fde68a" — ⚠ undocumented
170
+
72
171
  ## @jgengine/core/devtools/controls
73
172
 
74
173
  - `ControlRecord` (interface): interface ControlRecord — Internal mutable record backing a registered control: schema metadata, current value, and change listeners.
@@ -134,6 +233,23 @@
134
233
  - `snapshotDevtools` (function): function snapshotDevtools(): DevtoolsSnapshot — ⚠ undocumented
135
234
  - `tunable` (function): function tunable<T>(name: string, initial: T, options?: TunableOptions<T>): Tunable<T> — ⚠ undocumented
136
235
 
236
+ ## @jgengine/core/devtools/devtoolsOverrides.view
237
+
238
+ - `readStoredOverrides` (function): function readStoredOverrides(gameName: string): DevtoolsOverrides | null — ⚠ undocumented
239
+
240
+ ## @jgengine/core/devtools/devtoolsReports.draw
241
+
242
+ - `DebugSummary` (interface): interface DebugSummary — Compact `debug_summary` payload: the numbers an agent needs to judge a run, about 1 KB.
243
+
244
+ ## @jgengine/core/devtools/diagnosticOverlay.view
245
+
246
+ - `DiagnosticOverlay` (function): function DiagnosticOverlay({ diagnostics, gameName }: { diagnostics: RuntimeDiagnostic[]; gameName: string }): React.JSX.Element | null — ⚠ undocumented
247
+ - `ErrorReportActions` (function): function ErrorReportActions({ report, issueTitle }: { report: string; issueTitle: string }): React.JSX.Element — "Copy error" + "File issue" buttons for any error surface. `report` is the full text to copy / prefill.
248
+ - `GameUiErrorBoundary` (class): class GameUiErrorBoundary extends Component< { children: ReactNode; onRuntimeError: (error: unknown, phase: string, componentStack?: string) => void }, { failed: boolean } > — ⚠ undocumented
249
+ - `RuntimeDiagnostic` (interface): interface RuntimeDiagnostic { id: number; phase: string; message: string; stack?: string; componentStack?: string; capturedAt: string } — ⚠ undocumented
250
+ - `errorReportContext` (function): function errorReportContext(): string — Standalone error footer text (page, browser, engine) shared by every error surface.
251
+ - `logRuntimeError` (function): function logRuntimeError(error: unknown, phase: string, componentStack?: string): Omit<RuntimeDiagnostic, "id"> — ⚠ undocumented
252
+
137
253
  ## @jgengine/core/devtools/discover
138
254
 
139
255
  - `DiscoverModule` (interface): interface DiscoverModule — Discovery subsystem: the public discover and probes facades plus internal record map and helpers.
@@ -158,6 +274,16 @@
158
274
  - `FrameModule` (interface): interface FrameModule — Frame subsystem exposing the frame-timing, profiling, and render-sample facades.
159
275
  - `createFrameModule` (const): const createFrameModule: (deps: { readProbes: () => Record<string, unknown>; }) => FrameModule — Create the frame subsystem that records frame/sim/phase timings, computes stats, and captures long-frame events.
160
276
 
277
+ ## @jgengine/core/devtools/frameSampler.view
278
+
279
+ - `FrameReportInputs` (interface): interface FrameReportInputs — Everything the report needs, gathered by whichever producer is asking.
280
+ - `FrameSampler` (interface): interface FrameSampler — The installed sampler: ask it for a report, reset its window, or tear it down.
281
+ - `FrameTelemetrySources` (interface): interface FrameTelemetrySources — What the shell hands the sampler: live getters, called only when a report is built.
282
+ - `GlCounterTotals` (interface): interface GlCounterTotals — GPU-object creation counts, the fields the budget gate compares.
283
+ - `REPORT_PUSH_MS` (const): const REPORT_PUSH_MS: 1000 — Cadence of the push to the dev server, in milliseconds.
284
+ - `TELEMETRY_FLAG` (const): const TELEMETRY_FLAG: "telemetry" — In-page frame telemetry: the game reports on itself instead of needing a second process to attach a profiler. Frame timings come from the devtools frame ring, renderer counts from the live renderer, GPU-object creation from wrapped GL calls, and per-system numbers from `devtools.probes` — all read on demand, so nothing here runs on the hot path.
285
+ - `TELEMETRY_WARMUP_MS` (const): const TELEMETRY_WARMUP_MS: 5000 — How long after the sampler starts the GL counters get their post-warmup baseline.
286
+
161
287
  ## @jgengine/core/devtools/logBus
162
288
 
163
289
  - `LOG_BUS_CAPACITY` (const): const LOG_BUS_CAPACITY: 200 — Default measured window for the log ring; older lines are evicted, never grown.
@@ -181,6 +307,15 @@
181
307
  - `OverridesModule` (interface): interface OverridesModule — Overrides subsystem exposing the export/apply facade for persisting and restoring control values.
182
308
  - `createOverridesModule` (const): const createOverridesModule: (deps: { signal: ChangeSignal; controlRecords: Map<string, ControlRecord>; writeControl: (record: ControlRecord, raw: unknown) => boolean; discoveredRecords: Map<string, DiscoveredRecord>; enableDiscovered: (id: string) => void; }) => OverridesModule — Create the overrides subsystem that serializes changed control values and re-applies a saved overrides payload.
183
309
 
310
+ ## @jgengine/core/devtools/panelAtoms.view
311
+
312
+ - `SectionLabel` (function): function SectionLabel({ children }: { children: string }): React.JSX.Element — ⚠ undocumented
313
+ - `ms` (function): function ms(value: number): string — ⚠ undocumented
314
+
315
+ ## @jgengine/core/devtools/perfDiagnose.view
316
+
317
+ - `diagnose` (function): function diagnose(frame: NonNullable<ReturnType<typeof devtools.frame.stats>>, longs: readonly LongFrameEvent[]): string | null — ⚠ undocumented
318
+
184
319
  ## @jgengine/core/devtools/report
185
320
 
186
321
  - `FrameWindow` (interface): interface FrameWindow — Frame timings over the measured window, as the player sees them.
@@ -285,7 +420,7 @@
285
420
 
286
421
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
287
422
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
288
- - `VERSION` (const): const VERSION: "0.45.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
423
+ - `VERSION` (const): const VERSION: "0.46.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
289
424
 
290
425
  ## @jgengine/core/runtime/adapter
291
426
 
@@ -310,6 +445,15 @@
310
445
  - `ws` (function): function ws(config?: { topology?: MultiplayerTopology; url?: string; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — WebSocket transport. Omitting `authority` (or passing `"client"`) is **presence-only** — prefer `wsPresence()` to name that intent explicitly. Pass `{ authority: "server" }` for a shared, host-authoritative world — see `examples/HOSTED.md`.
311
446
  - `wsPresence` (function): function wsPresence(config?: { topology?: MultiplayerTopology; url?: string }): MultiplayerAdapterConfig — Presence-only WebSocket transport — each client runs its own `onTick`; only presence/feeds/chat sync. Sugar for `ws({ ...config, authority: "client" })`.
312
447
 
448
+ ## @jgengine/core/runtime/browserLifecycle.view
449
+
450
+ - `BrowserSuspensionOptions` (interface): interface BrowserSuspensionOptions — Select suspension events; all are observed by default. No callback runs on attachment.
451
+ - `BrowserSuspensionReason` (type): type BrowserSuspensionReason = "blur" | "hidden" | "pointer-lock-lost" — Browser events that can interrupt active play. The caller decides whether to pause.
452
+ - `BrowserSuspensionSources` (interface): interface BrowserSuspensionSources — Event sources for a browser window, an iframe, or a test.
453
+ - `CAPTURE_HOLD_KEY` (const): const CAPTURE_HOLD_KEY: "__jgengineCaptureHold" — Global a capture or drive host sets to `true` at document start. While it holds, window blur and a hidden page do not suspend play: a capture window loses focus and visibility whenever another window on the machine comes forward, and a drive that boots paused proves nothing.
454
+ - `observeBrowserSuspension` (function): function observeBrowserSuspension(onSuspend: (reason: BrowserSuspensionReason) => void, options: BrowserSuspensionOptions = {}, sources: BrowserSuspensionSources | null = typeof window === "undefined" || typeof document === "undefined" ? null : { window, document }): () => void — Observe interruption events until detached. Lock loss requires a preceding lock; SSR attaches nothing. Blur and hidden are ignored while a capture host holds play ({@link CAPTURE_HOLD_KEY}).
455
+ - `useBrowserSuspension` (function): function useBrowserSuspension(onSuspend: (reason: BrowserSuspensionReason) => void, options: BrowserSuspensionOptions = {}): void — Observe browser interruptions while mounted, invoking the latest callback without replacing listeners.
456
+
313
457
  ## @jgengine/core/runtime/cameraDirector
314
458
 
315
459
  - `CHASE_VIEWS` (const): const CHASE_VIEWS: readonly ChaseView[] — Default order {@link nextChaseView} cycles through.
@@ -321,36 +465,6 @@
321
465
  - `ChaseCameraTuning` (type): type ChaseCameraTuning = Partial< Pick< ChaseCameraConfig, | "distance" | "height" | "lookHeight" | "springDamping" | "fov" | "lead" | "bank" | "shakePerSpeed" | "velocityYaw" | "yawResponse" | "headingSource" | "distanceBySpeed" | "pitchFollow" | "fovKick" | "lookBackAction" | "collision" | "view" … — Runtime patch over the static `camera.chase` config — distance/height/fov retuning from gameplay events (#286.11), a whole driving-feel overlay applied only while a vehicle is piloted (#1299), or a `view` switch between chase and seat cameras.
322
466
  - `nextChaseView` (function): function nextChaseView(current: ChaseView, views: readonly ChaseView[] = CHASE_VIEWS): ChaseView — The view after `current` in `views`, wrapping at the end; a view missing from the list starts the cycle over. Pair with `setChaseTuning` to bind a "change camera" key: `ctx.camera.setChaseTuning({ ...ctx.camera.chaseTuning(), view: nextChaseView(view) })`.
323
467
 
324
- ## @jgengine/core/runtime/commandDispatch
325
-
326
- - `DispatchedCommandResult` (type): type DispatchedCommandResult = CommandResult<GameContext> | Promise<CommandResult<GameContext>> — A UI command completes locally or after its authoritative host acknowledges it.
327
- - `bindCommandTransport` (function): function bindCommandTransport(ctx: GameContext, route: Route): () => void — Bind UI command dispatch to the host for this context. A missing join rejects; it never mutates the local replica.
328
- - `dispatchCommand` (function): function dispatchCommand(ctx: GameContext, name: string, input: unknown): DispatchedCommandResult — Dispatch a player's command through the bound host, or locally for an unbound offline context. Await the result before showing success.
329
-
330
- ## @jgengine/core/runtime/commandInput
331
-
332
- - `QuantityResult` (type): type QuantityResult = { ok: true; quantity: number } | { ok: false; reason: "invalid-quantity" } — Validated integer quantity or a stable input rejection.
333
- - `isInputRecord` (function): function isInputRecord(value: unknown): value is Record<string, unknown> — Narrow an untrusted value to a plain object, rejecting arrays and null.
334
- - `readInputArray` (function): function readInputArray<T>(value: unknown, readItem: (item: unknown) => T | null, options: { maxItems?: number } = {}): T[] | null — Read an array of at most `maxItems` entries, each accepted by `readItem`; any rejected entry rejects the whole array.
335
- - `readInputNumber` (function): function readInputNumber(value: unknown, options: { min?: number; max?: number; integer?: boolean } = {}): number | null — Read a finite number inside `[min, max]`, or `null`. `NaN` and `±Infinity` are always rejected, so a coordinate can never drive an unbounded loop; `integer` also rejects fractions instead of flooring them.
336
- - `readInputOneOf` (function): function readInputOneOf<const T extends string | number>(value: unknown, allowed: readonly T[]): T | null — Read one of a fixed set of values, or `null`.
337
- - `readInputPoint2` (function): function readInputPoint2(value: unknown, options: { maxAbs?: number } = {}): { x: number; z: number } | null — Read an `{ x, z }` ground point whose axes are finite and within `maxAbs`, or `null`.
338
- - `readInputPoint3` (function): function readInputPoint3(value: unknown, options: { maxAbs?: number } = {}): { x: number; y: number; z: number } | null — Read an `{ x, y, z }` point whose axes are finite and within `maxAbs`, or `null`.
339
- - `readInputString` (function): function readInputString(value: unknown, options: { maxLength?: number } = {}): string | null — Read a non-empty string of at most `maxLength` characters, or `null`.
340
- - `readQuantity` (function): function readQuantity(value: unknown, options: { min?: number; max?: number } = {}): QuantityResult — Read an untrusted whole-item quantity without coercion, truncation, or clamping.
341
-
342
- ## @jgengine/core/runtime/commandRunner
343
-
344
- - `CommandAccess` (type): type CommandAccess = "client" | "server" — Who may send a command: any client through the public transport, or only trusted host code.
345
- - `CommandDef` (type): type CommandDef<TInput = unknown> = { /** * `"server"` when `input` carries facts only the host may decide (unlock state, prices, multipliers): * public transports then refuse it and only trusted host calls such as `helpers.runCommand` run it. * Defaults to `"client"`, so every field of `input` must… — ⚠ undocumented · used by `defineCommand`: Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and `apply` receive the parsed type.
346
- - `CommandScope` (type): type CommandScope = { /** Member ids to hydrate; omit for the whole roster. */ players?: readonly string[]; /** Chunk keys to hydrate; omit for every chunk of the server, `[]` for none. */ chunkKeys?: readonly string[]; } — Which slice of a server has to be hydrated before some work runs against it. Both fields default to "everything", which costs a read per member plus one per chunk — the cost that makes a large shared world unaffordable per mutation. Narrow them when the caller knows what it will touch.
347
- - `CommandScopeDefinition` (type): type CommandScopeDefinition = Omit<CommandScope, "players"> & { players?: readonly string[] | "actor" } — Declarative read scope; actor is resolved before host hydration.
348
- - `CommandValidationError` (type): type CommandValidationError = { reason: string; message?: string } — `reason` is a stable machine code; `message`, when set, is player-facing text the client shows on refusal.
349
- - `MALFORMED_COMMAND_INPUT_REASON` (const): const MALFORMED_COMMAND_INPUT_REASON: "Malformed command input" — Reason every host returns when a command's `parse` rejects its input.
350
- - `RunCommandResult` (type): type RunCommandResult = | { ok: true; snapshot: GameRuntimeSnapshot } | { ok: false; reason: string; message?: string } — ⚠ undocumented
351
- - `SERVER_ONLY_COMMAND_REASON` (const): const SERVER_ONLY_COMMAND_REASON: "Command is server-only" — Reason a public transport returns for a {@link CommandDef} marked `access: "server"`.
352
- - `defineCommand` (function): function defineCommand<TInput>(definition: CommandDef<TInput> & { parse: (input: unknown) => TInput | null }): CommandDef — Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and `apply` receive the parsed type. Returns the untyped {@link CommandDef} a runtime's command table holds.
353
-
354
468
  ## @jgengine/core/runtime/context/objectSlots
355
469
 
356
470
  - `ObjectSlotRejection` (type): type ObjectSlotRejection = | "unknown-object" | "no-slot-inventory" | "unknown-inventory" | "no-space" | "wrong-kind" | "slot-occupied" | "invalid-slot" | "insufficient" — Why an object slot mutation was refused — the instance, its catalog entry, the player inventory, or the declared `accepts`/capacity.
@@ -361,6 +475,7 @@
361
475
  ## @jgengine/core/runtime/gameContext
362
476
 
363
477
  - `CatalogEntityRole` (type): type CatalogEntityRole = "player" | "enemy" | "hostile" | "npc" | "vehicle" — ⚠ undocumented
478
+ - `EntityHitReactionInput` (interface): interface EntityHitReactionInput — A hit reaction between two entities by id, with a config or impact preset.
364
479
  - `FloatTextInput` (interface): interface FloatTextInput { instanceId?: string; position?: [number, number, number]; text?: string; kind?: string; amount?: number; hitType?: string; element?: string; crit?: boolean; scale?: number } — ⚠ undocumented
365
480
  - `GameAudio` (interface): interface GameAudio — Reachable audio seam on `ctx.game`: `play`, `music`, and `resume` route through the `audio.play`/`audio.music`/`audio.resume` events the shell's audio engine listens on, so game code triggers sound without importing the shell. Retained `loop`/`setLoop`/`stopLoop` add id-keyed loops with live pitch/gain control over `audio.loopStart`/`audio.loopSet`/`audio.loopStop` (#1051).
366
481
  - `GameContext` (interface): interface GameContext — The live engine handle a game's loop, systems, commands, and UI read and mutate — entities, objects, the reactive store, opted-in `game.*` subsystems, world queries, and the sim clock. One context = one running world.
@@ -379,7 +494,6 @@
379
494
  - `GameContextRace` (interface): interface GameContextRace { state(id: string, config?: RaceStateConfig): RaceState } — ⚠ undocumented
380
495
  - `GameContextTurn` (interface): interface GameContextTurn { loop(id: string, config?: TurnLoopConfig): TurnLoop } — ⚠ undocumented
381
496
  - `GameContextWorld` (interface): interface GameContextWorld { ground: TerrainField; groundHeightAt(x: number, z: number): number; solids: WorldSolids } — ⚠ undocumented
382
- - `HitReactionInput` (interface): interface HitReactionInput — ⚠ undocumented · used by `resolveHitReaction` (@jgengine/core/combat): Resolves hit feel (hitstop, knockback impulse, camera shake) from either a named `impactPresets` event (`resolveHitReaction("explosion", inp…
383
497
  - `MoveTowardCommitOptions` (interface): interface MoveTowardCommitOptions extends MoveTowardOptions — Options for {@link SceneEntityContext.moveTowardCommit}: {@link MoveTowardOptions} plus an optional facing turn.
384
498
  - `SceneEntityContext` (interface): interface SceneEntityContext { spawn(name: string, options?: SpawnOptions): string; despawn(instanceId: string): boolean; update: EntityStore["update"]; setPose(instanceId: string, pose: EntityPose): boolean; setVelocity: EntityStore["setVelocity"]; setPoseConstraint: … — ⚠ undocumented
385
499
  - `SceneObjectContext` (interface): interface SceneObjectContext extends ObjectStore { catalog(instanceId: string): GameContextObjectEntry | null; raycast(input: ObjectRaycastInput): ObjectRaycastHit | null; raycastAll(input: ObjectRaycastInput): readonly ObjectRaycastHit[]; setColliders(instanceId: string, colliders: Entit… — ⚠ undocumented
@@ -392,6 +506,7 @@
392
506
  ## @jgengine/core/runtime/gameContextTypes
393
507
 
394
508
  - `CatalogEntityRole` (type): type CatalogEntityRole = "player" | "enemy" | "hostile" | "npc" | "vehicle" — ⚠ undocumented
509
+ - `EntityHitReactionInput` (interface): interface EntityHitReactionInput — A hit reaction between two entities by id, with a config or impact preset.
395
510
  - `FloatTextInput` (interface): interface FloatTextInput { instanceId?: string; position?: [number, number, number]; text?: string; kind?: string; amount?: number; hitType?: string; element?: string; crit?: boolean; scale?: number } — ⚠ undocumented
396
511
  - `GameAudio` (interface): interface GameAudio — Reachable audio seam on `ctx.game`: `play`, `music`, and `resume` route through the `audio.play`/`audio.music`/`audio.resume` events the shell's audio engine listens on, so game code triggers sound without importing the shell. Retained `loop`/`setLoop`/`stopLoop` add id-keyed loops with live pitch/gain control over `audio.loopStart`/`audio.loopSet`/`audio.loopStop` (#1051).
397
512
  - `GameContext` (interface): interface GameContext — The live engine handle a game's loop, systems, commands, and UI read and mutate — entities, objects, the reactive store, opted-in `game.*` subsystems, world queries, and the sim clock. One context = one running world.
@@ -411,7 +526,6 @@
411
526
  - `GameContextTurn` (interface): interface GameContextTurn { loop(id: string, config?: TurnLoopConfig): TurnLoop } — ⚠ undocumented
412
527
  - `GameContextWorld` (interface): interface GameContextWorld { ground: TerrainField; groundHeightAt(x: number, z: number): number; solids: WorldSolids } — ⚠ undocumented
413
528
  - `GameFeedback` (interface): interface GameFeedback — The game-announced feel seam on `ctx.game`: name a moment the engine has no event for and the shell plays its cue's sound, rumble, camera kick and caption. The reaction table lives in the shell, so a game never encodes what a hit or a wave should sound like.
414
- - `HitReactionInput` (interface): interface HitReactionInput { from: string; to: string; config: HitReactionConfig | ImpactPresetName; power?: number } — ⚠ undocumented · used by `resolveHitReaction` (@jgengine/core/combat): Resolves hit feel (hitstop, knockback impulse, camera shake) from either a named `impactPresets` event (`resolveHitReaction("explosion", inp…
415
529
  - `MoveTowardCommitOptions` (interface): interface MoveTowardCommitOptions extends MoveTowardOptions — Options for {@link SceneEntityContext.moveTowardCommit}: {@link MoveTowardOptions} plus an optional facing turn.
416
530
  - `SceneEntityContext` (interface): interface SceneEntityContext { spawn(name: string, options?: SpawnOptions): string; despawn(instanceId: string): boolean; update: EntityStore["update"]; setPose(instanceId: string, pose: EntityPose): boolean; setVelocity: EntityStore["setVelocity"]; setPoseConstraint: … — ⚠ undocumented
417
531
  - `SceneObjectContext` (interface): interface SceneObjectContext extends ObjectStore { catalog(instanceId: string): GameContextObjectEntry | null; raycast(input: ObjectRaycastInput): ObjectRaycastHit | null; raycastAll(input: ObjectRaycastInput): readonly ObjectRaycastHit[]; setColliders(instanceId: string, colliders: Entit… — ⚠ undocumented
@@ -449,14 +563,13 @@
449
563
  - `LEADERBOARD_TOP_LIMIT` (const): const LEADERBOARD_TOP_LIMIT: 100 — ⚠ undocumented
450
564
  - `LeaderboardEntry` (type): type LeaderboardEntry = { userId: string; value: number; } — ⚠ undocumented
451
565
  - `LeaderboardIncrement` (type): type LeaderboardIncrement = { userId: string; stat: string; scope: LeaderboardScope; serverId?: string; by: number; } — ⚠ undocumented
452
- - `LeaderboardRow` (type): type LeaderboardRow = { gameId: string; stat: string; scope: LeaderboardScope; serverId?: string; userId: string; value: number; updatedAt: number; } — ⚠ undocumented · used by `createLeaderboard` (@jgengine/core/game/leaderboard): Ranked score tracking across global, server, and per-profile scopes, with top-N queries and per-profile lookups.
453
566
  - `OPEN_SERVER_LISTING_LIMIT` (const): const OPEN_SERVER_LISTING_LIMIT: 20 — ⚠ undocumented
454
567
  - `OPEN_SERVER_LISTING_MAX` (const): const OPEN_SERVER_LISTING_MAX: 100 — ⚠ undocumented
455
568
  - `PlayerProfileRecord` (type): type PlayerProfileRecord = { userId: string; gameId: string; playerState: RuntimePlayerRow; revision: number; updatedAt: number; } — ⚠ undocumented
456
569
  - `ServerListing` (type): type ServerListing = { serverId: string; status: GameServerStatus; memberCount: number; slotsPerServer: number; mode?: string; label?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; updatedAt: number; } — ⚠ undocumented
457
570
  - `ServerPersistPlan` (type): type ServerPersistPlan = { server: GameServerRecord; profiles: PlayerProfileRecord[]; chunks: WorldChunkRecord[]; deletedChunks: string[]; leaderboard: LeaderboardIncrement[]; /** * Which fields of the server row the plan actually moved. A store that pays per written field — * or whose subscribers r… — ⚠ undocumented
458
571
  - `SessionAttributes` (type): type SessionAttributes = { label?: string; mode?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; } — ⚠ undocumented
459
- - `SessionVisibility` (type): type SessionVisibility = "public" | "private" — ⚠ undocumented · used by `isAutoJoinCandidate` (@jgengine/core/runtime/hostPolicy): Auto-match candidate when no `serverId` is supplied: already a member, or a public room with free capacity.
572
+ - `StoredLeaderboardRow` (type): type StoredLeaderboardRow = LeaderboardRow & { gameId: string; updatedAt: number; } — A {@link LeaderboardRow} as a host stores it: keyed by game, stamped with its last write.
460
573
  - `ToServerListingOptions` (type): type ToServerListingOptions = { includeJoinCode?: boolean; } — ⚠ undocumented
461
574
  - `WorldChunkRecord` (type): type WorldChunkRecord = { serverId: string; chunkKey: string; snapshot: RuntimeChunkRow; updatedAt: number; } — ⚠ undocumented
462
575
  - `isSnapshotClean` (function): function isSnapshotClean(snapshot: GameRuntimeSnapshot): boolean — True when a snapshot carries no unwritten mutation — nothing for a persist to do.
@@ -466,6 +579,7 @@
466
579
  - `JG_MAX_MEMBERS_PER_SERVER` (const): const JG_MAX_MEMBERS_PER_SERVER: 256 — Room-topology ceiling on `slotsPerServer`; shared membership uses indexed rows instead.
467
580
  - `JoinCandidate` (type): type JoinCandidate = { memberUserIds: readonly string[]; slotsPerServer: number; visibility?: SessionVisibility | undefined; createdAt: number; } — A row `selectJoinTarget` can choose between — the matchmaking-relevant fields of a server record.
468
581
  - `JoinTarget` (type): type JoinTarget<T extends JoinCandidate> = | { kind: "join"; row: T } | { kind: "create" } | { kind: "refuse"; reason: string } — What a join should do with the candidate rows it found.
582
+ - `MAX_SESSION_ID_LENGTH` (const): const MAX_SESSION_ID_LENGTH: 128 — Longest accepted client join/leave `sessionId`, in UTF-16 code units.
469
583
  - `MatchmakingMode` (type): type MatchmakingMode = "auto" | "singleton" — How auto-match behaves when no `serverId` is supplied.
470
584
  - `canJoinPrivateServer` (function): function canJoinPrivateServer(args: { isMember: boolean; joinCode: string | undefined; suppliedCode: string | undefined; }): boolean — Private-server join-code gate. Existing members always pass; non-members must present a matching `joinCode` (loose-normalized via {@link normalizeJoinCode}). Callers still decide whether the server is private — this only answers the code/membership half.
471
585
  - `isAutoJoinCandidate` (function): function isAutoJoinCandidate(args: { memberUserIds: readonly string[]; slotsPerServer: number; visibility: SessionVisibility | undefined; userId: string; }): boolean — Auto-match candidate when no `serverId` is supplied: already a member, or a public room with free capacity. Private rooms are never auto-picked (join-by-code / direct id only).
@@ -481,7 +595,7 @@
481
595
 
482
596
  ## @jgengine/core/runtime/hostedGameRunner
483
597
 
484
- - `HostedGameRunner` (interface): interface HostedGameRunner — The GameContext-loop equivalent of the pure-reducer `createGameHost`: one authoritative `createGameContext` per world, driven server-side. `onInit` runs once at construction; `onNewPlayer`/`onPlayerLeave` fire per join/leave; `tick` advances `onTick` then commits a revision. Clients pull a full {@link WorldSnapshot} baseline once, then per-tick {@link WorldDiff}s from their last revision. Games ship only normal GameContext code — the runner adds no per-game surface.
598
+ - `HostedGameRunner` (interface): interface HostedGameRunner — The GameContext-loop equivalent of the pure-reducer `createGameServerHost`: one authoritative `createGameContext` per world, driven server-side. `onInit` runs once at construction; `onNewPlayer`/`onPlayerLeave` fire per join/leave; `tick` advances `onTick` then commits a revision. Clients pull a full {@link WorldSnapshot} baseline once, then per-tick {@link WorldDiff}s from their last revision. Games ship only normal GameContext code — the runner adds no per-game surface.
485
599
  - `HostedGameRunnerOptions` (interface): interface HostedGameRunnerOptions<TAssetRef extends ModelAssetRef, TMultiplayer> — Config for {@link createHostedGameRunner}: the game definition, its content lookup, and an optional host identity.
486
600
  - `INPUT_COMMAND` (const): const INPUT_COMMAND: "engine.input" — Reserved command name the authoritative host intercepts on the existing `runCommand` transport to route a client's {@link InputFrame} to `session.input`, so per-tick input needs no separate wire.
487
601
  - `InputFrame` (interface): interface InputFrame — One client's serializable held state and discrete edges for a simulation tick.
@@ -507,7 +621,7 @@
507
621
 
508
622
  - `InputFrame` (interface): interface InputFrame — One client's serializable held state and discrete edges for a simulation tick.
509
623
  - `InputPress` (interface): interface InputPress — One discrete press, identified across retransmitted live input frames.
510
- - `InputSnapshot` (interface): interface InputSnapshot { rumble(userId: string, options: {strong: number; weak: number; ms: number}): Promise<boolean>; haptics(userId: string): HapticChannels; publish(held: readonly string[], edges?: {pressed?: readonly string[]; reset?: boolean}): void; beginS… — ⚠ undocumented · used by `GamepadSource` (@jgengine/shell/input/gamepadSource): Poll browser gamepads and feed semantic actions into the shell tracker.
624
+ - `InputSnapshot` (interface): interface InputSnapshot { rumble(userId: string, options: {strong: number; weak: number; ms: number}): Promise<boolean>; haptics(userId: string): HapticChannels; publish(held: readonly string[], edges?: {pressed?: readonly string[]; reset?: boolean}): void; beginS… — ⚠ undocumented · used by `GamepadSource` (@jgengine/core/input/gamepadSource.draw): Poll browser gamepads and feed semantic actions into the shell tracker.
511
625
  - `createInputSnapshot` (function): function createInputSnapshot(): InputSnapshot — ⚠ undocumented
512
626
 
513
627
  ## @jgengine/core/runtime/localPlayers
@@ -616,13 +730,13 @@
616
730
  - `GameRuntimeFeeds` (type): type GameRuntimeFeeds = { subscribeServer: ( serverId: string, onChange: (view: GameRuntimeServerView | null) => void, ) => FeedUnsubscribe; subscribePlayer: ( args: { serverId: string }, onChange: (view: GameRuntimePlayerView | null) => void, ) => FeedUnsubscribe; subscribeFeed: ( args: { serverId:… — ⚠ undocumented
617
731
  - `GameRuntimePlayerView` (type): type GameRuntimePlayerView = { userId: string; gameId: string; playerState: unknown; updatedAt: number; } — ⚠ undocumented
618
732
  - `GameRuntimeServerView` (type): type GameRuntimeServerView = { serverId: string; gameId: string; revision: number; memberUserIds: string[]; serverState: unknown | WorldSyncFrame; updatedAt: number; } — ⚠ undocumented
619
- - `GameRuntimeTransport` (type): type GameRuntimeTransport = { /** * Join a server. `sessionId` names this client session (one tab, one mounted view) so the host keeps * the user a member while any of their sessions is still live. */ joinServer: (args: { gameId: string; serverId?: string; role?: MultiplayerRole; sessionId?: string … — ⚠ undocumented · used by `useServerSession` (@jgengine/react/useServerSession): Joins a host and exposes a retryable blocking state until membership is confirmed.
733
+ - `GameRuntimeTransport` (type): type GameRuntimeTransport = { /** * Join a server. `sessionId` names this client session (one tab, one mounted view) so the host keeps * the user a member while any of their sessions is still live. */ joinServer: (args: { gameId: string; serverId?: string; role?: MultiplayerRole; sessionId?: string … — ⚠ undocumented · used by `useServerSession` (@jgengine/core/session/useServerSession.view): Joins a host and exposes a retryable blocking state until membership is confirmed.
620
734
  - `JoinServerOutcome` (type): type JoinServerOutcome = | (JoinServerResult & { ok: true }) | { ok: false; reason: "full" | "closed" | "unauthorized" } — Join failures are returned to the caller so the triggering surface can offer retry.
621
735
  - `JoinServerResult` (type): type JoinServerResult = { serverId: string; isNew: boolean; resumeTicket?: ResumeTicket; } — ⚠ undocumented
622
736
  - `LiveGameBackend` (type): type LiveGameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = GameBackend<TPresenceRow, TPresenceLocation, TGameId> & { presenceSync: PresenceSync; pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise<void>; chatSyncFor… — ⚠ undocumented
623
737
  - `MultiplayerRole` (type): type MultiplayerRole = "player" | "spectator" — Connection role used for authoritative world access; spectators are read-only.
624
738
  - `MultiplayerSession` (type): type MultiplayerSession = { gameId: string; userId: string; backend: LiveGameBackend; feedActions: string[]; } — ⚠ undocumented
625
- - `PresencePoseRow` (type): type PresencePoseRow = { userId: string; /** Set when the host tracks presence per session, so one user can hold two rows. */ sessionId?: string; /** Actor class, e.g. `"player"` / `"agent"` — a host may clamp each differently. */ kind?: string; /** Display name carried on the row, so a nameplate ne… — ⚠ undocumented · used by `useShellMultiplayerSync` (@jgengine/shell/useShellMultiplayerSync): Joins the multiplayer server for the live context and wires presence, feed relay, and chat sync until teardown.
739
+ - `PresencePoseRow` (type): type PresencePoseRow = { userId: string; /** Set when the host tracks presence per session, so one user can hold two rows. */ sessionId?: string; /** Actor class, e.g. `"player"` / `"agent"` — a host may clamp each differently. */ kind?: string; /** Display name carried on the row, so a nameplate ne… — ⚠ undocumented · used by `useShellMultiplayerSync` (@jgengine/core/multiplayer/useShellMultiplayerSync.draw): Joins the multiplayer server for the live context and wires presence, feed relay, and chat sync until teardown.
626
740
  - `PresenceSync` (type): type PresenceSync = { subscribe: (serverId: string, onChange: (rows: PresencePoseRow[]) => void) => FeedUnsubscribe; syncPose: (serverId: string, pose: PlayerPose) => void; } — ⚠ undocumented
627
741
  - `ResumeTicket` (type): type ResumeTicket = { userId: string; serverId: string; token: string; } — A short-lived credential allowing a disconnected client to reclaim one server membership.
628
742
  - `RunCommandArgs` (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; } — ⚠ undocumented
@@ -630,18 +744,6 @@
630
744
  - `WorldSyncFrame` (type): type WorldSyncFrame = | { kind: "baseline"; revision: number; snapshot: WorldSnapshot } | { kind: "diff"; revision: number; diff: WorldDiff } — A revision-stamped world baseline or compact diff sent to a server-authoritative client.
631
745
  - `createTransportSessionId` (function): function createTransportSessionId(): string — Fresh id for one client session's join/leave pair (see {@link GameRuntimeTransport.joinServer}).
632
746
 
633
- ## @jgengine/core/runtime/visibility
634
-
635
- - `CameraVisibilityContext` (interface): interface CameraVisibilityContext — A camera's position and reach, as seen by the visibility system.
636
- - `DEFAULT_VISIBILITY_SETTINGS` (const): const DEFAULT_VISIBILITY_SETTINGS: Readonly<VisibilityDefaults> — Default `VisibilityDefaults` a `VisibilitySystem` falls back to when unspecified.
637
- - `VisibilityBounds` (interface): interface VisibilityBounds — Sphere bounds used for visibility/streaming distance checks.
638
- - `VisibilityDecision` (interface): interface VisibilityDecision — Result of evaluating one object against the active cameras this tick.
639
- - `VisibilityDefaults` (interface): interface VisibilityDefaults — Baseline policy settings a `VisibilitySystem` is constructed with.
640
- - `VisibilityId` (type): type VisibilityId = string | number — Stable identifier for a visibility-tracked object or camera.
641
- - `VisibilityObject` (interface): interface VisibilityObject — An entity/asset tracked by `VisibilitySystem.evaluate`.
642
- - `VisibilityOverrides` (interface): interface VisibilityOverrides — Per-object escape hatches that bypass or tune the default visibility policy.
643
- - `VisibilityPoint` (interface): interface VisibilityPoint — World-space point; `z` is optional for 2D adapters.
644
-
645
747
  ## @jgengine/core/runtime/worldChannel
646
748
 
647
749
  - `WorldClientFrame` (type): type WorldClientFrame = | { t: "join"; isNew: boolean } | { t: "leave" } | { t: "command"; name: string; input: unknown } | { t: "input"; frame: InputFrame } — Client→host frame: a player's session verbs. What a transport marshals upstream.
@@ -708,11 +810,11 @@
708
810
  - `GameHostProps` (interface): interface GameHostProps — Props for {@link GameHost}: the playable, optional multiplayer overrides, and the optional editor loader that enables the engine-owned F2+E summon.
709
811
  - `GameLoop` (interface): interface GameLoop<TContext = unknown> — Lifecycle hooks a game implements to drive init, per-tick simulation, and player join/leave.
710
812
  - `Hotbar` (function): function Hotbar({ inventoryId, activeSlot, keys, slotSize = 46, itemIcon, style, className, }: { inventoryId: string; activeSlot?: number; keys?: readonly string[]; slotSize?: number; /** Caller-supplied item id → icon registry; return null/undefined to fall back to the default glyph. */ itemIcon?: … — A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot. Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
711
- - `HudCanvas` (function): function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number; /** Opt-in play-phase gate: render the HUD only … — Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine anchor regions and stack automatically with a gap — no per-panel pixel offsets, no manual clearance for sibling panels, the touch-control dock (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the whole surface scales down and each panel applies its `compact` behavior.
813
+ - `HudCanvas` (function): function HudCanvas({ layout, editChord, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Opt-in play-phase gate: render the HUD only during these phases (e.g. `["playing"]`). Omit for always-visible (default). */ showDuring?: readonly GamePha… — Full-viewport HUD surface. Each `HudPanel` names an anchor and a priority; the canvas measures every panel and places them all at once so none lands on another, at any screen size. Panels sharing an anchor stack outward from that edge; a panel that would cover a neighbour slides along its edge to the nearest free spot; when the screen is too small, the lowest-priority panels fold into a toggle, and only then does the whole HUD shrink. Device safe areas and the touch-control dock (`--jg-hud-dock-clearance-*`) are kept clear. Games declare panels, never place them per size.
712
814
  - `HudFade` (function): function HudFade({ watch, idleMs = 3500, dimTo = 0.18, hold = false, children, className, style }: HudFadeProps): ReactNode — A HUD group that steps back when nothing is happening: it fades after a quiet spell and comes back the moment a watched value changes, so the world fills the screen and the HUD speaks only when it has news. Hovering it also brings it back.
713
- - `HudPanel` (function): function HudPanel({ id, anchor = "top-left", lane, order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** … — A HUD block that lives in one of the nine anchor regions. Panels sharing a region stack outward from the screen edge in ascending `order`. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement. On compact displays custom placements are ignored and the `compact` behavior applies.
815
+ - `HudPanel` (function): function HudPanel({ id, anchor = "top-left", order, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** Stack position within the anchor, asc… — A HUD block anchored to a corner, edge or the centre. `HudCanvas` places every panel together: panels sharing an anchor stack outward from the screen edge in ascending `order`, and a panel that would cover another slides to the nearest free spot. When the screen is too small, lower-`priority` panels fold into a toggle first. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement.
714
816
  - `HudSlots` (function): function HudSlots({ storageKey, slots, overlay, showDuring = DEFAULT_SLOT_PHASES }: HudSlotsProps): ReactNode — The play HUD in one line: a few small panels pinned to the screen's edges, scaled from a 1600x900 design to any window and movable by the player (the layout is remembered per `storageKey`). Keep it to a handful of slots; the world is the picture.
715
- - `PlayableGame` (type): type PlayableGame = EnginePlayableGame< ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps> > & { /** HUD theme the shell applies at the `GameUI` root. Unset uses `defaultHudTheme`; `false` opts out of the default theme and the en… — The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle.
817
+ - `PlayableGame` (type): type PlayableGame = PlayableGameContract< ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps> > & { /** HUD theme the shell applies at the `GameUI` root. Unset uses `defaultHudTheme`; `false` opts out of the default theme and the … — The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle.
716
818
  - `StoreHandle` (interface): interface StoreHandle<T> — A typed, cast-free handle onto one slot of the reactive game store (`ctx.game.store`). The single type parameter is fixed at definition; `read`/`write`/`update` never widen to `unknown`, so no call site repeats `store.get(key) as T`. Writes flow through the engine store, so they bump `ctx.version()`, serialize into a {@link WorldSnapshot}, and replay under host authority — the same guarantees a hand-rolled `store.get`/`store.set` pair gives up the moment it forks run state into a module-level singleton.
717
819
  - `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.
718
820
  - `authoredSpawnPosition` (function): function authoredSpawnPosition(document: AuthoredSpawnDocumentLike, options: AuthoredSpawnOptions = {}): [number, number, number] | null — Position of the authored spawn marker as a spawn-ready `[x, y, z]` tuple, or null when the document has none. Reads the first `player_spawn` marker by default, so dragging the marker in the editor moves where players spawn — no coordinates copied into game code.