---
name: jgengine-world
description: Build spatial runtime, movement, interaction, AI, and environments.
---

# JGengine world runtime

## Ownership

This skill owns runtime spatial behavior: authored-scene consumption, environments, movement, input mapping, interaction, AI, navigation, physics, visibility, time, and spatial audio. Scene creation and placement stay in `jgengine-editor`; presentation stays in `jgengine-ui`.

Search [capabilities.md](capabilities.md) by intent before designing a primitive. Use [api.md](api.md) for exact exports and [reference.md](reference.md) for environment, camera, navigation, physics, audio, and authored-scene recipes. Culling and streaming overrides are in [visibility.md](visibility.md).

For a *connected* walkthrough that wires several primitives into a running loop, read the goal-oriented recipes under [recipes/](recipes/) — organized by composition seam (what connects to what), never by genre. Start there before reading a game's source to learn how pieces fit; a hybrid game is just a different composition of the same primitives.

- [recipes/commanding-entities.md](recipes/commanding-entities.md) — select → order → tick → resolve: direct command over a group of entities (the command loop behind squad control, base-defense, herding, autobattle, or a mutating game's real-time phase).
- [recipes/npc-senses.md](recipes/npc-senses.md) — perceive → remember → decide → route: perception memory written into a decision-graph blackboard, with nav-mesh routing (guards, mobs, pedestrians, police response).
- [recipes/building-kit.md](recipes/building-kit.md) — bind the facade generator's part slots to real models: one massing, swappable art per style, blocks where the kit is silent.
- [recipes/vehicle-feel.md](recipes/vehicle-feel.md) — feel target → handling metrics → physical knobs for a ground vehicle, plus the camera, sound and rumble hooks that hang off its telemetry.
- [recipes/flight-feel.md](recipes/flight-feel.md) — an aircraft as physical numbers: surfaces, inertia and engine, with the symptom → knob table for how it flies.
- [recipes/controls.md](recipes/controls.md) — actions → layered contexts that rebind live → keyboard, touch and gamepad on the same actions → per-axis feel.
- [recipes/character-animation.md](recipes/character-animation.md) — clips → graph → rig → ground: modular instance visibility, data-driven state machines, triggers and clip events, root motion, foot IK that keeps feet on slopes, and instanced crowds.

## Canonical workflows

### Declare the place

A world is the place you play in: substrate + laws, via `world()` from `@jgengine/core/world/place` — `{ id, ground: { mode: flat|round|voxel|board, size, surface?, generator? }, physics? }`. Thin is complete: flat with `Infinity` axes for 3D, `board` for 2D, or omit `world` for pure UI/rules games. Never put seed, sky, or vegetation in a world definition — seeds derive from world id + save/run (`seedForPlace`); sky look and foliage scatter are editor-authored scene content, with the engine default sky when the document has none. `ground.surface` carries matter/feel laws (metal vs slime vs felt) that the same rule systems read — swap the surface, not the rules. See [reference.md](reference.md) for the full shape; `environment()` is the consumption path for editor/preset-written scene data: a created 3D game's `world.ts` passes it the document's sculpt, sky, and clearings through `environmentContentFromDocument`, never code-declared noise or coordinates.

### Authored world

1. Load the scene document through the shared authored-scene feature.
2. Render objects, paths, terrain, foliage, and markers generically.
3. Query the same ids/layers for spawns, routes, plots, and interaction. The shell batches compatible static catalog props above eight placements per 24 m cell, preserving chosen models, material groups, shadows, measured colliders and object ids for picking. Interactive/custom props, per-object visual/style overrides, and animated, rigged, composed, transparent or mirrored models keep individual renderers. Runtime compatibility changes return a placement to its individual renderer. Visibility compacts each batch to the current visible placements.
4. Collision for generated and studio content comes from its data through `ctx.world.solids`; never hand-place invisible blockers for it (see reference.md, World solids).
5. Keep derived caches rebuildable; the document remains authoritative.

For existing `environment()` consumers, `defineGame` overlays only defined editor sky fields on the legacy sky, retaining settings the editor cannot represent; an explicit `backdrop.sky` remains authoritative.

For environment-driven surface appearance, sample only selected targets through `material/appearanceSignals`; wetness already includes shelter. Capture the assigned material's dry baseline, apply explicit wet endpoints, and restore it on unbinding. Keep wind in geometry/simulation consumers and local card backlighting separate from scene exposure. See [reference.md](reference.md#environment-fields-weather-hooks--realm-composition).

### Movement and interaction

Choose input intent, controller/motor, collision/navigation, and camera as separate seams. Commands express game intent; world systems resolve motion. Interaction targets come from bounded spatial/sensor queries, not full-world scans.

Walk feel is data, not code. `movement.feel` sets `groundAcceleration`, `airAcceleration` and `groundFriction` (response rates, 1/s) plus `runMultiplier`, `crouchMultiplier`, jump forgiveness (`jumpBufferMs`, `coyoteMs`) and jump shape (`jumpCutFactor`, `apexGravityScale`, `fallGravityScale`, `landingRecoveryMs`), and `physics` sets `gravity` and `jumpVelocity`. The heightfield and capsule paths share these feel controls; voxel jumping retains its press-frame policy. Crouch follows supported catalog poses, and capsule standing requires headroom. Pin numeric movement behavior with `measureMovement` (`movement/movementProbe`) in a test.

### Stateful placed objects

Per-instance state belongs on the placement, not in a game-owned map keyed by `instanceId`. Set `SceneObject.state` (opaque) through `ctx.scene.object.setState`/`patchState`; declare `slotInventory` on the catalog entry and mutate contents through `ctx.scene.object.slots`, which enforces the declared capacity and `accepts`. Both replicate and save with the object through the always-on `objects` snapshot module, and `toRuntimeObjectRow`/`fromRuntimeObjectRow` (`@jgengine/core/multiplayer`) convert to and from the host's persisted rows.

### AI and navigation

Compose perception, selection, planning/behavior, movement, and lifecycle independently. Inject randomness and scheduling. Use spatial indexes, interest tiers, or bounded candidate sets for scale.

For chase-to-reach and cooldown effects, attach `pursue({ aggroRadius, reach, leashRange, speed, attack })` as behavior data. The host schedules acquisition and solid-aware movement; `behaviorControl` exposes pause/reset and optional threat. Pursuit progress survives GameContext save/replication without a game-local home/cooldown map. For allies or neutral players, configure `defineGame({ pursuit: { eligible(ctx, selfId, candidateId) { /* bounded caller policy */ } } })`; the predicate receives the current world and constrains nearest, held, explicit and forced targets. Bind the receiving world's policy data before advancing after a cold restore. Omission keeps the all-player default; see the reference for faction composition. Use a decision graph and navigation actions when a route or richer perception policy is required.

NPC decisions are a `decisionGraph` behavior: the graph and its blackboard are data, actions are registered by name, and perception or game systems write facts into `behaviorControl(ctx).blackboard(id)` rather than actions querying the world. Set `thinkInterval` so crowds think a few times a second, not every frame, and release movement or claims in `onAbort`.

For a bounded local detour over movement collision geometry, use `planSolidRoute` (`nav/solidRoute`) with an indexed `solidObstaclesNear` or `sourceObstaclesNear` query. Handle `no-path` and `budget` separately; retain caller movement, goals, terrain and retry policy. See [reference.md](reference.md#local-collision-routes) for limits.

Route on an editor-baked polygon nav mesh through `createNavRuntime` (`nav/navRuntime`): read the bake off the scene document with `navMeshFromBake(document.bakes)`, price terrain and gate doors with `areaCosts`, author doors, ladders and jumps as `AuthoredNavLink` world points, and call `applyMesh(nextBake)` after an edit — polygons that survive keep their identity through the returned `remap`, so only routes crossing changed polygons need re-planning (`createNavCrowd`'s `revalidate`). A bare `createNavMeshQuery` per mesh is still the lower-level seam; keep `maxNodes` bounded so a long request returns a `partial` route instead of stalling a frame.

Move many agents across that mesh with `createNavCrowd` (`nav/navCrowd`): it plans corridors, follows the funnel-smoothed waypoints, keeps them apart with `createRvoCrowd` (`ai/rvo`) reciprocal velocity obstacles, bounds path searches to a per-tick time and count budget, and serializes through `snapshot`/`restore`. Give a crowd converging on one spot a `spread` radius so agents fan out (`ai/crowd`'s stable per-id offset) instead of stacking. Avoidance keeps agents inside the mesh's outer bounds; interior walls remain the game's collision layer.

### Street traffic

Vehicles on roads do not path-find: they follow authored lane spans. `createLaneGraph` (`nav/laneGraph`) resolves those spans — `traffic_lane` paths, or `road` paths from the editor's street bake, split where streets cross — into junctions, crossings with a signal program or first-come right-of-way, and parking spots; `laneGraphFromDocument` reads that straight off the scene document, so editing a road moves its traffic. `createTraffic` (`nav/traffic`) then drives cars on it: IDM car following and braking, one car at a time through each intersection box (only once the exit lane has room, which is what keeps a blocked crossing from wedging the network), signals with phases, lane changes across authored `left`/`right` neighbours, parking that reserves a spot, and `setDemand(count)` for fleet size — the time-of-day curve is the caller's, not the engine's. Cars are posed, not driven: when a player or a chase needs real handling, feed intent into `physics/kinematicVehicle` and let this seam keep the ambient city.

### Heat and police response

A crime only counts when a witness holds the suspect. `createPoliceResponse` (`ai/policeResponse`) reads witness facts from `sensor/perception` memory — `memoryOf` per observer, keeping the sighting's `sourceId` and `sourceTick` — and adds heat on the shared `ai/heatSystem` curve, so there is no second sight path and no second decay. The caller's tier table decides what police do at each heat: patrol, investigate, pursue, roadblock, with a pursuer budget per tier. The search area sits on the suspect's last known position, grows while the trail is cold, and a roadblock tier cordons it with sites `policeRoadsFromLaneGraph` reads off the lane graph (`traffic_roadblock` markers projected onto the lane they block, else the graph's intersections). Breaking sight for `escapeSeconds` is what starts the decay; standing in view keeps the heat. Heat, curve, tiers and search shape are caller policy with defaults, `state()`/`restore()` round-trip mid-chase, and the injected generator is the only source of variety.

### Fallback-seam diagnostics

Render seams fall back to placeholders when content is unauthored: default green ground (no environment), primitive capsule/box actors (no `entityModels`/`objectModels` mapping), stylized proxy foliage (no scatter `resolveItem`). These placeholders are sometimes intended, so runtime never changes — but a dev-only, opt-in probe (`@jgengine/core/devtools/fallbackSeams`) tells "unauthored/misconfigured" apart from "intended". The shell arms it via `armFallbackSeams(devtoolsEnabled)`; each seam reports at its choice site (`reportFallbackSeam`), and counts surface through the devtools `fallbacks` probe. Disarmed (production) it is a pure, allocation-free no-op.

## Traps

- Do not put seed, sky presets, or vegetation/scatter fields in a world definition — `world()` rejects them; dressing belongs to the editor's scene document.
- Do not put UI layout, touch chrome, combat resolution, or asset acquisition here.
- Do not duplicate editor coordinates in runtime state.
- Public APIs accept semantic policies and caller data, not renderer mechanics or internal tuning rolls.
- Generated environments still need deterministic data assertions; authored environments need scene-document assertions.
- Bound SFX through `PlayableGame.audio.maxVoices` and game-owned `SoundDef.priority`/`maxVoices`; choose reject or deterministic stealing with `voiceOverflow`. See the spatial audio reference for custom hosts and cleanup.
- Audio files and licensing belong to `jgengine-assets`; spatial playback policy belongs here.


### Territory and placement

Use `world/territory` for chunk-backed ownership: `planFootprintClaims` prices previews, `placeWithTerritory` commits claims and object placement atomically, and `createTerritory` binds host-owned snapshot storage to `ctx.game.territory`. Use `territoryChunkKeys` to load every footprint and gap-neighbor chunk before planning; never interpret an unloaded neighborhood as vacant. Claims persist in `RuntimeChunkRow.territory`; `RuntimePlayerRow.territoryOwnedCount` preserves the price curve and `ownedTerritoryChunkKeys` bounds owner queries. Convex hosts use `loadTerritoryState`/`persistTerritoryState` from `@jgengine/convex/territory` to commit dirty claim chunks/profiles without a shared-server write. Grant `starterBlock` before the first placement verb. Placement previews must show the claim cost and affordability on the placement surface.

Use `chunkKeysAround(position, rings)` for bounded command/presence reads; declare `commands[name].scope(input)` for every hosted command. Missing scopes load only the actor and no chunks. A whole-world command must opt in explicitly with `scope: () => ({})` and justify its read budget.
