@jgengine/shell 0.18.0 → 0.18.1

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +114 -2
  2. package/dist/GamePlayerShell.js +10 -4
  3. package/dist/JoinGate.d.ts +17 -0
  4. package/dist/JoinGate.js +14 -0
  5. package/dist/Shell3dPresentation.d.ts +8 -1
  6. package/dist/Shell3dPresentation.js +34 -5
  7. package/dist/ShellChrome.d.ts +2 -0
  8. package/dist/ShellChrome.js +6 -0
  9. package/dist/TerritoryOverlay.d.ts +21 -0
  10. package/dist/TerritoryOverlay.js +9 -0
  11. package/dist/audio/AudioComponents.js +8 -1
  12. package/dist/audio/audioEngine.d.ts +7 -1
  13. package/dist/audio/audioEngine.js +67 -5
  14. package/dist/camera/cameraRigs.js +7 -0
  15. package/dist/defineGame.js +21 -6
  16. package/dist/devtools/KeysPanel.js +3 -3
  17. package/dist/drivers/FrameDriver.d.ts +5 -1
  18. package/dist/drivers/FrameDriver.js +61 -24
  19. package/dist/drivers/HudOnlyDriver.js +9 -4
  20. package/dist/environment/EnvironmentScene.js +1 -1
  21. package/dist/input/gamepadMerge.d.ts +10 -0
  22. package/dist/input/gamepadMerge.js +12 -0
  23. package/dist/input/gamepadSource.d.ts +12 -0
  24. package/dist/input/gamepadSource.js +107 -0
  25. package/dist/materialOverride.d.ts +4 -1
  26. package/dist/materialOverride.js +10 -1
  27. package/dist/postfx/PostProcessing.d.ts +10 -7
  28. package/dist/postfx/PostProcessing.js +33 -15
  29. package/dist/postfx/gradeShader.js +13 -0
  30. package/dist/render/CascadedShadows.d.ts +11 -0
  31. package/dist/render/CascadedShadows.js +19 -8
  32. package/dist/render/EnvironmentLighting.d.ts +4 -1
  33. package/dist/render/EnvironmentLighting.js +45 -9
  34. package/dist/render/SceneLighting.d.ts +16 -2
  35. package/dist/render/SceneLighting.js +56 -6
  36. package/dist/render/SceneModels.js +45 -9
  37. package/dist/render/modelLoad.d.ts +10 -0
  38. package/dist/render/modelLoad.js +28 -0
  39. package/dist/render/modelRender.d.ts +3 -0
  40. package/dist/render/modelRender.js +6 -2
  41. package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +15 -0
  42. package/dist/render/testFixtures/proceduralSpriteAtlas.js +30 -0
  43. package/dist/render/testFixtures/proceduralTileLayer.d.ts +13 -0
  44. package/dist/render/testFixtures/proceduralTileLayer.js +30 -0
  45. package/dist/render/useFootIk.d.ts +9 -0
  46. package/dist/render/useFootIk.js +89 -0
  47. package/dist/render/useModelAnimation.d.ts +2 -0
  48. package/dist/render/useModelAnimation.js +163 -107
  49. package/dist/scene/CityRenderer.js +661 -236
  50. package/dist/scene/cityWindowMaterial.d.ts +8 -6
  51. package/dist/scene/cityWindowMaterial.js +80 -21
  52. package/dist/settings/appliedSettings.d.ts +3 -1
  53. package/dist/settings/appliedSettings.js +5 -2
  54. package/dist/terrain/terrainDetailMaterial.js +7 -3
  55. package/dist/useShellMultiplayerSync.d.ts +8 -1
  56. package/dist/useShellMultiplayerSync.js +25 -5
  57. package/dist/visibility/CullingProvider.d.ts +2 -1
  58. package/dist/visibility/CullingProvider.js +7 -2
  59. package/dist/world/GridWorldScene.js +4 -0
  60. package/dist/world/TileLayerRenderer.d.ts +9 -0
  61. package/dist/world/TileLayerRenderer.js +64 -0
  62. package/dist/world/WorldParticles.d.ts +31 -0
  63. package/dist/world/WorldParticles.js +128 -0
  64. package/dist/world/WorldScene.d.ts +2 -1
  65. package/dist/world/WorldScene.js +46 -4
  66. package/dist/world/entityPose.d.ts +4 -0
  67. package/dist/world/entityPose.js +9 -0
  68. package/dist/world/tileLayerInstances.d.ts +11 -0
  69. package/dist/world/tileLayerInstances.js +28 -0
  70. package/dist/worldSync.d.ts +5 -1
  71. package/dist/worldSync.js +25 -4
  72. package/package.json +4 -4
  73. package/skills/ce-handoff/SKILL.md +1 -1
  74. package/skills/game-design/SKILL.md +11 -1
  75. package/skills/jgengine/SKILL.md +2 -0
  76. package/skills/jgengine/api.md +94 -19
  77. package/skills/jgengine/capabilities.md +28 -0
  78. package/skills/jgengine-ui/SKILL.md +8 -0
  79. package/skills/jgengine-ui/api.md +127 -16
  80. package/skills/jgengine-ui/capabilities.md +21 -0
  81. package/skills/jgengine-ui/reference.md +3 -0
  82. package/skills/jgengine-world/SKILL.md +6 -0
  83. package/skills/jgengine-world/api.md +312 -12
  84. package/skills/jgengine-world/capabilities.md +102 -0
  85. package/skills/level-design/SKILL.md +1 -1
  86. package/skills/jgengine-ui/references/visual-scorecard.md +0 -73
@@ -14,7 +14,8 @@ import { MeasuredBoundsGroup } from "../render/measureBounds.js";
14
14
  import { EntitySprite, IsolatedEntityModel } from "../render/SceneModels.js";
15
15
  import { resolveModel, resolveEntityModel, tryResolveCatalogModel } from "../render/resolveModel.js";
16
16
  import { useRenderVisibility } from "../visibility/CullingProvider.js";
17
- import { writeEntityPose } from "./entityPose.js";
17
+ import { writeEntityPose, writeRenderPose } from "./entityPose.js";
18
+ import { createSnapshotBuffer } from "@jgengine/core/runtime/snapshotBuffer";
18
19
  import { POINTER_ENTITY_KEY, POINTER_OBJECT_KEY } from "../pointer/pointerService.js";
19
20
  const GROUND_SIZE = 160;
20
21
  const GROUND_SEGMENTS = 80;
@@ -26,6 +27,7 @@ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, select
26
27
  const role = entity.role;
27
28
  const name = entity.name;
28
29
  const color = isLocal ? "#4ade80" : role === "npc" ? colorFromId(name) : "#9ca3af";
30
+ const scratchRef = useRef([0, 0, 0, 0]);
29
31
  useFrame(() => {
30
32
  const group = groupRef.current;
31
33
  if (group === null)
@@ -33,7 +35,7 @@ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, select
33
35
  const live = ctx.scene.entity.get(entityId);
34
36
  if (live === null)
35
37
  return;
36
- writeEntityPose(group, live);
38
+ writeRenderPose(group, ctx, entityId, live, scratchRef.current);
37
39
  group.visible = live.hidden !== true && visibleRef.current(entityId);
38
40
  });
39
41
  return (_jsxs("group", { ref: groupRef, userData: { [POINTER_ENTITY_KEY]: entityId }, onPointerDown: (event) => {
@@ -187,6 +189,46 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
187
189
  export function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
188
190
  return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds, hideLocalActor: hideLocalActor })] }));
189
191
  }
190
- export function RemotePlayers({ rows }) {
191
- return (_jsx(_Fragment, { children: rows.map((row) => (_jsxs("group", { position: [row.position.x, row.position.y, row.position.z], "rotation-y": row.rotationY, children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: colorFromId(row.userId) })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] }, row.userId))) }));
192
+ export function RemotePlayers({ rows, presenceInterpolationMs = 100 }) {
193
+ const groups = useRef(new Map());
194
+ const buffers = useRef(new Map());
195
+ const rowsRef = useRef(rows);
196
+ rowsRef.current = rows;
197
+ useEffect(() => {
198
+ const active = new Set(rows.map((row) => row.userId));
199
+ for (const row of rows) {
200
+ let buffer = buffers.current.get(row.userId);
201
+ if (buffer === undefined) {
202
+ buffer = createSnapshotBuffer({ delayMs: presenceInterpolationMs, capacity: 16 });
203
+ buffers.current.set(row.userId, buffer);
204
+ }
205
+ else {
206
+ buffer.retune({ delayMs: presenceInterpolationMs });
207
+ }
208
+ buffer.push(row.lastSeenAt, row);
209
+ }
210
+ for (const userId of buffers.current.keys()) {
211
+ if (!active.has(userId))
212
+ buffers.current.delete(userId);
213
+ }
214
+ }, [rows, presenceInterpolationMs]);
215
+ useFrame(({ clock }) => {
216
+ const now = clock.getElapsedTime() * 1000;
217
+ for (const [userId, buffer] of buffers.current) {
218
+ const group = groups.current.get(userId);
219
+ const sample = buffer.sampleAt(now);
220
+ if (group === undefined || sample === null)
221
+ continue;
222
+ const before = sample.before;
223
+ const after = sample.after;
224
+ group.position.set(before.position.x + (after.position.x - before.position.x) * sample.alpha, before.position.y + (after.position.y - before.position.y) * sample.alpha, before.position.z + (after.position.z - before.position.z) * sample.alpha);
225
+ group.rotation.y = before.rotationY + (after.rotationY - before.rotationY) * sample.alpha;
226
+ }
227
+ });
228
+ return (_jsx(_Fragment, { children: rows.map((row) => (_jsxs("group", { ref: (group) => {
229
+ if (group === null)
230
+ groups.current.delete(row.userId);
231
+ else
232
+ groups.current.set(row.userId, group);
233
+ }, children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: colorFromId(row.userId) })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] }, row.userId))) }));
192
234
  }
@@ -1,3 +1,5 @@
1
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
2
+ import type { RenderPose } from "@jgengine/core/runtime/poseInterpolation";
1
3
  export interface PoseWritable {
2
4
  position: {
3
5
  set(x: number, y: number, z: number): void;
@@ -12,5 +14,7 @@ export interface PoseSource {
12
14
  }
13
15
  /** @internal */
14
16
  export declare function writeEntityPose(target: PoseWritable, source: PoseSource): void;
17
+ /** Write the interpolated render pose from `ctx.sim` (falls back to `live` when no history exists). @internal */
18
+ export declare function writeRenderPose(target: PoseWritable, ctx: Pick<GameContext, "sim">, entityId: string, live: PoseSource, scratch: RenderPose): void;
15
19
  /** @internal */
16
20
  export declare function posesEqual(a: PoseSource, b: PoseSource): boolean;
@@ -3,6 +3,15 @@ export function writeEntityPose(target, source) {
3
3
  target.position.set(source.position[0], source.position[1], source.position[2]);
4
4
  target.rotation.y = source.rotationY;
5
5
  }
6
+ /** Write the interpolated render pose from `ctx.sim` (falls back to `live` when no history exists). @internal */
7
+ export function writeRenderPose(target, ctx, entityId, live, scratch) {
8
+ if (ctx.sim.renderPose(entityId, scratch)) {
9
+ target.position.set(scratch[0], scratch[1], scratch[2]);
10
+ target.rotation.y = scratch[3];
11
+ return;
12
+ }
13
+ writeEntityPose(target, live);
14
+ }
6
15
  /** @internal */
7
16
  export function posesEqual(a, b) {
8
17
  return (a.position[0] === b.position[0] &&
@@ -0,0 +1,11 @@
1
+ import type { TilemapWorldConfig } from "@jgengine/core/world/features";
2
+ /** One resolved map cell with its world position and atlas UV rectangle. */
3
+ export interface TileLayerInstance {
4
+ x: number;
5
+ z: number;
6
+ frame: string;
7
+ uvOffset: readonly [number, number];
8
+ uvScale: readonly [number, number];
9
+ }
10
+ /** Resolve map glyphs to atlas frames and normalized UV rectangles for deterministic tests and renderers. */
11
+ export declare function resolveTileLayerInstances(config: TilemapWorldConfig): readonly TileLayerInstance[];
@@ -0,0 +1,28 @@
1
+ /** Resolve map glyphs to atlas frames and normalized UV rectangles for deterministic tests and renderers. */
2
+ export function resolveTileLayerInstances(config) {
3
+ if (config.tileSet === undefined)
4
+ return [];
5
+ const { atlas, tiles } = config.tileSet;
6
+ const rows = config.map.replace(/\r\n/g, "\n").split("\n").filter((row) => row.length > 0);
7
+ const width = Math.max(0, ...rows.map((row) => row.length));
8
+ const instances = [];
9
+ for (let row = 0; row < rows.length; row += 1) {
10
+ const line = rows[row].padEnd(width, " ");
11
+ for (let column = 0; column < width; column += 1) {
12
+ const frame = tiles[line[column]];
13
+ if (frame === undefined)
14
+ continue;
15
+ const rect = atlas.frames[frame];
16
+ if (rect === undefined)
17
+ throw new Error(`tileSet frame "${frame}" is missing from the atlas`);
18
+ instances.push({
19
+ x: column - (width - 1) / 2,
20
+ z: row - (rows.length - 1) / 2,
21
+ frame,
22
+ uvOffset: [rect.x / atlas.size[0], 1 - (rect.y + rect.h) / atlas.size[1]],
23
+ uvScale: [rect.w / atlas.size[0], rect.h / atlas.size[1]],
24
+ });
25
+ }
26
+ }
27
+ return instances;
28
+ }
@@ -1,5 +1,7 @@
1
1
  import type { GameContext } from "@jgengine/core/runtime/gameContext";
2
2
  import type { GameRuntimeFeeds } from "@jgengine/core/runtime/transport";
3
+ /** Receives the revision of an authoritative world frame after it is applied locally. */
4
+ export type AuthoritativeFrameHandler = (revision: number) => void;
3
5
  /**
4
6
  * Client half of host-authoritative play: subscribe to the server-state channel and mirror each authoritative
5
7
  * `WorldSnapshot` (carried in `serverState`) into the local `ctx`, so the game renders the host's world instead
@@ -8,4 +10,6 @@ import type { GameRuntimeFeeds } from "@jgengine/core/runtime/transport";
8
10
  * opts into `authority: "server"`.
9
11
  * @internal
10
12
  */
11
- export declare function attachWorldSync(feeds: Pick<GameRuntimeFeeds, "subscribeServer">, serverId: string, ctx: Pick<GameContext, "hydrate">): () => void;
13
+ export declare function attachWorldSync(feeds: Pick<GameRuntimeFeeds, "subscribeServer" | "requestServerBaseline">, serverId: string, ctx: Pick<GameContext, "hydrate">,
14
+ /** Called after each accepted authoritative baseline or diff, for local prediction reconciliation. */
15
+ onAuthoritativeFrame?: AuthoritativeFrameHandler): () => void;
package/dist/worldSync.js CHANGED
@@ -7,14 +7,35 @@ import { createWorldMirror } from "@jgengine/core/runtime/worldMirror";
7
7
  * opts into `authority: "server"`.
8
8
  * @internal
9
9
  */
10
- export function attachWorldSync(feeds, serverId, ctx) {
10
+ export function attachWorldSync(feeds, serverId, ctx,
11
+ /** Called after each accepted authoritative baseline or diff, for local prediction reconciliation. */
12
+ onAuthoritativeFrame) {
11
13
  const mirror = createWorldMirror(ctx);
12
14
  return feeds.subscribeServer(serverId, (view) => {
13
15
  if (view === null)
14
16
  return;
15
- const snapshot = view.serverState;
16
- if (snapshot === null || snapshot === undefined)
17
+ const frame = view.serverState;
18
+ if (frame === null || frame === undefined)
17
19
  return;
18
- mirror.applyBaseline(view.revision, snapshot);
20
+ if (isWorldSyncFrame(frame)) {
21
+ if (frame.kind === "baseline") {
22
+ mirror.applyBaseline(frame.revision, frame.snapshot);
23
+ onAuthoritativeFrame?.(frame.revision);
24
+ }
25
+ else {
26
+ mirror.applyDiff(frame.diff);
27
+ onAuthoritativeFrame?.(frame.revision);
28
+ if (mirror.needsResync())
29
+ feeds.requestServerBaseline?.(serverId);
30
+ }
31
+ return;
32
+ }
33
+ mirror.applyBaseline(view.revision, frame);
34
+ onAuthoritativeFrame?.(view.revision);
19
35
  });
20
36
  }
37
+ function isWorldSyncFrame(value) {
38
+ return (typeof value === "object" &&
39
+ value !== null &&
40
+ (value.kind === "baseline" || value.kind === "diff"));
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/shell",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -35,9 +35,9 @@
35
35
  "test": "bun test src"
36
36
  },
37
37
  "dependencies": {
38
- "@jgengine/core": "^0.18.0",
39
- "@jgengine/react": "^0.18.0",
40
- "@jgengine/ws": "^0.18.0"
38
+ "@jgengine/core": "^0.18.1",
39
+ "@jgengine/react": "^0.18.1",
40
+ "@jgengine/ws": "^0.18.1"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@react-three/drei": "^10.0.0",
@@ -146,4 +146,4 @@ When sufficient, return a concise orientation: objective, progress, decisions, c
146
146
  - Multi-session engine work often spans `docs/plans/`, GitHub issues, and stacked PRs — point at those; do not paste full plan bodies.
147
147
  - Prefer `bun run agent:bootstrap --check` status, branch names, and PR links as state anchors.
148
148
  - Local parallelism uses `.claude/worktrees/`; never invent `C:\tmp\…` Codex-shared trees.
149
- - Papercuts and durable process changes belong in `PAPERCUTS.md` / skills, not only the handoff.
149
+ - Durable process changes belong in skills, not only the handoff.
@@ -11,7 +11,7 @@ Own the intended player experience and the rules that produce it: fantasy, pilla
11
11
 
12
12
  Use `level-design` for spatial realization; route implementation to `jgengine-gameplay`, `jgengine-world`, `jgengine-combat`, `jgengine-ui`, `jgengine-multiplayer` once intent is coherent.
13
13
 
14
- Read [references/application-playbook.md](references/application-playbook.md) only when the task explicitly calls for design depth — a design brief, a redesign, or "make this fun/better" work; it is not a mandatory step of creating a game. Read [references/field-guide.md](references/field-guide.md) when the task needs deeper loop, economy, progression, genre, accessibility, or research guidance.
14
+ For a greenfield build, read [references/application-playbook.md](references/application-playbook.md) and produce its greenfield contract (promise, pillars, anti-pillars, core decision, and falsifier) before writing code. Audits may read only the playbook's audit sections. Read [references/field-guide.md](references/field-guide.md) when the task needs deeper loop, economy, progression, genre, accessibility, or research guidance.
15
15
 
16
16
  ## Choose the mode
17
17
 
@@ -28,3 +28,13 @@ Every design pass yields an observed journey, a north star (promise, pillars, an
28
28
  An audit is not completion for a build/improve request. Finish when the slice is playable, a fresh player can state goal/choice/consequence, failure/recovery behave as intended, and evidence supports the pillars. Use `jgengine-verify` and `workflow` to ship.
29
29
 
30
30
  **Required: player death is a designed, visible moment.** If the game can kill or down the player, death is part of the experience contract, not an invisible state reset. A lethal hit must resolve into an authored beat the player perceives and understands — a death or downed moment, the stakes it carries, and a legible path back into play (respawn, revive, restart) — never a silent teleport to spawn. Design what death means for this pitch (permadeath, checkpoint, bleed-out-and-revive, run reset) and treat "player dies with no acknowledged consequence" as a failure/recovery defect. `jgengine-ui` owns building the screen and respawn feedback.
31
+
32
+ ## Persistent-builder checks
33
+
34
+ Before content, write a first-hour balance sheet: starting cash, time to first income, net income per hour at starting hardware and at ten times that hardware, bill cadence versus income, and the insolvency recovery path. Insolvency resets the affected player or changes their options; it never deletes the world. Express clicks and event rewards as a fraction of the designed income rate, never an unrelated flat payout.
35
+
36
+ Declare currency `decimals` for income paid per second. Anchor time-based decay to `game.createdAt` or a persisted per-instance creation time, never a fixed wall-clock epoch. Test fractional income over real tick cadences before tuning prices.
37
+
38
+ The first-60-seconds gate requires the first tutorial verb to be executable from spawn with starting inventory. Grant dependencies such as land, power, and slots or let players buy them in-context; the tutorial cannot lock the surface needed to complete itself. Placement previews show cost and affordability inline.
39
+
40
+ For a persistent shared-world builder, compose neighborhood presence, declared command read scopes, membership storage, online-player batched ticks, territory, chat, and join retry UI through their engine owners. Verify capacity and first-player onboarding before scaling the content catalog.
@@ -9,6 +9,8 @@ Read the JGengine README for package truth, commands, layering, and license —
9
9
 
10
10
  ## 1. Establish the target
11
11
 
12
+ For greenfield builds, write `src/art-direction.md` before any code.
13
+
12
14
  Treat the pitch as a unique composition of needs, not a genre to fill in — never reach for a genre kit, preset, archetype, or class template ("default sports car", "default RPG", "default FPS") in the SDK or skills (see [AGENTS.md](../../../AGENTS.md)). Use `game-design` to turn the pitch into a testable player promise, verbs, loops, progression, failure, and completion scenario. Use `level-design` when play depends on authored spaces, routes, encounters, exploration, or spatial pacing. Capture only decisions that change architecture:
13
15
 
14
16
  - player point of view and controls
@@ -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.18.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.18.1" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
10
10
 
11
11
  ## @jgengine/core/authoring
12
12
 
@@ -24,7 +24,7 @@
24
24
  - `OwnershipDecision` (type): type OwnershipDecision = "expose" | "bake" | "reject" — The boundary's resolution for one object.
25
25
  - `OwnershipDiagnostic` (interface): interface OwnershipDiagnostic — One boundary problem found while auditing a set of declarations.
26
26
  - `OwnershipVerdict` (interface): interface OwnershipVerdict — The full verdict for one object: its decision plus the flags and diagnostic the editor needs to act on it.
27
- - `PhysicsConfig` (interface): interface PhysicsConfig — World gravity and jump tuning, plus scene-object collision opt-ins, for the game's physics step.
27
+ - `PhysicsConfig` (interface): interface PhysicsConfig — World gravity and jump tuning, plus the optional rigid-body backend and character capsule.
28
28
  - `ProviderCapabilities` (interface): interface ProviderCapabilities — What a provider lets the editor do with one of its objects. Absent flags default to `false`.
29
29
  - `RuntimeProvenance` (interface): interface RuntimeProvenance — An object owned entirely by runtime or procedural code with no document entry. Not authorable unless its provider can bake it into schema-valid authored data.
30
30
  - `SCENE_OWNERSHIP_MANIFEST_VERSION` (const): const SCENE_OWNERSHIP_MANIFEST_VERSION: 1 — Current {@link SceneOwnershipManifest} schema version.
@@ -52,7 +52,7 @@
52
52
  - `plots` (function): function plots(config: PlotsWorldConfig = {}): WorldFeature — Declares a subdivided-plots world — farming, base-building, and other parcel-based layouts.
53
53
  - `rain` (function): function rain(config: RainEnvironmentConfig = {}): RainEnvironmentDescriptor — Declares a rainfall weather effect for `environment()` — area, density, speed, wind, and drop width/opacity.
54
54
  - `road` (function): function road(config: RoadEnvironmentConfig): RoadEnvironmentDescriptor — Declare a road ribbon for an `environment()` world; the shell drapes and renders it over the terrain.
55
- - `seededRng` (function): function seededRng(seed: string | number): () => number — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
55
+ - `seededRng` (function): function seededRng(seed: string | number): SeededRng — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
56
56
  - `seededStreams` (function): function seededStreams(seed: string | number): (stream: string) => () => number — Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name.
57
57
  - `snow` (function): function snow(config: SnowEnvironmentConfig = {}): SnowEnvironmentDescriptor — Declares a snowfall weather effect for `environment()` — area, density, drift, wind, and flake opacity.
58
58
  - `terrain` (function): function terrain(config: TerrainEnvironmentConfig = {}): TerrainEnvironmentDescriptor — Declares a heightfield terrain patch for `environment()` — bounds, noise, materials, and flatten masks.
@@ -228,14 +228,14 @@
228
228
 
229
229
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
230
230
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
231
- - `VERSION` (const): const VERSION: "0.18.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
231
+ - `VERSION` (const): const VERSION: "0.18.1" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
232
232
 
233
233
  ## @jgengine/core/runtime/adapter
234
234
 
235
235
  - `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
236
236
  - `MultiplayerAuthority` (type): type MultiplayerAuthority = "server" | "client" — Where the world simulation is authoritative.
237
237
  - `MultiplayerTopology` (type): type MultiplayerTopology = "shared" | "lobbies" | "private" — ⚠ undocumented
238
- - `ServersPoolConfig` (type): type ServersPoolConfig = { maxServers: number; slotsPerServer: number; minPlayersToStart?: number; adapter: MultiplayerAdapterConfig; } — ⚠ undocumented
238
+ - `ServersPoolConfig` (type): type ServersPoolConfig = { minPlayersToStart?: number; adapter: MultiplayerAdapterConfig; } & ( | { topology: "shared"; maxServers?: number; slotsPerServer?: number } | { topology?: "rooms"; maxServers: number; slotsPerServer: number } ) — ⚠ undocumented
239
239
  - `adapterOf` (function): function adapterOf(multiplayer: unknown): MultiplayerAdapterConfig | null — ⚠ undocumented
240
240
  - `convex` (function): function convex(config?: { topology?: MultiplayerTopology; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — Convex transport. Omitting `authority` (or passing `"client"`) is **presence-only** — prefer `convexPresence()` to name that intent explicitly. Pass `{ authority: "server" }` for a shared, host-authoritative world — see `examples/HOSTED.md`.
241
241
  - `convexPresence` (function): function convexPresence(config?: { topology?: MultiplayerTopology }): MultiplayerAdapterConfig — Presence-only Convex transport — each client runs its own `onTick`; only presence/feeds/chat sync. Sugar for `convex({ ...config, authority: "client" })`.
@@ -248,7 +248,7 @@
248
248
  - `offline` (function): function offline(): MultiplayerAdapterConfig — Explicit single-player adapter. Solo games never need this — omitting `multiplayer` in the shell `defineGame` already defaults to offline; pass it only where an adapter value is structurally required.
249
249
  - `p2p` (function): function p2p(config?: { topology?: MultiplayerTopology; room?: string; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — Serverless peer-to-peer (WebRTC) session — one peer hosts, friends join by room code.
250
250
  - `resolveAuthority` (function): function resolveAuthority(multiplayer: unknown): MultiplayerAuthority | null — Resolved authority for a multiplayer config. - `offline` / missing adapter → `null` (single-player; not multiplayer authority). - unset or `"client"` → `"client"` (presence-only; each client ticks). - `"server"` → host-authoritative shared sim.
251
- - `servers` (function): function servers(config: ServersPoolConfig): ServersPoolConfig — ⚠ undocumented
251
+ - `servers` (function): function servers(config: ServersPoolConfig): { topology: "shared" | "rooms"; maxServers: number; slotsPerServer: number; minPlayersToStart?: number | undefined; adapter: MultiplayerAdapterConfig; } | { topology: "shared" | "rooms"; maxServers: number; slotsPerServer: number; minPlayersToStart?: numb… — Resolve the shared-world pool to one unbounded world; room pools keep explicit capacities.
252
252
  - `socketIo` (function): function socketIo(config?: { topology?: MultiplayerTopology; url?: string; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — ⚠ undocumented
253
253
  - `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`.
254
254
  - `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" })`.
@@ -258,10 +258,16 @@
258
258
  - `CameraDirector` (interface): interface CameraDirector — ⚠ undocumented
259
259
  - `ChaseCameraTuning` (type): type ChaseCameraTuning = Partial< Pick<ChaseCameraConfig, "distance" | "height" | "lookHeight" | "springDamping" | "fov" | "lead" | "bank" | "shakePerSpeed" | "velocityYaw" | "yawResponse"> > — Runtime patch over the static `camera.chase` config — distance/height/fov retuning from gameplay events (#286.11), or a whole driving-feel overlay (speed→FOV, lead, bank, speed shake, drift-lag) applied only while a vehicle is piloted (#1299).
260
260
 
261
+ ## @jgengine/core/runtime/commandInput
262
+
263
+ - `QuantityResult` (type): type QuantityResult = { ok: true; quantity: number } | { ok: false; reason: "invalid-quantity" } — Validated integer quantity or a stable input rejection.
264
+ - `readQuantity` (function): function readQuantity(value: unknown, options: { min?: number; max?: number } = {}): QuantityResult — Read an untrusted whole-item quantity without coercion, truncation, or clamping.
265
+
261
266
  ## @jgengine/core/runtime/commandRunner
262
267
 
263
268
  - `CommandDef` (type): type CommandDef<TInput = unknown> = { /** * What this command reads and writes, derived from its own input. A host that hydrates through a * scope loads only this slice instead of the whole world, and refuses the command if `apply` then * dirties a player or chunk the scope did not name — writing an… — ⚠ undocumented
264
269
  - `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.
270
+ - `CommandScopeDefinition` (type): type CommandScopeDefinition = Omit<CommandScope, "players"> & { players?: readonly string[] | "actor" } — Declarative read scope; actor is resolved before host hydration.
265
271
  - `CommandValidationError` (type): type CommandValidationError = { reason: string } — ⚠ undocumented
266
272
  - `RunCommandResult` (type): type RunCommandResult = | { ok: true; snapshot: GameRuntimeSnapshot } | { ok: false; reason: string } — ⚠ undocumented
267
273
 
@@ -335,13 +341,14 @@
335
341
 
336
342
  ## @jgengine/core/runtime/gameRuntime
337
343
 
338
- - `GameRuntime` (type): type GameRuntime = { gameId: string; save: SaveConfig; /** * Whether this runtime declares `loop.onTick`. A host's tick cron reads it to skip hydrating and * persisting a server whose `tick` is a no-op by construction — the difference between a world that * costs a full read/write every second while… — ⚠ undocumented
339
- - `GameRuntimeDefinition` (type): type GameRuntimeDefinition = { gameId: string; save: SaveConfig; commands: Record<string, CommandDef>; loop?: ServerLoopHooks; } — ⚠ undocumented
340
- - `HydrateInput` (type): type HydrateInput = { gameId: string; serverId: string; serverRow: RuntimeServerRow; playersByUserId: Record<string, RuntimePlayerRow>; chunksByKey: Record<string, RuntimeChunkRow>; revision?: number; } — ⚠ undocumented
341
- - `RuntimeInitContext` (type): type RuntimeInitContext = { snapshot: GameRuntimeSnapshot; setSnapshot: (snapshot: GameRuntimeSnapshot) => void; } — ⚠ undocumented
344
+ - `GameRuntime` (type): type GameRuntime = { gameId: string; topology?: "shared" | "rooms"; save: SaveConfig; /** * Whether this runtime declares `loop.onTick`. A host's tick cron reads it to skip hydrating and * persisting a server whose `tick` is a no-op by construction — the difference between a world that * costs a ful… — ⚠ undocumented
345
+ - `GameRuntimeDefinition` (type): type GameRuntimeDefinition = { gameId: string; topology?: "shared" | "rooms"; save: SaveConfig; commands: Record<string, CommandDef>; loop?: ServerLoopHooks; } — ⚠ undocumented
346
+ - `HydrateInput` (type): type HydrateInput = { gameId: string; serverId: string; serverRow: RuntimeServerRow; playersByUserId: Record<string, RuntimePlayerRow>; chunksByKey: Record<string, RuntimeChunkRow>; revision?: number; /** Host wall clock for `onInit`, in ms. Defaults to `Date.now()`. */ nowMs?: number; } — ⚠ undocumented
347
+ - `RuntimeInitContext` (type): type RuntimeInitContext = { snapshot: GameRuntimeSnapshot; setSnapshot: (snapshot: GameRuntimeSnapshot) => void; /** * Host wall clock at the start of this call, in ms. The host already knows it, so anything keyed to * real time — a UTC date rollover, a `lastTickAt` anchor other code paths read, a s… — ⚠ undocumented
342
348
  - `RuntimeLoopContext` (type): type RuntimeLoopContext = RuntimeInitContext & { player: { userId: string; isNew: boolean; }; } — ⚠ undocumented
343
349
  - `RuntimeWorldContext` (type): type RuntimeWorldContext = RuntimeInitContext & { playerIds: readonly string[]; } — ⚠ undocumented
344
350
  - `ServerLoopHooks` (type): type ServerLoopHooks = { onInit?: (ctx: RuntimeInitContext) => void; onNewPlayer?: (ctx: RuntimeLoopContext) => void; onTick?: (ctx: RuntimeWorldContext, dtSeconds: number) => void; /** * What `onNewPlayer` touches, so a join hydrates that instead of the whole world. Only consulted * when `onNewPlay… — ⚠ undocumented
351
+ - `initialPlayerState` (function): function initialPlayerState(runtime: GameRuntime, userId: string, nowMs: number = Date.now()): RuntimePlayerRow — Seed a fresh player through the same onNewPlayer hook as joining, without hydrating or modifying a world.
345
352
 
346
353
  ## @jgengine/core/runtime/headlessRunner
347
354
 
@@ -375,7 +382,7 @@
375
382
 
376
383
  ## @jgengine/core/runtime/hostPolicy
377
384
 
378
- - `JG_MAX_MEMBERS_PER_SERVER` (const): const JG_MAX_MEMBERS_PER_SERVER: 256 — Hard ceiling on `slotsPerServer` for a single hosted server row.
385
+ - `JG_MAX_MEMBERS_PER_SERVER` (const): const JG_MAX_MEMBERS_PER_SERVER: 256 — Room-topology ceiling on `slotsPerServer`; shared membership uses indexed rows instead.
379
386
  - `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.
380
387
  - `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.
381
388
  - `MatchmakingMode` (type): type MatchmakingMode = "auto" | "singleton" — How auto-match behaves when no `serverId` is supplied.
@@ -383,11 +390,11 @@
383
390
  - `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).
384
391
  - `isListablePublicly` (function): function isListablePublicly(visibility: SessionVisibility | undefined): boolean — True when a server's `visibility` should surface in public listings / browse results.
385
392
  - `isPrivateJoinBlocked` (function): function isPrivateJoinBlocked(args: { visibility: SessionVisibility | undefined; memberUserIds: readonly string[]; userId: string; joinCode: string | undefined; suppliedCode: string | undefined; }): boolean — Whether a private-visibility server blocks this join (non-member without a matching code). Public / undefined visibility never blocks.
386
- - `isServerFull` (function): function isServerFull(memberUserIds: readonly string[], slotsPerServer: number, userId: string): boolean — True when the server has no free slots for a non-member. Existing members never count as "full" so rejoin/leave cycles keep working.
393
+ - `isServerFull` (function): function isServerFull(memberCount: number, slotsPerServer: number, isMember: boolean): boolean — True when the server has no free slots for a non-member. Existing members never count as "full" so rejoin/leave cycles keep working.
387
394
  - `isServerMember` (function): function isServerMember(memberUserIds: readonly string[], userId: string): boolean — Whether `userId` is already on the server's member roster.
388
395
  - `selectJoinTarget` (function): function selectJoinTarget<T extends JoinCandidate>(rows: readonly T[], args: { userId: string; mode?: MatchmakingMode; /** Capacity to judge rows against — the host's current option, when it supersedes the stored value. */ slotsPerServer?: number; }): JoinTarget<T> — Resolve an auto-match join against the game's joinable rows under a {@link MatchmakingMode}. Reach for it instead of hand-rolling the "find a room with space, else make one" scan, which is where a singleton world silently shards into per-player copies.
389
396
  - `statusAfterLeave` (function): function statusAfterLeave(remainingMemberCount: number, currentStatus: GameServerStatus): GameServerStatus — Status after a leave: empty rooms reopen; non-empty rooms keep their current status.
390
- - `validateSlotsPerServer` (function): function validateSlotsPerServer(slotsPerServer: number): { ok: true } | { ok: false; reason: string } — Whether a `slotsPerServer` value is a capacity a single hosted server row can actually hold.
397
+ - `validateSlotsPerServer` (function): function validateSlotsPerServer(slotsPerServer: number, topology: "rooms" | "shared" = "rooms"): { ok: true } | { ok: false; reason: string } — Whether a `slotsPerServer` value is a capacity a single hosted server row can actually hold.
391
398
  - `withJoinedMember` (function): function withJoinedMember(memberUserIds: readonly string[], userId: string): string[] — Roster after a successful join: unchanged when already a member, else appended.
392
399
  - `withoutMember` (function): function withoutMember(memberUserIds: readonly string[], userId: string): string[] — Roster after a leave: `userId` removed; order of remaining members preserved.
393
400
 
@@ -405,6 +412,22 @@
405
412
  - `HostedWorldSessionOptions` (interface): interface HostedWorldSessionOptions<TAssetRef extends ModelAssetRef, TMultiplayer> — Config for {@link createHostedWorldSession}: the game, its persistence store, and the auto-save cadence.
406
413
  - `HostedWorldStore` (interface): interface HostedWorldStore — Narrow persistence seam for a hosted world — the {@link HostedWorldRecord} counterpart of `HostPersistence`. Backends implement it (memory/file/sql/convex); the session never names one. A stateful host loads once and saves on a cadence; a stateless host reconstructs from `load()` each invocation.
407
414
  - `HostedWorldSync` (type): type HostedWorldSync = | { kind: "baseline"; revision: number; snapshot: WorldSnapshot } | { kind: "diff"; diff: WorldDiff } — A client replication pull: a full baseline (first sync / fell behind) or a diff since the client's cursor.
415
+ - `SyncHostedWorldStore` (interface): interface SyncHostedWorldStore — Synchronous store adapter retained for deterministic in-process tests.
416
+ - `asyncMemoryWorldStore` (function): function asyncMemoryWorldStore(seed?: HostedWorldRecord): HostedWorldStore — Async in-process store for callers exercising the production persistence contract.
417
+ - `createHostedWorldSessionAsync` (function): function createHostedWorldSessionAsync<TAssetRef extends ModelAssetRef, TMultiplayer>(options: Omit<HostedWorldSessionOptions<TAssetRef, TMultiplayer>, "store"> & { store: HostedWorldStore }): Promise<HostedWorldSession> — Build a hosted session from an asynchronous persistence backend.
418
+
419
+ ## @jgengine/core/runtime/hostedWorldStore
420
+
421
+ - `HostedWorldRecord` (interface): interface HostedWorldRecord — One hosted world's persisted authoritative state — the unit a {@link HostedWorldStore} loads and saves.
422
+ - `HostedWorldStore` (interface): interface HostedWorldStore — Narrow persistence seam for a hosted world — the {@link HostedWorldRecord} counterpart of `HostPersistence`. Backends implement it (memory/file/sql/convex); the session never names one. A stateful host loads once and saves on a cadence; a stateless host reconstructs from `load()` each invocation.
423
+ - `SyncHostedWorldStore` (interface): interface SyncHostedWorldStore — Synchronous store adapter retained for deterministic in-process tests.
424
+
425
+ ## @jgengine/core/runtime/inputRecorder
426
+
427
+ - `InputRecorder` (interface): interface InputRecorder — Tick-indexed input log: record per tick, look up the frame in force at any tick.
428
+ - `InputRecorderState` (interface): interface InputRecorderState — Serializable recorder contents, sorted by tick.
429
+ - `RecordedInput` (interface): interface RecordedInput — An input frame and the first tick it applies to.
430
+ - `createInputRecorder` (function): function createInputRecorder(): InputRecorder — Tick-indexed input log for deterministic replay: record what each tick saw, then feed {@link frameAt} back into a runner stepping from a matching {@link createSimSnapshotRegistry} snapshot.
408
431
 
409
432
  ## @jgengine/core/runtime/inputSnapshot
410
433
 
@@ -440,6 +463,22 @@
440
463
  - `planScenarioReset` (function): function planScenarioReset(reset: ScenarioReset): NormalizedScenarioReset — ⚠ undocumented
441
464
  - `resetRun` (function): function resetRun<T extends Record<string, unknown>>(scoped: ScopedState<T>): ScopedState<T> — ⚠ undocumented
442
465
 
466
+ ## @jgengine/core/runtime/poseInterpolation
467
+
468
+ - `PoseHistory` (interface): interface PoseHistory — Previous-step pose buffer: capture at each fixed step, sample between steps for rendering.
469
+ - `PoseHistoryState` (interface): interface PoseHistoryState — Serializable previous-step poses keyed by entity id.
470
+ - `RenderPose` (type): type RenderPose = [x: number, y: number, z: number, rotationY: number] — A render pose sampled between two simulation steps: `[x, y, z, rotationY]`.
471
+ - `createPoseHistory` (function): function createPoseHistory(options?: { snapDistance?: number }): PoseHistory — Previous-step pose buffer for render interpolation under a fixed-step loop. Reuses one 4-slot array per entity so a step over N entities allocates only for entities seen for the first time.
472
+ - `lerpAngle` (function): function lerpAngle(from: number, to: number, t: number): number — Shortest-arc interpolation between two yaw angles.
473
+
474
+ ## @jgengine/core/runtime/prediction
475
+
476
+ - `PredictionBuffer` (type): type PredictionBuffer<S> = { record(input: InputFrame, dt?: number, observedState?: S): S; reconcile(tick: number, authoritative: S): { error: number; replayed: number; snapped: boolean; state: S }; state(): S; snapshot(): PredictionSnapshot<S>; restore(next: PredictionSnapshot<S>): void; } — Stateful prediction handle supporting recording, reconciliation, inspection, and restoration.
477
+ - `PredictionRecord` (type): type PredictionRecord<S> = { tick: number; input: InputFrame; state: S; dt: number } — One input, tick, and resulting state retained for authoritative replay.
478
+ - `PredictionSnapshot` (type): type PredictionSnapshot<S> = { confirmedTick: number; confirmed: S; predicted: S; records: PredictionRecord<S>[]; } — Serializable prediction state containing the confirmation baseline and pending input history.
479
+ - `PredictionStep` (type): type PredictionStep<S> = (state: S, input: InputFrame, dt: number) => S — Deterministic state transition used to advance one predicted input frame.
480
+ - `createPredictionBuffer` (function): function createPredictionBuffer<S>(config: { initial: S; step: PredictionStep<S>; maxTicks: number; startTick?: number; dt?: number; /** Error above which a caller should snap its rendered pose to the reconciled result. */ snapThreshold?: number; }): PredictionBuffer<S> — Keeps local input predictions ahead of the last server confirmation. Reconciliation replaces the confirmed state and deterministically replays unconfirmed inputs through the supplied step function. `maxTicks` bounds retained work and memory; dropped inputs are folded into the confirmed baseline.
481
+
443
482
  ## @jgengine/core/runtime/runtimeSave
444
483
 
445
484
  - `RuntimeSave` (interface): interface RuntimeSave — Whole-world save/load bound to a live world and a pluggable backend. `save()` captures `target.snapshot()` and writes it; `load()` reads it back and `target.hydrate()`s the whole world. In `autosave` mode it also writes on a trailing timer while the world keeps changing. Named slots, versioned migration, and offline↔cloud (backend swap) all come for free from the underlying save store.
@@ -454,18 +493,49 @@
454
493
  - `SaveConfig` (type): type SaveConfig = | "none" | { auto: string; scope: SaveScope; } — ⚠ undocumented
455
494
  - `SaveScope` (type): type SaveScope = "player" | "chunks" | "player+chunks" — ⚠ undocumented
456
495
 
496
+ ## @jgengine/core/runtime/simContext
497
+
498
+ - `SimContext` (interface): interface SimContext — `ctx.sim`: the simulation stepper shared by every driver plus the render-side interpolation it enables. Drivers call {@link advance} with real elapsed seconds and do their per-step work inside the callback; renderers call {@link renderPose} each frame.
499
+ - `SimContextState` (interface): interface SimContextState — Serializable `ctx.sim` state: loop position plus the interpolation pose history.
500
+ - `SimStage` (interface): interface SimStage — Extra work a game or engine seam (a physics backend, a netcode buffer) runs inside every simulation step.
501
+ - `SimStagePhase` (type): type SimStagePhase = "beforeMovement" | "afterMovement" | "afterTick" — Where a registered stage runs inside one simulation step.
502
+ - `createSimContext` (function): function createSimContext(options: { config?: SimulationConfig; entities: EntityStore }): SimContext — Build `ctx.sim` for a context; `createGameContext` calls this from `definition.simulation`.
503
+
504
+ ## @jgengine/core/runtime/simLoop
505
+
506
+ - `SimAdvanceResult` (interface): interface SimAdvanceResult — What one {@link SimLoop.advance} did.
507
+ - `SimLoop` (interface): interface SimLoop — The stepper handle: advance with real time, read the tick and interpolation alpha, retune the rate, snapshot and restore.
508
+ - `SimLoopState` (interface): interface SimLoopState — Serializable loop position: the tick count and the real seconds accumulated toward the next fixed step.
509
+ - `SimulationConfig` (interface): interface SimulationConfig — How the simulation is stepped: a fixed rate with a catch-up cap, or once per frame with the frame's dt.
510
+ - `createSimLoop` (function): function createSimLoop(initial?: SimulationConfig): SimLoop — The simulation stepper every driver (shell frame, headless runner, authoritative host) advances through, so the sim sees the same dt sequence regardless of who drives it. Fixed mode accumulates real time and steps in equal slices with a catch-up cap; variable mode steps once per advance.
511
+
512
+ ## @jgengine/core/runtime/simSnapshot
513
+
514
+ - `SimSnapshot` (type): type SimSnapshot = Record<string, unknown> — Captured contributor data keyed by contributor id.
515
+ - `SimSnapshotContributor` (interface): interface SimSnapshotContributor<T = unknown> — One piece of simulation state that can be captured and restored by id.
516
+ - `SimSnapshotRegistry` (interface): interface SimSnapshotRegistry — Registry of {@link SimSnapshotContributor}s that captures and restores them together.
517
+ - `createContextSimSnapshot` (function): function createContextSimSnapshot(ctx: GameContext): SimSnapshotRegistry — The engine's own contributors for a context: replicated world state, the sim clock, the loop and pose history, and the rng cursor when `ctx.rng` is a seeded stream. Games register theirs on the same registry.
518
+ - `createSimSnapshotRegistry` (function): function createSimSnapshotRegistry(): SimSnapshotRegistry — A registry of everything that must round-trip for a simulation to resume bit-for-bit: world state, the clock, the rng cursor, the loop accumulator, and whatever a physics backend or game adds. Pair with {@link createInputRecorder} for replay.
519
+
457
520
  ## @jgengine/core/runtime/snapshot
458
521
 
459
522
  - `GameRuntimeSnapshot` (type): type GameRuntimeSnapshot = { version: number; gameId: string; serverId: string; server: RuntimeServerRow; players: Record<string, RuntimePlayerRow>; chunks: Record<string, RuntimeChunkRow>; revision: number; dirty: { server: boolean; players: string[]; chunks: string[]; }; } — ⚠ undocumented
460
523
  - `RUNTIME_SNAPSHOT_VERSION` (const): const RUNTIME_SNAPSHOT_VERSION: 1 — ⚠ undocumented
461
- - `RuntimeChunkRow` (type): type RuntimeChunkRow = { chunkKey: string; objects: RuntimeObjectRow[]; entities: RuntimeEntityRow[]; flags?: Record<string, unknown>; } — ⚠ undocumented
524
+ - `RuntimeChunkRow` (type): type RuntimeChunkRow = { /** Cell key to owner id, persisted with its spatial chunk. */ territory?: Record<string, string>; territoryReceipts?: Record<string, { claimedAt: number; costPaid: number }>; chunkKey: string; objects: RuntimeObjectRow[]; entities: RuntimeEntityRow[]; flags?: Record<string,… — ⚠ undocumented
462
525
  - `RuntimeEntityRow` (type): type RuntimeEntityRow = { instanceId: string; catalogId: string; position?: [number, number, number]; rotationY?: number; parentSpace?: string; group?: string; stats?: Record<string, { current: number; max: number; min?: number }>; targetInstanceId?: string | null; userId?: string; } — ⚠ undocumented
463
526
  - `RuntimeInventorySlot` (type): type RuntimeInventorySlot = { item: string; count: number; slot?: number; } — ⚠ undocumented
464
527
  - `RuntimeObjectRow` (type): type RuntimeObjectRow = { instanceId: string; catalogId: string; position: [number, number, number]; rotationY?: number; parentSpace?: string; /** Opaque per-instance state; the persisted name for `SceneObject.state`. */ flags?: Record<string, unknown>; /** Container contents for a catalog `slotInve… — The persisted form of a placed `SceneObject`: identity, placement, per-instance state, and slot contents. Convert with `toRuntimeObjectRow`/`fromRuntimeObjectRow` (`runtime/objectRows`).
465
- - `RuntimePlayerRow` (type): type RuntimePlayerRow = { userId: string; inventories: Record<string, RuntimeInventorySlot[]>; economy: Record<string, number>; unlocks: string[]; quests?: unknown; social?: unknown; leaderboard?: Record<string, number>; session?: Record<string, unknown>; } — ⚠ undocumented
528
+ - `RuntimePlayerRow` (type): type RuntimePlayerRow = { territoryOwnedCount?: number; ownedTerritoryChunkKeys?: string[]; userId: string; inventories: Record<string, RuntimeInventorySlot[]>; economy: Record<string, number>; unlocks: string[]; quests?: unknown; social?: unknown; leaderboard?: Record<string, number>; session?: Rec… — ⚠ undocumented
466
529
  - `RuntimeProfileRow` (type): type RuntimeProfileRow = { userId: string; gameId: string; player: RuntimePlayerRow; updatedAt: number; } — ⚠ undocumented
467
530
  - `RuntimeServerRow` (type): type RuntimeServerRow = { entities: RuntimeEntityRow[]; objects: RuntimeObjectRow[]; session: Record<string, unknown>; feeds?: Record<string, unknown[]>; } — ⚠ undocumented
468
531
 
532
+ ## @jgengine/core/runtime/snapshotBuffer
533
+
534
+ - `SnapshotBuffer` (type): type SnapshotBuffer<T> = { push(atMs: number, value: T): void; sampleAt(nowMs: number): SnapshotBufferSample<T> | null; snapshot(): SnapshotBufferState<T>; restore(next: SnapshotBufferState<T>): void; retune(config: { delayMs?: number; capacity?: number }): void; } — Handle for a timestamped delayed snapshot buffer.
535
+ - `SnapshotBufferSample` (type): type SnapshotBufferSample<T> = { before: T; after: T; alpha: number; } — A sampled pair of snapshots and interpolation factor.
536
+ - `SnapshotBufferState` (type): type SnapshotBufferState<T> = { delayMs: number; capacity: number; entries: Array<{ atMs: number; value: T }>; } — Serializable snapshot-buffer state.
537
+ - `createSnapshotBuffer` (function): function createSnapshotBuffer<T>(config: { delayMs: number; capacity: number; }): SnapshotBuffer<T> — Stores timestamped snapshots and samples a delayed, interpolatable pair.
538
+
469
539
  ## @jgengine/core/runtime/transport
470
540
 
471
541
  - `FeedUnsubscribe` (type): type FeedUnsubscribe = () => void — ⚠ undocumented
@@ -473,15 +543,19 @@
473
543
  - `GameRuntimeFeedView` (type): type GameRuntimeFeedView = { action: string; entries: unknown[]; } — ⚠ undocumented
474
544
  - `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
475
545
  - `GameRuntimePlayerView` (type): type GameRuntimePlayerView = { userId: string; gameId: string; playerState: unknown; updatedAt: number; } — ⚠ undocumented
476
- - `GameRuntimeServerView` (type): type GameRuntimeServerView = { serverId: string; gameId: string; revision: number; memberUserIds: string[]; serverState: unknown; updatedAt: number; } — ⚠ undocumented
477
- - `GameRuntimeTransport` (type): type GameRuntimeTransport = { joinServer: (args: { gameId: string; serverId?: string }) => Promise<JoinServerResult>; leaveServer: (args: { serverId: string }) => Promise<void>; runCommand: (args: RunCommandArgs) => Promise<TransportRunCommandResult>; } — ⚠ undocumented
478
- - `JoinServerResult` (type): type JoinServerResult = { serverId: string; isNew: boolean; } — ⚠ undocumented
546
+ - `GameRuntimeServerView` (type): type GameRuntimeServerView = { serverId: string; gameId: string; revision: number; memberUserIds: string[]; serverState: unknown | WorldSyncFrame; updatedAt: number; } — ⚠ undocumented
547
+ - `GameRuntimeTransport` (type): type GameRuntimeTransport = { joinServer: (args: { gameId: string; serverId?: string; role?: MultiplayerRole }) => Promise<JoinServerOutcome>; leaveServer: (args: { serverId: string }) => Promise<void>; runCommand: (args: RunCommandArgs) => Promise<TransportRunCommandResult>; } — ⚠ undocumented
548
+ - `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.
549
+ - `JoinServerResult` (type): type JoinServerResult = { serverId: string; isNew: boolean; resumeTicket?: ResumeTicket; } — ⚠ undocumented
479
550
  - `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
551
+ - `MultiplayerRole` (type): type MultiplayerRole = "player" | "spectator" — Connection role used for authoritative world access; spectators are read-only.
480
552
  - `MultiplayerSession` (type): type MultiplayerSession = { gameId: string; userId: string; backend: LiveGameBackend; feedActions: string[]; } — ⚠ undocumented
481
553
  - `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
482
554
  - `PresenceSync` (type): type PresenceSync = { subscribe: (serverId: string, onChange: (rows: PresencePoseRow[]) => void) => FeedUnsubscribe; syncPose: (serverId: string, pose: PlayerPose) => void; } — ⚠ undocumented
555
+ - `ResumeTicket` (type): type ResumeTicket = { userId: string; serverId: string; token: string; } — A short-lived credential allowing a disconnected client to reclaim one server membership.
483
556
  - `RunCommandArgs` (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; } — ⚠ undocumented
484
557
  - `TransportRunCommandResult` (type): type TransportRunCommandResult = | { ok: true } | { ok: false; reason: string } — ⚠ undocumented
558
+ - `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.
485
559
 
486
560
  ## @jgengine/core/runtime/visibility
487
561
 
@@ -510,6 +584,7 @@
510
584
  - `chunkCoordAt` (function): function chunkCoordAt(position: readonly [number, number, number], size: number = DEFAULT_CHUNK_SIZE): ChunkCoord — Chunk coordinates containing a world position at `size` units per cell.
511
585
  - `chunkKeyAt` (function): function chunkKeyAt(position: readonly [number, number, number], size: number = DEFAULT_CHUNK_SIZE): string — Chunk key containing a world position at `size` units per cell.
512
586
  - `chunkKeyOf` (function): function chunkKeyOf(coord: ChunkCoord): string — Chunk key for a cell — the canonical `"cx,cz"` string a `jgWorldChunks` row is stored under. Games used to invent this mapping each time, which made two systems in one game disagree about which row a position belongs to.
587
+ - `chunkKeysAround` (function): function chunkKeysAround(position: readonly [number, number, number], rings = 1, size = DEFAULT_CHUNK_SIZE): string[] — Square chunk neighborhood around a world position, including its own chunk.
513
588
  - `chunkKeysInRadius` (function): function chunkKeysInRadius(center: readonly [number, number, number], radius: number, size: number = DEFAULT_CHUNK_SIZE): string[] — Every chunk key whose cell intersects a radius around a world position — the key list a viewer's bounded hydration or client chunk query asks for, instead of loading a whole world.
514
589
  - `createEmptyChunkRow` (function): function createEmptyChunkRow(chunkKey: string): RuntimeChunkRow — An empty chunk row for `chunkKey` — the starting value for a cell nothing has been placed in yet.
515
590
  - `deleteChunk` (function): function deleteChunk(snapshot: GameRuntimeSnapshot, chunkKey: string): GameRuntimeSnapshot — Drop a chunk from a snapshot and mark the key dirty so the host deletes its stored row.
@@ -575,5 +650,5 @@
575
650
  - `environmentContentFromDocument` (function): function environmentContentFromDocument(doc: EditorDocument, options?: EnvironmentContentOptions): EnvironmentContent — Derives the coordinate/placement content of an `environment()` world from its scene document: terrain footprint (via {@link terrainBoundsFromDocument}), ground clearings under authored spawns/POIs (via `clearanceZonesFrom`), and the document's sculpt snapshot. This is the seam that lets a game author its world footprint and flatten regions in the editor instead of hardcoding them — the `world.ts` that consumes it carries only engine tuning.
576
651
  - `grounded` (function): function grounded(ctx: Pick<GameContext, "world">, x: number, z: number): readonly [number, number, number] — Ground an XZ point onto the live world surface as `[x, groundY, z]`.
577
652
  - `offline` (function): function offline(): MultiplayerAdapterConfig — Explicit single-player adapter. Solo games never need this — omitting `multiplayer` in the shell `defineGame` already defaults to offline; pass it only where an adapter value is structurally required.
578
- - `seededRng` (function): function seededRng(seed: string | number): () => number — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
653
+ - `seededRng` (function): function seededRng(seed: string | number): SeededRng — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
579
654
  - `useHudLayout` (function): function useHudLayout(options?: { storageKey?: string; snap?: number; locked?: boolean; /** * Scene-document `ui` section — source of truth for panel placement/size. * When provided, hydrates the layout store (and wins over legacy localStorage). */ documentUi?: EditorUiDocument; /** * When true (def… — Layout state for `HudCanvas` — panel placements, edit-mode drag/resize, and per-game persistence.