# Changelog

The lockstep game SDK set is `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets}`
(this file’s 0.x.y releases). The CLI package `jgengine` and `@jgengine/github` use
**separate** version lines and may lag. Format follows [Keep a Changelog](https://keepachangelog.com);
each release **leads with a Migrate block** — the concrete steps to move a game
from the previous version onto the new APIs — because the point of a bump is to
let consumers pick up the better stuff, not just to list what moved.

Agents building on the published SDK can also read this programmatically:
`import { VERSION, CHANGELOG } from "@jgengine/core/meta/changelog"` gives the
same data as typed values, so an updater can diff its installed version against
the latest and surface the migration steps. Easier still: `npx jgengine upgrade`
run inside a game project diffs the installed `@jgengine/*` versions against the
latest release and prints every Migrate step and Adopt-worthy addition in
between (`--json` for structured output).

## [Unreleased]

<!--
Every PR that changes `packages/*/src` records its consumer-facing change here, so
the next release's notes are complete by construction. Add a bullet under the right
subhead below. `bun run check-changelog` fails a source PR whose `[Unreleased]` block
is untouched; pure refactors/tests bypass with `[skip changelog]` in a commit message.
At publish, rename this heading to the new version and mirror the entries into
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
-->

## 0.32.0

### Migrate

- **Bump lockstep SDK packages to `^0.32.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.29.0`; `@jgengine/github` is `0.19.0`.

### Added

- Audio mixing grew a mute that composes with gain: `AudioEngine.setBusMute` / `setMasterMute` keep the player's volume while silencing a bus, `masterLevel()` / `busLevel()` report the composed value, and Settings carries a mute row per bus plus "Mute all" (`sound.muted`, `sound.bus.<id>.muted`). `AudioEngine.play` and `playOneShot` hold cues in a bounded 16-slot queue while the browser keeps the context suspended and play them on the first gesture instead of dropping them; `installGestureResume` wires that gesture for both presentation paths.
- `AudioEngine.measure()` reads master RMS and peak from a lazily built analyser, and `installAudioProbe` publishes `window.__jgengineAudio` so a headless capture can prove sound with a measured level rather than a screenshot.
- `@jgengine/core/audio/feedbackCues` ships seven procedural reactions - `hit`, `hurt`, `kill`, `pickup`, `objective`, `wave`, `deny` - on an `sfx` bus, merged into every game's catalog as `feel.<cue>`. A game sound with that id replaces one; `audio.feedbackCues` replaces or silences them.
- `@jgengine/shell/audio/eventFeedback` maps the events the engine already announces to those reactions: `combat.damage` (local player reads as hurt, anything else as a hit), `entity.died`, `worldItem.picked_up`, `quest.completed`, `stat.levelUp` and `command.refused`. Each row carries a sound, a gamepad pulse, camera trauma and a caption; the mapping and reactions are data (`DEFAULT_FEEDBACK_BINDINGS`, `DEFAULT_FEEDBACK_REACTIONS`) a game can replace, cooldowns fold a burst into one impact, reduced motion keeps sound and rumble but drops the camera kick, and the resolved reaction is pooled so handling an event allocates nothing per frame. `attachWaveFeedback(runner, ctx.game.feedback)` announces each new wave.
- `ctx.game.feedback.cue(cue, caption?)` announces a moment no engine event covers and emits `feedback.cue`.
- `@jgengine/shell/audio/captions` and `CaptionOverlay` render a bounded, deduping, self-expiring caption line under the accessibility captions setting; `FeedbackHost` mounts the whole layer - feedback mapping, gesture resume, audio probe and captions - for both the 3D and HUD presentation paths.

### Changed

- The CI gate runs nine test shards instead of ten, and a `types` job whose stages overlap, so a docs-only pull request fits the account's 20-job runner cap with room for another run beside it. `scripts/test-shard.ts` owns which files each shard runs and what it prepares first; `bun scripts/type-shard.ts` runs the `check-types` plan two stages at a time and then the tsc fan-out alone.

## 0.31.0

### Migrate

- **Bump lockstep SDK packages to `^0.31.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.28.0`; `@jgengine/github` is `0.18.0`.

### Added

- `createAgent` (`@jgengine/core/agents/agent`) runs one resident's whole loop on the world clock: needs decay through `sim/needs`, a utility brain picks among the targets the game offers, an authored day plan says where the agent belongs, and the agent falls back to a declared target when its pick disappears. Mood, action and the far flag snapshot and restore with the needs.
- `createMoodReader` / `readMood` (`@jgengine/core/agents/mood`) derive a 0-100 mood and a caller-labelled tier from weighted need satisfaction, plus named sources a queue, a litter patch or a cold snap raises and clears.
- `createAgentSchedule` (`@jgengine/core/agents/schedule`) resolves an authored day plan through `sim/routine` and adds a work beat: a stop names the places it patrols, so a janitor is an agent with a route rather than a special case. Off-hours behaviour is a stop without a beat.
- `createRelationshipBook` (`@jgengine/core/agents/relationships`) keeps undirected familiarity and opinion per pair. A social action moves both and reports the milestones crossed, friends prefer each other's company, and a bonded pair refuses to split. Pair keys come from `relation/keyedValues` and crossings from `relation/thresholds`.
- `GameSave.deleteSlot(slot)` and `SaveStore.deleteSlot(slot)` remove one slot's payload and drop it from the slot index; they resolve `true` when a document was removed and `false` for an unknown slot. Deleting the active slot resets the live state to the initial document. `load()` no longer lists a slot that holds nothing, so a deleted slot cannot return.
- `HudPanel` takes `lane` (`@jgengine/react`): blocks naming the same anchor and lane stack without overlap, lanes in one anchor sit side by side, and `resolveHudLanes` resolves the rectangles. A moved panel leaves its lane and keeps its authored placement.
- `hudBlockOverlaps` and `formatHudOverlapReport` (`@jgengine/react`) name every pair of rendered HUD blocks whose rectangles intersect, with both ids and the viewport; `HudCanvas` warns with that report in a dev build.
- `rollDrop` (`@jgengine/core/loot/drop`) rolls one deterministic item from a seed and `compareToEquipped` turns it into per-stat deltas against worn gear with a weighted verdict a HUD can render.
- `createRunHaul` (`@jgengine/core/loot/haul`) gates carried loot by weight through the shared encumbrance rules, banks what the persistent stash fits, and discards the run's haul when the game calls `fail`.
- `salvageItem` and `createItemLedger` (`@jgengine/core/loot/salvage`) break an item down for the currency its level and rarity say and claim the instance, so a salvaged item cannot be salvaged, equipped or sold again.
- `stockVendor` and `createVendorRotation` (`@jgengine/core/loot/vendor`) fill a `ShopStock` with rolled items priced through the economy wallet, with a run-count restock rule the game authors.
- `markerProximity` on `createMissionRuntime` picks the plane a marker proximity test compares in: `"horizontal"` (default) measures ground distance and allows the actor a `tolerance` (default 8 m) above or below the marker, `"spatial"` is the straight 3D check for worlds with stacked platforms. `MissionMarkerPlane`, `MissionMarkerProximity` and `DEFAULT_MARKER_TOLERANCE` are exported with it.

### Changed

- `evaluatePressure` (`@jgengine/core/ai/director/pressure`) reads pressure as player control: a healthy player further into a run is leaned on harder, a hurt player is eased off, and every effect stays inside the rule's `maxTier`.
- Loot bases and affixes take `minLevel`, so `rollLoot` only draws from entries at or below the item level. Loot bases take `mass`; rolled items carry it for carry checks.

### Fixed

- `HudCanvas` resolved its host with `canvas.parentElement`, which is `HudThemeRoot`'s `display: contents` element and measures 0x0 — the boundary overlay, the HUD-fit measurement and the new overlap report never ran under the standard HUD root. It now uses the nearest ancestor with a real box.
- The HUD boundary check returned early whenever nothing overflowed, so a later report in the same pass could never run.
- `reach`, `interact` and the `left-area` fail condition compared a marker in 3D, so a marker authored at `y=0` under rising ground was unreachable. They now read the same plane.

## 0.30.0

### Migrate

- **Bump lockstep SDK packages to `^0.30.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.27.0`; `@jgengine/github` is `0.17.0`.

### Added

- `ColorblindFrame` (`@jgengine/react/accessibility`) wraps the 3D canvas and applies the player's colour-vision mode to that box alone, so HUD, menu and caption text keep their own contrast. `colorblindFrameFilter` (`@jgengine/core/ui/accessibility`) resolves a mode to the CSS filter, and `useColorblindMode` reads the mode from the settings store the settings rows write; the 3D shell does both for the mounted game.
- `SettingsSection` (`@jgengine/react/settingsBlocks`) is one `useMenuNav` menu: a d-pad or left stick moves between rows and left/right steps the focused value — slider by its step, toggle flipped, select to the next option, confirm arms a keybind — through the same `onAdjust` path the arrow keys use. `onAdjust` (`@jgengine/react/menuNav`) is the option that makes left/right adjust instead of move.
- `createStanding` (`@jgengine/core/progression/standing`) holds per-faction or per-district standing over authored tracks and moves it from named outcomes, so an outcome that resolves to nothing throws at the call instead of silently moving nothing.
- `createGateSet` (`@jgengine/core/progression/gates`) evaluates authored milestones against standing and money, and refuses to load when a gate's unlock id resolves to nothing.
- `createCareerBank` (`@jgengine/core/progression/careerBank`) keeps persistent career money apart from a run's haul, so a failed run loses the haul and keeps the bank.
- `createUpgradeTree` (`@jgengine/core/progression/upgrades`) prices each rank from the shared curve primitive and gates purchases on prerequisites and standing.

### Changed

- `AccessibilityProvider` (`@jgengine/react/accessibility`) sets its data attributes and text-scale var without filtering its own subtree. A colour matrix over the HUD and menu root recoloured the text the rest of the UI assumes, so wrap the game canvas in `ColorblindFrame` instead; a custom root can still offset it with its own `ColorblindFilters` and `filter: url(#jg-cb-<mode>)`.
- World health bars and nameplates derive their range from the render camera and the target height when a game sets no `maxDistance`: an overhead camera reaches the units it looks down at, and a bar is kept while the target it annotates still covers 18 px, instead of hiding every target past a fixed 60 units.
- A configured `worldHealthBars.maxDistance` or `nameplates.maxDistance` that hides every target in view now warns once with the nearest measured distance and the prop to raise, instead of rendering nothing.

## 0.29.0

### Migrate

- **Bump lockstep SDK packages to `^0.29.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.26.0`; `@jgengine/github` is `0.16.0`.

### Fixed

- `MissionView.remaining` (`@jgengine/core/gameplay`) now counts down from a `survive` objective's own `duration`, not only from a `timeout`. A defend or hold objective reports the seconds it still has to run, which is the one number its HUD needs; a completed run still reports null.

## 0.28.0

### Migrate

- **Bump lockstep SDK packages to `^0.28.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.25.0`; `@jgengine/github` is `0.15.0`.
- `groupStaticObjects` (`@jgengine/shell/world/staticObjectBatches`) takes an explicit `cell` argument (default `STATIC_BATCH_CELL`), and `STATIC_BATCH_CHUNK` is now `STATIC_BATCH_CELL`. `STATIC_BATCH_MIN` is 2. A caller that passed a chunk size to pick the grouping keeps working by passing it as the third argument.
- `createNearAgents` (`@jgengine/core/gameplay`) `snapshot()`/`restore()` now carry the crowd as well as the agents: `{ agents, crowd }` where `crowd` is a `NavCrowdSnapshot`. A restored near agent resumes the route it was walking instead of re-planning a fresh one, which is what makes an off-screen world round-trip tick-exactly.
- `OffScreenSnapshot.residents[].staleAt` is `number | null`; `null` is the "never goes stale" sentinel that was `Infinity`. `Infinity` does not survive JSON, so a saved off-screen world decayed a far, routine-less resident on every load.
- `TrafficSnapshot.counters` and `PoliceResponseState.counters` no longer carry `slowestTickMs`. A wall-clock timing sample is not state; it stays on `stats()` only, so a save document is byte-deterministic.
- `MaterialMaps.displacement` (`@jgengine/assets`) and `TerrainMaterialMaps.displacement` (`@jgengine/core/world/features`) are optional now. A caller that used to invent a displacement URL can drop it; nothing reads it.

### Added

- `setModelSubMeshMerge` (`@jgengine/shell/render/modelMeshMerge`) turns load-time sub-mesh merging off for a game that must keep one node per source mesh.
- `render/staticBatches` devtools system probe reports the grouped batches, the instances behind them, the draws submitted and the distinct instances drawn after culling, so batching and per-instance culling are visible in `debug_snapshot().systems`. `countStaticBatchDraws` (`@jgengine/shell/world/staticObjectBatches`) reads those counts from a live scene.
- `freezeStaticTransforms` / `thawStaticTransforms` (`@jgengine/shell/render/staticTransforms`) keep authored scenery out of the per-frame matrix update.
- `ModelLodConfig` (`@jgengine/core/game/playableGame`) sets a character model's full-rate, half-rate and crowd distances plus the crowd hysteresis.
- `characterLodTier` / `characterLodPolicy` (`@jgengine/shell/render/characterLod`) resolve a distance to a tier, and the `render/characters` devtools system probe reports rigs by tier, skinned parts, bone pose writes per frame and crowd instances in `debug_snapshot().systems`.
- Load-time shader warmup for 3D presentations. `createShaderWarmup` (`@jgengine/shell/render/shaderWarmup`) compiles the mounted scene through `WebGLRenderer.compileAsync` in rounds, one per burst of streamed models, and reports a phase, round and program count a loading screen can render. It is bounded: a round is raced against its budget, so a driver that never reports a finished program cannot hold the first frame.
- `SceneWarmupCover` (`@jgengine/shell/render/SceneWarmupGate`) is the opaque cover the shell puts over the canvas until that warmup finishes. It carries `data-shell-code-loading`, so capture and drive already wait for it, and it clears itself on a failed compile. Game UI and menus mount above it and stay usable.
- `createPoliceResponse` (`@jgengine/core/world`) is the wanted level over the shared `ai/heatSystem` curve: a crime raises heat only while `sensor/perception` memory holds the suspect in view (the sighting's own `sourceId`/`sourceTick` kept as its owner), decay starts once every witness has been out of sight for `escapeSeconds`, and a caller-supplied tier table decides what police do — patrol, investigate, pursue, roadblock — with pursuer budgets, a last-sighting search area that grows while the trail is cold, and roadblock sites off the authored road data. `state()`/`restore()` round-trip mid-chase and the injected generator is the only source of variety.
- `policeRoadsFromLaneGraph` (`@jgengine/core/world`) reads cordon sites and an indexed nearby-lane query out of an authored lane graph: `traffic_roadblock` markers projected onto the lane they block, or the graph's intersections when none are authored.
- `DEFAULT_POLICE_TIERS` and `DEFAULT_POLICE_TUNING` are the starting tier table and curve a game overrides.
- `createRideSystem` (`@jgengine/core/gameplay`) runs a park's rides: each one cycles load → ride → unload at its own capacity, boards from its own line, wears against its own breakdown curve (chance per completed cycle, repair seconds and money) and reports boards, alights, breakdowns and repairs through a sink, so the game keeps the bodies, the visuals and the wallet. `snapshot()`/`restore()` round-trip a park mid-cycle, `retune(id, patch)` changes a ride's ratings, capacity, timings or breakdown curve in play, and `service(id)` pays the maintenance cost and clears wear.
- `createWaitQueue` (`@jgengine/core/gameplay`) is a real line: guests join the back, leave from anywhere, and the wait a new arrival faces comes from the queue's own measured boarding throughput rather than a configured constant. `DEFAULT_QUEUE_SEED_SERVICE_SECONDS` only bootstraps the estimate until two boardings have been observed.
- `createRideChooser`, `rideChoiceOptions`, `rideChoiceCandidates`, `rideQueueCostWeight` and `rideAppeal` (`@jgengine/core/gameplay`) turn a guest's `RideTaste` (intensity comfort, nausea comfort, queue tolerance) plus the park's ride data into `createUtilityBrain` options, each ride costed by the seconds its own line reports — ride choice is one more scored option, not a fixed order.
- `DEFAULT_RIDE_BREAKDOWN`, `DEFAULT_RIDE_RATING_RANGE` and `DEFAULT_RIDE_FUN_CURVE` are the starting curve and scales a park overrides.
- `createMissionRuntime` (`@jgengine/core/gameplay`) runs missions as data: a mission is a graph of objectives the machine walks, with named fail states, per-objective timers, conditional branches, per-objective and completion rewards, a `rollout` fact rolled from the injected RNG, and a `next` mission that chains a follow-up. `tick(dt, world)` advances the active mission against caller-supplied actors, counters, facts and survivors; `signal`/`fail` let an authored trigger complete or fail it. `snapshot()`/`restore()` round-trip a run, `stats()` and an optional `devtoolsName` probe expose it, and cost per tick is the active objective's own fail list plus one goal test.
- Objective kinds are the shared primitives `reach`, `interact`, `survive`, `pursue`, `kill` and `collect`; a fail condition can be `actor-died`, `timer` or `fact`.
- `escortMission`, `chaseMission` and `defendMission` (`@jgengine/core/gameplay`) are compositions of those kinds, not archetypes: an escort is a `reach` with an `actor-died` guard, a defend is a `survive` with a survivor floor that banks what is left standing as the score, and a chase is a `pursue` with an escape guard.
- `validateMissionCatalog` (`@jgengine/core/gameplay`) is the mission content gate: duplicate ids, unknown branch/next/timeout targets, unknown fail states and markers, per-kind required fields, unreachable objectives, and the counter-only rule — a mission with a completion path made entirely of `kill`/`collect` objectives is an error. `createMissionRuntime` runs it at construction, so a counter-only mission throws instead of shipping.
- `registerMissionTriggerActions`, `MISSION_TRIGGER_ACTIONS`, `createMissionTriggerBridge` and `missionMarkers` (`@jgengine/core/gameplay`) start, advance and fail a mission from authored scene markers using the shared `on`/`action` trigger vocabulary; positional objectives read marker positions off the authored document.
- `applyMissionRewards` (`@jgengine/core/gameplay`) pays a mission through the same appliers quests use (XP, currency, items, unlocks) and rolls named loot tables through the game's loot registry.
- `createMarket` (`@jgengine/core/gameplay`) prices goods from supply and demand over finite stock on a stated curve: `price = clamp(basePrice × (coverPeriods × demand / max(stock, 1))^elasticity)` between `basePrice × minFactor` and `basePrice × maxFactor`. `sell` decrements the finite stock and refuses a sale it cannot cover with the shortfall named, `restock` lowers the price, `quote`/`priceAfter` read the move without mutating, `recordDemand(unitsSold, unitsUnmet)` folds a period of observed buying into demand, and `priceCurve` replaces the curve. `retune` changes a good's policy in play; `snapshot()`/`restore()` round-trip stock and demand; money is integer minor units.
- `createTransactionLedger` (`@jgengine/core/gameplay`) keeps per-account, per-currency balances in integer minor units with a journal that records every movement, the balance it produced and the reason it happened. `post` is the one money path — sales, purchases, upkeep and loan payments all carry a caller-owned reason and an optional `ref` — and `totals()` keeps a per-reason count and net beyond the `maxEntries` journal cap. `addCharge` registers a recurring charge (upkeep, rent) settled on its cadence by `chargeDue`, bounded per charge and per advance; a cycle with no funds is counted as missed instead of overdrawing.
- `buildLoanSchedule` and `createLoanBook` (`@jgengine/core/gameplay`) draw a loan, post the principal, and settle interest (`"loan-interest"`) and principal (`"loan-principal"`) as separate reason-tagged entries. A schedule is built up front in integer minor units — `"amortized"` levels the payment on the outstanding balance, `"flat"` charges interest on the original principal — and the final payment absorbs the rounding, so the principal parts sum to the principal exactly. A payment that comes due with no funds stays in arrears and counts once per advance; after `maxMissed` the loan is defaulted and stops being advanced. `repay` pays principal early, and state is terms plus progress, so the schedule is rebuilt on restore.
- `createGameSave` (`@jgengine/core/gameplay`) is the one game save document: a `version`, the game/time/playtime metadata a save menu shows, the world's shared RNG seed and cursor, and one section per registered system. Register each stateful system once (`name` + `snapshot()`/`restore()`, optional `reset()`); `capture()` builds the document, `save()`/`touch()` persist it through the same injected `SaveBackend` `createSaveStore` uses, `load()`/`switchSlot()` restore every section and the RNG cursor, and `migrate` upgrades an older payload. Autosave cadence is caller policy, and `digest()`/`digests()` hash the whole state (or one section) for a save/reload determinism test.
- `gameSaveDigest` (`@jgengine/core/gameplay`) hashes a `GameSaveDocument`'s state — every section, the clock and the RNG cursor — excluding the wall-clock `savedAt`.
- `GameSaveConfig.devtoolsName` registers a devtools probe reporting the active slot, the save status, the change revision and the system count.
- `createVisitorPopulation` (`@jgengine/core/sim/population`) counts a crowd by its members: a visitor exists only once the game's `VisitorWorld.admit` has made its body and `VisitorWorld.has` confirms it, so `onSite()` is the member count and `arrivalsToday()` is the sum of the admissions the world accepted. `arrivalRate`, `capacity`, `shouldLeave` and `inside` are caller policy; `adopt` takes over a body a save or a seed made; `rollDay()` closes the day without emptying the park.
- `createDamageRuntime` (`@jgengine/core/combat/damageRuntime`) announces every damage application as `combat.damage` on the game's own event bus: `target`, `source`, `type`, `amount`, `absorbed`, `remaining`, `lethal`, plus `status` for a status tick. Pass `events: ctx.game.events` and `healthOf` (the target's live health, read before you apply it) so a HUD can name the blow and tell whether the target died; without `healthOf` the event reports `remaining: null` and `lethal: false`. `recentDamage(limit)` and the devtools probe line carry the same stream.
- `StatusDamageTick.source` (`@jgengine/core/combat/statusRuntime`) names who applied the status that is ticking, so a burn names its arsonist.
- `clearCameraBoom` (`@jgengine/shell/camera`) answers where a camera can sit without entering world collision: the desired position is rejected when the boom hits a surface, and the camera is placed one `clearance` before it. Pulling in is immediate, easing back out follows `releaseRate`, so a boom reads as a spring arm rather than a hard stop. `sceneCameraQuery(ctx.scene.raycast)` feeds it the existing world solids, object colliders and walls — no new collision layer — and any other hit query can be passed instead.
- `FirstPersonCameraConfig.collision` (`{ clearance?, armBack? } | false`) keeps the `first` rig's eye out of the trunk or wall the body can reach. On by default; `false` restores the unchecked camera.
- `createEffectSystem` (`@jgengine/core/combat/effects`) takes `onDamage?(application)`: one `EffectDamageApplication` per target a draining effect lands on, before `onLethal` despawns it. It carries `target`, `source` (the effect's `from`), `effect`, `incoming`, `dealt`, `remaining` (the last pool's current) and `lethal`; restorative applications stay silent.
- `installCaptureCamera`/`readCaptureCamera`/`clearCaptureCamera` and `CaptureCameraOverride` (`@jgengine/core/world/captureCamera`) are the harness counterpart of `?look=`, and the same install-style seam as `world/spawnOverride`: the runner installs a rig, the shell reads it. `resolveCaptureCameraConfig` (`@jgengine/shell/camera/rigResolve`) reads it above a game's static `camera` block, its runtime `ctx.camera.setRig` selection and its director cinematics, so a game that drives its own camera can no longer photograph that camera instead of a requested `--look` framing. With nothing installed the resolution is exactly `resolveRuntimeCameraConfig`, so every game renders as before.
- `EFFECT_LIBRARY` and `resolveEffect` (`@jgengine/core/vfx/effectLibrary`) name 24 GPU particle effects — fire, smoke, sparks, dust, splashes, muzzle flash, explosions, magic and weather — each authored as rate, lifetime, size, colour and a procedural sprite texture, so `ctx.particles.play("explosion/core", { position })` renders one by name and the editor authors the same name.
- `ctx.particles.play(name)` and `ctx.particles.attachEffect(id, name)` (`@jgengine/core/vfx/particleDirector`) play a library effect by name; `effectNames()` lists them.
- `planParticleBudget` (`@jgengine/core/vfx/particleBudget`) caps the live particle count to a graphics tier and drops whole effects lowest-priority-first when the request does not fit.
- `GraphicsProfile.particleBudget` is the tier's scene-wide live particle cap (low 1024, medium 2048, high 4096, ultra 8192); `particleCap` stays the per-effect pool clamp.
- `AuthoredEmitter.effect` (`@jgengine/core/editor/simulation`) authors a named library effect in `editor.scene.json` instead of a full config; `config` is now optional and exactly one of the two is required. `SimulationInspector` gains an effect picker.
- `foliage/grass-macro` and `ground/dirt-macro` in the shared material library: the same ambientCG scans at a 24 m / 20 m tile, so they carry structure at the scale an RTS camera 50 m out resolves instead of texel noise.
- `libraryTerrainMaterial(name)` (`@jgengine/assets/catalogs/materialLibrary`) returns the terrain detail material config for a library name, tiling from the entry's authored metres and blending from its terrain policy; a map the archive does not ship is omitted.
- `libraryBuildingSurface(name, options)` and `librarySurfaceParameters(name)` bridge a library name to `BuildingSurfaceMaterial` and `MaterialSurfaceParameters`, so a facade names its material instead of hand-wiring maps, colour and repeat.
- The library reports a `displacement` role (`libraryMaterialRoles`) without loading it, and `MaterialMaps.displacement` / `TerrainMaterialMaps.displacement` are optional; ambientCG archives without one (Fingerprints001) no longer publish a phantom displacement URL. `libraryTerrainMaterial` passes it through for callers that do displace.
- `terrain-macro` dev demo captures one field from 52 m in `?mode=flat|dense|macro` to compare ground materials at RTS scale.
- `SKELETON_STANDARD`, `mapRigToStandard`, `retargetTracks`, `retargetBoneScales` and the `RIG_SCHEMES` table (`@jgengine/core/anim/skeletonStandard`) are the engine's one humanoid skeleton standard: the CC0 KayKit character rig that `kaykit-adventurers` and `kaykit-skeletons` already share, named slot by slot (`root`, `hips`, `spine`, `chest`, `head`, `upperarm.l`…`toes.r`, `handslot.l/r`). A rig on another naming scheme maps onto those slots through a scheme table — Mixamo (`mixamorig*`) and the Quaternius sci-fi aliens ship as `MIXAMO_SCHEME` and `QUATERNIUS_ALIEN_SCHEME` — and every slot the rig cannot fill comes back in `missing` instead of borrowing a neighbour's bone. `retargetTracks` renames a clip's tracks onto a mapped rig, `retargetBoneScales` gives the per-slot length ratios that match bone proportions, and `runtimeBoneName` is the `.`-stripped spelling the loaded hierarchy and clips both use.
- `locomotionGraph` (`@jgengine/core/anim/locomotionGraph`) now builds the whole directional locomotion set: `strafeLeft`/`strafeRight` add a `strafe` blend on the new `direction` parameter, `backward` adds a `backward` blend behind the new `facing` parameter, and every graph gets a `turn` blend on the new `turnRate` parameter (pivoting on `walk` when no turn clip is authored). Each state joins the idle/walk/run blend through a crossfaded transition, so a direction change blends instead of popping; supplying none of them leaves the speed-only tree unchanged. `LOCOMOTION_DIRECTION_PARAM`, `LOCOMOTION_FACING_PARAM`, `LOCOMOTION_TURN_RATE_PARAM`, `LOCOMOTION_STRAFE_STATE`, `LOCOMOTION_BACKWARD_STATE` and `LOCOMOTION_TURN_STATE` are exported, and a custom mover feeds the three parameters through the entity blackboard key `ANIM_PARAMS_KEY`.
- `ModelAnimationStates` gains `strafeLeft`, `strafeRight`, `backward` and `turn`.
- `classifyClip`/`rolesFromClips` know the `strafe` and `backward` clip roles, so `defaultAnimationForClips` derives the KayKit directional states automatically (`Running_Strafe_Left`, `Running_Strafe_Right`, `Walking_Backwards`) for any rig resolved with `animation: "auto"`.
- `ragdollConfigFromPoses`, `ragdollBoneOrientations`, `RagdollPose`, `RagdollBodyShape` and `HUMANOID_RAGDOLL_BODIES` (`@jgengine/core/anim/ragdollRig`) turn a rig mapped onto the skeleton standard into a physics ragdoll at the moment of death: one box body per standard slot that has a shape, sized to the character, and one distance link per body to its nearest shaped ancestor, holding the bones' own separation. The bodies start where the character was, so the collapse continues the death pose. `handSlot.l/r` carry no body (prop sockets, not body parts); a slot the rig lacks is skipped and its children link to the nearest present ancestor. Because the engine's `PhysicsWorld` bodies carry translation only, `ragdollBoneOrientations` gives the skeleton its orientation by rotating each bone's death-pose direction-to-child onto the current one.
- `useRagdoll` (`@jgengine/shell/render/useRagdoll`) arms a ragdoll on a rigged character when `active` goes true and drives every mapped bone's world position and aim from its body each frame. It steps the injected `PhysicsWorld`; deactivating removes the bodies and leaves the rig where the ragdoll put it. Stop the character's `ModelConfig.ik` while it is active, or the two write the same joints.
- `multiplyQuaternions` and `quaternionFromUnitVectors` (`@jgengine/core/anim/ikSolver`) are the two pure rotation helpers the ragdoll's aim needs.
- `locomotionGraph({ rootMotion: true })` (and `ModelAnimationStates.rootMotion`) marks the whole locomotion set — idle/walk/run, strafe, backward and turn — as root-motion states, so one authored locomotion graph carries its own travel as `AnimGraphOutput.rootDelta` for the movement owner to consume.
- `actionLayer` and `withActionLayers` (`@jgengine/core/anim/actionLayer`) build a masked overlay layer from plain data: a resting clip plus a one-shot state per entry in `actions`, each joined by a consumed-trigger transition and returning on `exitTime`. Aim, reload, wave and hit reactions are one builder instead of a hand-written state machine per game. `additive: true` makes it a recoil/breathing overlay — the shell's `makeClipAdditive` makes the rest clip's first frame the zero reference, so the rest state is held at `speed: 0` and contributes no pose while the base keeps animating. `mask` is required and nonempty, because an overlay that names no bones would drive the whole rig; `withActionLayers` rejects a duplicate layer id instead of shadowing the base.
- `standardBoneMask`, `UPPER_BODY_SLOTS`/`LOWER_BODY_SLOTS` and `UPPER_BODY_MASK`/`LOWER_BODY_MASK` (`@jgengine/core/anim/skeletonStandard`) derive a layer mask from the skeleton standard's own slots, so a rig mapped onto the standard is masked without hand-typing its bone names.
- `ModelLookAtConfig` (`@jgengine/core/game/playableGame`, `ModelConfig.ik.lookAt`) can aim at more than the camera: `target` takes `"camera"` (default), `{ entity }` offset by `entityEyeHeight` (default 1.5), or `{ point }`. `maxYaw`/`maxPitch` bound the turn in radians and `weight` (default 1) scales how much of the solved turn reaches the bone. An entity target that is not spawned keeps its last known direction instead of snapping the head back to the camera. `applyFootIk`'s sixth argument is now `lookAtTarget` and applies the limits and weight; `FootIkRig` carries `lookAtLimits` and `lookAtWeight`.
- The shell derives the directional locomotion parameters from the entity's own movement, so a rig with the `strafe`/`backward` states uses them with no game-side code: `direction` (the motion's share along the character's right axis), `facing` (its share along the facing axis) and `turnRate` (its yaw change per second) come from the entity's `position` and `rotationY` each visual sample and override conflicting authored extras. `readModelAnimationParams` gains an optional `ModelMotionStep` argument (`dx`, `dz`, `dyaw`, `seconds`); omitting it leaves direction to the authored extras, which is the previous behaviour. A step past the live `snapDistance` reports no direction and zero speed rather than inventing a lateral.
- `createMenuTransition` (`@jgengine/core/ui/menuTransition`): closed → enter → open → exit → closed timing over an injected clock, with reduced-motion collapse to zero and `snapshot`/`restore`.
- `@jgengine/core/ui/menuNav`: `menuNavMove` focus movement over a vertical list, horizontal row or grid with disabled-item skipping, `menuIntentForKey`, `menuIntentForPadButton`, `menuIntentForPadAxis` and `shouldRepeatMenuNav` for held d-pad/stick repeat.
- `MenuScreen` (`@jgengine/react/menuScreen`): animated enter/exit screen with a translucent scrim, a `backdrop` slot for a live 3D scene, reduced-motion collapse and exit unmount. Animates transform/opacity only.
- `TitleMenu` (`@jgengine/react/titleMenu`): title-screen block — wordmark, eyebrow, tagline, navigable verb list, footer, control hint and backdrop slot. Games own the wording, verbs and art.
- `useMenuNav` (`@jgengine/react/menuNav`): keyboard and standard-gamepad focus navigation for any menu — roving `tabIndex`, arrow/WASD/Home/End moves, Enter/Space/A confirm, Escape/Backspace/B/Start back, held-stick repeat, disabled-item skipping, no focus trap (Tab stays native, typing is never intercepted) and keys that work when focus starts outside the menu.
- `PauseMenu` now navigates with the same keyboard/gamepad rules; Resume is the first item and Escape resumes.
- Engine demo `menu-framework` (`bun run shoot menu-framework --mode play`) renders a live 3D scene behind the title and pause screens.
- `credits-panel` — `CreditsPanel` (`@jgengine/react/creditsPanel`): a scrollable, HudTheme-skinned credits screen over the scene with one back affordance reachable by keyboard, gamepad and pointer.
- Loading screen with tips — `LoadingScreen` (`@jgengine/react/loadingScreen`): progress bar (real 0–1 value or indeterminate), rotating tips, a ready gate any key / A / click accepts, reduced-motion aware, and a scene backdrop.
- `createTipRotation` (`@jgengine/core/ui/loadingTips`): deterministic, seeded tip order that cycles every tip once, with `snapshot`/`restore`.
- Save slots wired to the save document — `refreshSaveSlots`, `saveSlotMeta`, `formatSavePlaytime` and `gameSaveSlotSource` (`@jgengine/core/game/saveSlotIndex`) read the slots a `createGameSave` actually wrote and fill a `createSaveSlots` index with the name, saved time and playtime chip a save-select menu shows.
- `SaveStore.peek(slot)` and `GameSave.peek(slot)`: read a slot's stored document without touching the live session, applying sections or changing status.
- `SaveSlotWrite.savedAt`: a save payload that knows when it was written can stamp that time instead of the index's clock.
- `@jgengine/core/settings/accessibilitySettings`: the persisted accessibility ids (`accessibility.textScale`, `accessibility.colorblind`, `accessibility.reducedMotion`, `accessibility.highContrast`, `accessibility.captions`, `controls.holdVsToggle`), `accessibilitySettingRows(store)` for a settings surface, `readAccessibilitySettings(store)`, and `holdToggleActive(previous, { pressed, held }, mode)` for hold-or-toggle actions.
- The shell's settings menu has an `accessibility` category (text size, colourblind palette, reduce motion, high contrast, captions, hold-or-toggle) unless a game hides it. Text size scales every `--jg-type-*` token through `--jg-text-scale`; reduce motion zeroes the engine's motion scale; high contrast, colourblind and captions are exposed as `data-*` hooks on the HUD root.
- `SettingsSection`, `SettingsRowControl`, `SettingsSlider`, `SettingsToggle`, `SettingsSelect`, `SettingsKeybindControl` and `SettingsTabs` (`@jgengine/react/settingsBlocks`): the settings rows and click-to-capture controls as composable, HudTheme-tokenized blocks, so a game lays out its own settings screen instead of taking the stock dialog. Rebinding writes into the game's existing rebind/holdings store.
- `useAccessibilitySettings`, `useHoldVsToggle` and `useAccessibilityRows` (`@jgengine/react/accessibilitySettings`) read the live preferences for any custom layout.
- `HudThemeRoot` takes `accessibility` and applies text scale and the preference data attributes at the HUD root; the shell passes the player's stored preferences there for both the 3D and HUD-only presentation paths.
- `MissionTickInput.events` (`@jgengine/core/gameplay`) carries the world events that happened this tick - `kill`, `collect`, `deliver` with a target, the actor, free-form `tags` and a count. An objective's `match` filter (`target`, `by`, `tags`, `withoutTags`) credits a constrained counter only from matching events, so a decoy, a wrong weapon or another killer's kill does not advance it.
- `failOn` accepts `event` (a matching reported event ends the run) and `left-area` (the watched actor is farther than `radius` from an authored marker), completing the reachable fail states: cargo lost, time out, escort died, left the area. `validateMissionCatalog` rejects a `left-area` fail with no actor or unknown marker and a `match` on a non-counter objective instead of ignoring it.
- `MissionView` now carries `counted`, `count`, `step`, `steps`, `outcome` and `reason`, so a HUD renders "step 2 of 3", a timer and the fail state from `view()` alone. A counter objective's progress mirrors the caller's counters into `state.progress`, so `view()` and a save both see it.
- `TerrainDetailMaterialConfig.missing` (`@jgengine/core/world/features`): a terrain detail material built from pulled scans no longer takes the whole view down when those maps are not on disk. `"procedural"` (the default) keeps the authored procedural ground and logs one warning naming the missing map, so a game whose asset packs are pulled rather than committed boots anywhere; `"fail"` keeps the presentation retry screen for a view that genuinely cannot render without the scan, which is what the recovery demo asks for.

### Changed

- `bun run land` (`scripts/land.ts`) lands a task branch in one command: it refuses `main` or a dirty tree, pushes, opens or reuses the PR into `beta`, runs `--verify`, watches checks, merges and prints `landed <url>`. Exit codes: 0 landed, 1 verify failed, 2 checks failed, 3 could not merge.
- The CI `test` job is split by path into four matrix shards (`scripts/test-shard.ts`), about 90 s each instead of 5m37s in one job; `ci` still aggregates every shard and no test file is dropped.
- A loaded model whose meshes share a material now merges to one mesh per material at load, baking each source mesh's transform into the merged vertices. Every instance cloned from that model draws once per material instead of once per sub-mesh, and the instanced scatter/static-batch path harvests the same merged meshes. Skinned meshes, instanced meshes, meshes with morph targets or rigged ancestors, meshes with children, hidden meshes, mirrored meshes, and every model carrying an animation clip are left untouched. A material-assignment `selector.mesh` still resolves a sub-mesh name the merge absorbed.
- Static authored/`scenery` props batch per model per 128 m cell with a minimum of 2, instead of per model per 64 m cell with a minimum of 3. Props spread thinner than the old minimum in their cell used to fall back to a full individual model draw — Harbor Heat drew 425 of its 488 props that way — and now join one instanced draw per material. Cell size stays configurable because one `InstancedMesh` has one bounding sphere, which three also frustum-tests in the shadow pass: a batch spanning the whole city submits every camera-visible instance to every shadow cascade (Harbor Heat +14% triangles), while 128 m keeps that rejection working (+1.1%).
- Culling stays per instance: the visibility predicate compacts each batch's drawn range every frame and the batch's bounding sphere is rebuilt from what it draws, so a batch with nothing in view contributes a draw of zero.
- Static subtrees stop paying three's per-frame matrix pass. `freezeStaticTransforms` (`@jgengine/shell/render/staticTransforms`) bakes a subtree's world matrices once and clears `matrixAutoUpdate`/`matrixWorldAutoUpdate`; `thawStaticTransforms` puts it back for a seam that moves on an event. Batched static props and object/entity marker groups use it.
- Actor and object marker groups now own their matrix: the per-frame pose write recomposes only when the pose actually changed, so a parked prop or an idle actor leaves its subtree out of that frame's world-matrix pass. Moving actors are unchanged.
- A postfx prepass no longer re-walks the scene graph. `renderWithCurrentMatrices` (`@jgengine/shell/postfx/shadowPrepass`) draws the stylize normal prepass and the GTAO/DOF prepasses with the matrices the beauty pass built moments earlier in the same frame.

Harbor Heat: `updateMatrixWorld` 2.99 ms → 1.66 ms per frame.
- A rigged model's skinned parts merge into one skinned mesh per skeleton and material at load (`mergeSkinnedModelParts`, `@jgengine/shell/render/modelMeshMerge`). Vertices move into bind space and skin indices are remapped onto the union of the parts' bones, so the posed result is unchanged. A KayKit character's six body parts become one draw and one bone-matrix upload per character.
- Characters now carry distance tiers. Under 15 m the rig poses every frame, under 40 m every other frame with the accumulated time, and past 25 m a crowd-eligible rig is drawn by the GPU crowd path and stops updating bones. The thresholds live in `ModelConfig.lod` per model; `lod: false` keeps a model at full rate (a player avatar or a cutscene character), and the local player is always full rate. Any animated model with no `ik`, `attachments` or `parts` is crowd-eligible, so a game with no configuration still loses its distant characters' skeletons.
- `CharacterCrowd` (`@jgengine/shell/render/CharacterCrowd`) draws those far characters: one baked rig per kind, one instanced draw per material, each member's clip, gait phase and speed sampled on the GPU. Entity markers decide the tier, so a character only leaves its rig once the batch for its kind is baked.

Harbor Heat at the spawn view: 426 skinned meshes  71, bone pose writes per frame 2,894  41, draws 2,537  1,387, `updateMatrixWorld` 1.47  1.36 ms, frame 25.1  16.7 ms.
- Shadow casters under a 0.5 m world radius clear `castShadow` as they stream in (`cullSmallShadowCasters`, `@jgengine/shell/render/shadowCasters`). A prop that small is a couple of shadow-map texels wide, so the depth pass spent a draw on a smudge; the authored flag comes back when the mesh leaves the scene. Only clears, so a mesh the game keeps out of the pass stays out. The `render/shadows` devtools system probe reports casters and how many were skipped.
- Cascaded shadow maps refresh the far cascades on a cadence instead of every frame (`scheduleCascadeRefresh`): with three cascades the nearest redraws every frame, the middle every 2nd and the far every 4th; with four, the two nearest every frame and the rest every 2nd/4th. A skipped cascade keeps the matrix and map from its last refresh, so the pair stays consistent and the shadows do not slide.
- Cascaded shadow reach now comes from the graphics tier when a game does not author `shadowMaxFar`: the tier's shadow-map resolution sets the reach (512 → 100 m, 1024 → 150 m, 2048/4096 → 200 m), clamped by that tier's draw distance. An authored `shadowMaxFar` still wins, so existing games keep the distance they chose.

Harbor Heat's shadow pass self time at `--quality full`: 2.88 → 1.37 ms per frame, `WebGLShadowMap.render` 11.0% → 5.3% of the sampled window; the material patch hook every draw pays dropped out of the top self-time entries (0.83 ms → under 0.3 ms per frame). Frame time is unchanged within the capture noise floor at that view, where the post-processing chain dominates.
- `EntityDiedEvent.source` and `DeathReason` name what killed the entity: a `player_kill`'s `source` is the killer's instance id, and an environment death carries `attacker` for a non-player entity. `deathReasonFromEffect` fills both from the effect's `from`. Reading an existing environment reason keeps working; a rule that matched `kind` still matches.
- `ctx.scene.entity.effect` (including telegraph-bound and projectile effects) now raises exactly one `combat.damage` per target on `ctx.game.events`, with the same shape `createDamageRuntime` emits: `type` is the effect id, `remaining`/`lethal` come from the last pool in the catalog's `receive` order. `createDamageRuntime` keeps its own emission, so a game using both paths gets one event per blow, not two.
- `.claude/skills/jgengine-combat` says which damage path to use: the runtime for typed/zoned/armored damage, the effect path for catalog `receive` pools.
- The dev runner installs the resolved `?look=`/`--cam=` rig as the capture camera, and the shell's camera rig and presentation both resolve through `resolveCaptureCameraConfig`.
- `metricsPathFor` (`scripts/shot-metrics`) is the `--inspect` metrics path for a shot. `--out` without an image extension used to swap nothing, so the metrics JSON was written straight over the screenshot; the metrics path now always differs from the shot path.
- `WorldParticles` allocates the tier's `particleBudget` across the standing emitters and queued bursts with `planParticleBudget`, and reports `cap`/`budgetParticles`/`droppedIds` on its `particles` diagnostics probe.

### Fixed

- `settingsBoundary.test.ts` builds its fixture under `.scratch/` inside the checkout instead of `%TEMP%`, so `@jgengine/*` resolves through the workspace and the browser test passes in a fresh worktree with no `dist`; CI was hiding this because it builds first.
- The skill installer treats an unreadable `.jgengine-version.json` as missing and says which file it ignored, so a half-written or hand-edited marker is rewritten on the next install instead of blocking it; refusal messages now name the metadata file.
- Skill and doctor fixtures write a realistic next-minor version instead of `99.0.0`, so the CI log no longer reports impossible install versions.
- `SettingsMenu` keeps Escape closing the dialog while a keybind capture is ending. The capture already consumes Escape at the window, so the extra "wait for capture state" guard only dropped a close request that arrived before the next render, which failed `touchHudReachability` on every `beta` run since #2100.
- `RoutineTimeline.nextChangeAfter` (`@jgengine/core/sim/routine`) no longer answers with the time it was given when a stop time is fractional. `time` arrives as a whole number of cycles plus a stop time, so its low bits are already rounded; a boundary that rounding put a hair behind `time` read as a change still to come, and the answer repeated, so anything that stepped by it looped forever. Boundaries are compared in absolute time within a few ulps of `time`, so every answer is strictly later. `phaseAt` uses the same tolerance, so a phase that starts exactly on a rounded boundary reports the new phase instead of the walk that led to it.
- `createOffScreenWorld.refreshCheap` stops catching up instead of spinning if a schedule ever again fails to name a later change: it decays needs to `now`, holds the phase and retries one cycle later.
- A type token now honors the player's text scale: `--jg-type-*` are `calc(max(floor, size) * var(--jg-text-scale, 1))`, so raising text size resizes every engine block rather than only a menu's own labels.

## 0.27.0

### Migrate

- **Bump lockstep SDK packages to `^0.27.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.24.0`; `@jgengine/github` is `0.14.0`.

### Added

- `rollLoot` (`@jgengine/core/loot/lootGen`) rolls a named item from a caller `LootCatalog`: bases with mount slots, parts, affix pools, weighted rarity tiers, manufacturer personalities and a `levelScale` curve. Stats are `base × rarity.statScale × levelScale`, then part stats/multipliers, maker `bias`/`scale`, then additive and multiplicative affixes; additive affix rolls scale with level, multiplicative ones do not.
- `createLootRoller` wraps a validated catalog with roll counts per rarity, `snapshot()`/`restore()`/`reset()` and an optional devtools probe; `assertLootCatalog` rejects duplicate ids, unresolvable pools, empty item slots and a non-ascending level curve at construction.
- `validateLootItem` returns coded issues (`unknown-base`, `manufacturer-base`, `missing-slot-part`, `part-category`, `unknown-affix`, `empty-name`, …) and `compareLootItems` returns sorted per-stat deltas plus a weighted score with caller-named `lowerIsBetter` stats.
- `lootWeaponDefinition` (`@jgengine/core/loot/lootWeapon`) projects a rolled item onto a `WeaponDefinition` through a caller stat map, so a generated gun runs in `createWeaponSystem` after `assertWeaponDefinition`.
- `defineXpCurve` (`@jgengine/core/gameplay`) resolves one authored growth rule (`flat`, `linear`, `power`, `geometric`, `table`) plus a `maxLevel` cap into per-level thresholds, lifetime totals, a plain-words rule string and a `leveling` track.
- `createSkillTree` spends and refunds points over authored nodes with `cost`, `requires`, `excludes`, passives, actives and multi-node `synergies`; `resolved()` returns the stat modifiers and actions, and state snapshots/restores as `{ points, ranks }`.
- `gearScore` / `compareGearSets` score a whole loadout from stat weights, level, rarity and slot coverage.
- `lootStatContributions` and `scoreLootStats` expose the weight policy `compareLootItems` already used.
- `createEquipment` (`@jgengine/core/inventory/equipment`) wears gear by slot: slot defs declare accepted kinds and the slots they block, item defs declare the slots an item may occupy, a filled slot swaps and hands back the replaced item, and a two-handed item and the slot it blocks cannot both be filled. `state()` / `restore()` round-trip the worn set; a devtools probe reports filled slots.
- `characterLoad` and `canCarry` (`@jgengine/core/inventory/carryLoad`) weigh carried inventories plus worn gear against a capacity and hand back the shared encumbrance bands (tier, `moveMultiplier`, sprint/jump gates, `remaining`), so a stash left out of `carried` adds no weight.
- `fitShapedItem` (`@jgengine/core/inventory/shapedGrid`) places a shaped item at its first fit or names why not: `too-large` (with both sizes), `no-space`, `budget-exceeded`.
- `InventorySet.snapshot()` / `restore()` / `reset()` (`@jgengine/core/inventory/inventoryModel`) save and reload every declared inventory as detached plain data, so a bag and a stash never share live slots.
- `splitStack` takes an optional stack `limit`, rejecting `over-stack` instead of merging past the max stack.
- `createLaneGraph` and `laneGraphFromDocument` (`@jgengine/core/world`) read an authored lane graph out of the scene document: `traffic_lane` paths are one directed span each, `road` paths (the street bake) become a lane per direction split where streets cross, `traffic_intersection` markers carry signal phases and right-of-way in `meta`, and `traffic_parking` markers place kerbside spots.
- `createTraffic` (`@jgengine/core/world`) runs cars on that graph: IDM car following and braking, one car at a time through each intersection box behind a signal phase or first-come right-of-way, lane changes across authored neighbours, parking that reserves a spot, deterministic injected variety, `snapshot`/`restore`, and a devtools probe.
- `Traffic.setDemand(count)` is the fleet target; the time-of-day curve stays the caller's policy.
- `applyVehicleParts` (`@jgengine/core/world`) composes data-driven tuning parts onto a `KinematicVehicleTuning` — stat multipliers/addends applied in install order plus an optional tire grip curve — and returns the composed tuning, the installed ids, and a `VehicleFeel` (top speed, drive, brakes, turn rate, handbrake grip, and the grip curve's peak/slide). `withPart` swaps a part within its slot and `feelOf` reads the numbers out of any tuning.
- `createVehicleDamage` (`@jgengine/core/world`) is fixed-zone crash damage wired to handling and visuals: impacts accumulate into discrete stages, each stage costs grip/drive/brakes/top speed/drag and pulls the steering (`effect`), and each zone shows one bounded dent (`deformation`) that never exceeds its authored `deform`. Parts can detach at a set stage, a total impulse can disable the vehicle, and `snapshot`/`restore` resume mid-crash. `damagedTuning` and `damagedAxis` fold the effect into a tuning or an input, and `modifiers()` carries it as per-tick overrides.
- `KinematicVehicleTuning.weightTransfer` (`@jgengine/core/world`) adds load-transfer grip: longitudinal transfer under braking/traction and lateral transfer under cornering cut the tire grip curve (tire load sensitivity), so braking into a corner holds less slide than coasting through it. Each tick reports `weightTransfer` and `loadGrip`; omitting the block keeps the previous flat-grip model exactly.
- `KinematicVehicleModifiers.gripScale` scales the grip curve for one tick, mirroring `VehicleDynamicsModifiers.gripScale` so a damage layer drives either sim.
- `decal` scene kind (`@jgengine/core/world/decalKind`) and its renderer (`@jgengine/shell/scene/DecalRenderer`): dirt, cracks, paint and blood decals authored as placed markers, draped on the live terrain or standing on an authored tilt, with a deterministic procedural texture per style+seed.
- `TerrainMaterialLayer.material` (`@jgengine/core/world/terraform`): a material library name whose scanned color map paints that terrain layer. The runtime samples up to four layer maps at each layer's `tiling` and blends them through the authored mask weights (`createTerrainDetailMaterial`'s `layerMaps`), so painted terrain blends between authored layers instead of tiling one flat map.
- `grass_field` volumes now paint a ground-cover mat under their blades (`groundCover` rule, `@jgengine/shell/terrain/GrassCover`), so an authored patch reads as turf rather than bare ground with stubble. `GrassEnvironmentConfig.groundCover` does the same for `grass()` patches.
- `ModelConfig.wind` (`@jgengine/core/game/playableGame`) and `applyModelWind` (`@jgengine/shell/render/modelWind`): per-model foliage sway with height-ramped travel, shared gust clock and per-model strength/speed/direction.

### Fixed

- The player shell re-renders only for the ctx slices it reads — the possessed entity and the touch control mode — instead of on every `ctx` write. Subscribing the shell root to the raw ctx version reconciled the canvas, the whole actor tree and the HUD on each per-frame entity pose write, which is what `subscribeMembership` and the markers' imperative `useFrame` pose reads exist to avoid (#625). Scrap Signal: 86.6 ms → 16.7 ms per frame at the same 621 draws; Harbor Heat: 102.8 ms → 46.2 ms at the same ~7.2k draws.
- The `ocean()` environment feature passes the live ground to its shader, so the sea shades shallow→deep and its shoreline foam band lands where the bed meets the surface.

### Numbers

- Test catalog (60/25/10/4/1 rarity weights) over 10,000 drops at level 20, fixed seed: common 59.41% (target 60%), uncommon 25.66% (25%), rare 9.58% (10%), epic 4.20% (4%), legendary 1.15% (1%); every tier inside its 4σ binomial bound. All 10,000 items validated with zero issues and composed names (e.g. `Voss Rare Long Keen Lightframe of Reach`, `Tallow Long Lightframe`, `Voss Snub Lightframe of Steadying`).
- Level curve `[[1,1],[15,1.6],[30,2.4],[60,4]]`: a level-60 roll's base and additive affix values are 4× the same seed at level 1; maker personalities move the same roll (`Voss` +power/−cadence, `Tallow` the reverse).
- A rolled epic projects to a hitscan `WeaponDefinition` (rate, damage, integer magazine capacity, spread, range), passes `assertWeaponDefinition`, and its first `tick` fires one shot and one impact through `createDamageRuntime` with the rolled capacity spent.

## 0.26.0

### Migrate

- **Bump lockstep SDK packages to `^0.26.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.23.0`; `@jgengine/github` is `0.13.0`.

### Added

- `createLogBus` (`@jgengine/core/devtools/logBus`): a fixed-capacity ring of log lines tagged by owning system, with a separate ring for warnings and errors so a crash line is never pushed out by info traffic. `devtools.logs` now writes through it (`write(level, system, message)`, `snapshot()`, `counts()`, `dropped()`); `DevtoolsLogEntry` gained `system`.
- `devtools.probes.registerSystem(id, { counters, timers, state })`: a system reports its own counters, named timings and one state line in a few lines, with no shell wiring. Readings surface as `systems` in `devtools.snapshot()`, in `debug_snapshot`, and in every perf record.
- `PerfReport` (`@jgengine/core/devtools/report`): the one perf record shape shared by `bun run perf` and the in-page report.
- The shell samples frame telemetry in-page in dev builds: `window.__JG.report()` returns that record, and the page pushes it about once a second to the dev server's `GET/POST /__jgengine/report` route. `bun run debug <game>` prints the record of a game already open and exits non-zero on a budget breach in `scripts/perf-budget.json`.
- `createNeedSet` (`@jgengine/core/gameplay`) drains named needs on game time and reads them by index — decaying hunger, energy, fun with snapshot/restore and per-need rate retuning.
- `createUtilityBrain` (`@jgengine/core/world`) picks an action from decaying-need appeal: linear, quadratic, logistic, exponential and threshold response curves, candidate cost (travel, queues, price), a switch margin plus minimum hold (hysteresis), declared interrupts, optional seeded jitter, and a devtools probe string.
- `createNavRuntime` (`@jgengine/core/world`) consumes an editor-baked nav mesh: reads the `nav` bake off a scene document (`navMeshFromBake`), routes with the mesh query, resolves authored doors, ladders and jumps (`AuthoredNavLink`, priced by kind) onto the current surface, and swaps in a new bake after an edit while matching polygon identity by geometry — `applyMesh` returns a `remap` so only routes over changed polygons need re-planning.
- `createNavCrowd` (`@jgengine/core/world`) moves many agents across that mesh: plans a corridor, follows the funnel-smoothed waypoints, keeps agents apart with reciprocal velocity obstacle avoidance, bounds path searches per tick by count and milliseconds, fans converging agents out with `spread`, and round-trips through `snapshot`/`restore`.
- `createRvoCrowd` (`@jgengine/core/world`) is the avoidance solver on its own: RVO/ORCA half-plane solve over a uniform-grid neighbour search capped at `maxNeighbors`, deterministic tie-breaking, overlap relief, retunable `timeHorizon`/`neighborDist`/`maxNeighbors`/`sideBias`/`overlapRelief`, and `snapshot`/`restore` for saves and replays.
- `resolveRoutine` (`@jgengine/core/gameplay`) resolves an authored day into real arrival and departure times against the world clock. Travel time is injected per stop or as a callback, so a stop departs early enough to walk to the next one and an arrival that runs late slips instead of teleporting; `phaseAt(time)` returns the held place, the walking leg and its progress, and `nextChangeAfter(time)` drives a scheduler. A day whose walks cannot fit its cycle throws at construction.
- `createOffScreenWorld` (`@jgengine/core/gameplay`) keeps a whole population without simulating it: residents beyond the injected radius are cheap state (position, routine phase, needs) that only moves when the clock crosses a routine boundary, and they spawn into the game's own near simulation through a `NearAgents` interface when they come close. The radius, an optional per-agent cost model, a near budget and a per-tick switch budget are all injected; `snapshot`/`restore` round-trip the whole population, and crossing the boundary preserves identity, schedule progress, need values and position.
- `createNearAgents` (`@jgengine/core/gameplay`) is the default near half of that seam: a nav crowd with a need set and utility brain per agent, goal-tracking as routines move residents between places, and a decision counter for tests and probes.
- `createPerception` shares squad knowledge: `squadMates(observerId)` names an observer's roster, and a sighting one member makes is written into every mate's memory with `sourceId` (who saw it) and `sourceTick` (the observe pass it was recorded on). A mate's own sighting at the same instant replaces the hand-off. `maxSharesPerTick` (default 64, per observer pass) bounds a large roster and reports elided shares.
- Perception memory is auditable without guessing: `memoryOf(observerId, targetId)` returns the one remembered target with its confidence right now or `null` once it has faded, and `stats()` counts pair checks, range prunes, cone tests, line-of-sight calls, sightings, heard stimuli, shares and dropped shares. The check order is range → cone → line of sight, so occlusion work only sees the pruned set. `resetStats()` opens a measurement window; `memories` are pruned per observer so a 200-observer pass does not rescan every memory.
- `devtoolsName` registers a probe showing observers, remembered targets and the work counters, and `dispose()` removes it.
- `createCoverField` and `coverPointsFromSolids` (`@jgengine/core/combat`) derive stand-able cover from authored solid boxes instead of a coordinate list: one row of points per vertical face, clear of the corners, filtered by an injected `walkable` test, rebuilt when the world is edited, and queried through a bounded `near(position, radius, limit)`. `CoverSource` is the seam, so a game with its own spatial index can supply cover without the box generator.
- `selectCover` / `scoreCoverPoint` choose cover by threat: `hidden` is the confidence-weighted share of remembered threats whose line of sight the point (and a body-sized footprint at it) blocks, scored against the role's preferred fighting range, flank angle around the threat, distance, a squadmate's claim on the point, and whether the walk to it is a detour. Fading memories stop steering at `minConfidence`, and a point that hides from only one of two shooters is not cover.
- `createSquadCombat` (`@jgengine/core/combat`) drives a squad over existing seams: role-driven cover selection, fire discipline (hold, lean out, shoot), suppression from `underFire()`, retreat below a role's health threshold, and relocation under sustained fire. Threat comes only from `threatsFromMemory`, i.e. the perception memory the game already keeps; movement goes through `NavCrowd` with a tighter arrival tolerance so a body actually reaches the stand point. Serializable through `snapshot()`/`restore()` (member decisions, crowd, tuning), retunable, and a `devtoolsName` probe reports pins, peeks, shots and the worst exposure.
- `assignSquadRoles` and `threatsFromMemory` (`@jgengine/core/combat`): deterministic role assignment over a caller-authored `SquadComposition` (role ids, counts, ranges, flank angles, retreat thresholds and cover weights are all content), and a bounded, confidence-ranked threat list from one observer's memory.
- A 2v4 arena soak proves the behavior: 90 simulated seconds, 2700 ticks, 4 squad members against 2 scripted raiders, worst exposure 2.33 s (bar 3 s), 0 of 38 exposure spells over 3 s, per-tick cost p50 0.05 ms / p95 0.19 ms.
- `createDamageRuntime` (`@jgengine/core/combat`) composes a damage table, hit zones, the armor curve and carried statuses into one stateful model: `hit()` returns every stage of the number (`base × zone multiplier × type multiplier × (1 − armor mitigation)`), `tick()` runs status effects through the same table and armor, `timeToKill()` quotes a fight, and `table()` reads the whole chart. `snapshot()`/`restore()` carry registered targets and carried statuses; `retune()` swaps table, zones or status definitions without dropping either; a `devtoolsName` probe reports hits, crits, ticks and the last hit.
- A damage table (`combat/damageTable`) of damage type × resistance class, with `armor / (armor + halfValue)` mitigation — armor equal to the half value halves the hit — a per-type `bypassArmor`, and `readDamageTable` / `formatDamageTable` to read every row at given armor values. An unknown damage type throws instead of silently dealing 1×.
- Hit zones and crits (`combat/hitZones`): a zone carries a normal multiplier and an optional crit multiplier, `resolveHitZone` rolls once and returns one multiplier, and a crit replaces the zone multiplier rather than compounding with it. `hitZoneMultiplier` quotes the same number without a roll.
- `createStatusRuntime` (`combat/statusRuntime`): data-defined statuses with the existing `StatusStackPolicy` (`refresh`, stack to `max`, `replace`, `ignore`) as the only thing that changes stacks, per-tick effects of `{ kind: "damage", channel }` or `{ kind: "stat", stat }`, damage routed through an injected `applyDamage` so resistances and armor still apply, bounded ticks per call, and plain-data state out and back.
- Numbers from the tests: 25 base kinetic against a 150 HP target, 0.15 s between shots — 8 shots / 1.05 s unarmored, 20 shots / 2.85 s at 60 armor, 36 shots / 5.25 s at 200 armor; two stacked burns of 4 per tick deal 8, not 16; a 1.5× head zone with a 2.5× crit multiplier deals 2.5×, not 3.75×.
- `createWeaponSystem` (`@jgengine/core/combat`) runs one `WeaponDefinition`: single, automatic or burst fire, hitscan or ballistic projectiles, recoil that moves the aim the shot flies down, spread bloom, interrupted/resumed reloads, a readable ammo economy (`ammo()`, `stats()`), `snapshot()`/`restore()` and a devtools probe. Every hit, hitscan or projectile, resolves through `createDamageRuntime`. Pellet work is bounded per tick (`maxShotsPerTick`, default 4) and per weapon (`maxInFlight`, default 32).
- `WeaponDefinition` (`@jgengine/core/combat/weaponDefinition`) as plain JSON — rate, fire mode, delivery, magazine, handling, damage, falloff — with `assertWeaponDefinition`, `weaponIntervalMs`, `weaponDamageAt` and `weaponMaxInFlight`. A generated gun is the same shape.
- `Magazine.pauseReload()` / `resumeReload()` / `isPaused()` suspend a reload and keep its progress; a paused reload is not `isReloading()`, does not advance in `tick`, and round-trips through `snapshot()`/`restore()`.
- `environment.grading` on the scene document authors a per-game colour grade: shadow/midtone/highlight swatches plus tint, saturation and contrast bake into a small display-space LUT (`lutCubeFromGrading`, `colorGradeFromDocument` in `@jgengine/core/editor/environment`), edited in the Lighting workspace. The shell applies it from `editorLayers`, and a graded game needs no LUT asset or hardcoded pass.
- Clamped auto-exposure: `postProcessing.autoExposure: { min, max, adaptationSeconds } | false` meters the pre-tone-map frame every four frames toward 18% grey and stays inside the authored range. `WHITE_POINT` and `MID_GREY` in `@jgengine/core/render/colorPipeline` are the single documented calibration the pipeline uses.
- `GradeConfig.lut` also accepts a baked `{ cube: { size, data } }` LUT, sampled trilinearly after the numeric grade.
- `MATERIAL_LIBRARY` (`@jgengine/assets/catalogs/materialLibrary`) names 56 PBR surfaces — wet asphalt, painted metal, stucco, brick, wood, foliage with cut-out coverage and sheen, glass, water, sand, concrete and more — each mapping a name to an ambientCG source, a world tile size in metres and a physical response. `libraryMaterialAsset("asphalt/wet")` returns the editable `MaterialAsset` those names resolve to, and `filterLibraryMaterials` backs pickers. `assets pull library` pulls every pinned map set.
- `TriplanarAssetMaterial` / `useTriplanarAssetMaterial` / `applyTriplanarProjection` (`@jgengine/shell/render/triplanarMaterial`) render a material asset from world position, so world geometry without a `uv` attribute still gets colour, normal, roughness and AO maps. Tile size is world metres; instanced meshes project in instanced space.
- The editor's Materials workspace browses the library by name and writes the chosen entry into the document's material assets.

### Changed

- `createPerception` (`@jgengine/core/world`, also `@jgengine/core/sensor/perception`) takes `isSegmentClear` instead of `occluded`: the game supplies collision as a "nothing blocks this segment" test, and it is only called for pairs already inside range and cone. **Migration:** flip the predicate — a `occluded: (from, to) => blocked(from, to)` becomes `isSegmentClear: (from, to) => !blocked(from, to)`.
- `MagazineSnapshot` gains an optional `paused: true` field, present only while a reload is suspended, so existing snapshots and idle shapes are unchanged.
- AgX is the pipeline's tone map: `postProcessing.toneMapping` defaults to `"agx"`, and the `cinematic`, `photoreal`, `comic`, `retro` and `STUDIO_STAGE_POST` configs now pick it too. `"toon"` keeps `"linear"` on purpose. Games that authored `"aces"` explicitly are unchanged.
- The grade pass samples LUT tiles at texel centres and blends the blue axis by hand, so a supplied strip LUT no longer bleeds across slices.

### Fixed

- `bun run debug <game>` now finds the server `bun dev` started. The website dev server publishes the same checkout marker the runner does (`scripts/devServerMarker.ts`, written at listen, removed on shutdown, dev builds only), and the marker records the host that answers, since Vite's default `localhost` can be IPv6-only where `127.0.0.1` hangs.
- The website dev server serves `GET/POST /__jgengine/report` too, so a game opened at `/games/<id>` reports from the `/play` runner it embeds.
- The in-page sampler installs when the dev report route answers, not on a build-time `PROD` check: the `/play` runner is a production build, so the old check silently disabled the report on `bun dev`.
- A report taken inside the sampler's warmup is no longer judged on link/framebuffer counts, which only mean something after the warmup baseline exists.
- `extractMaterialMaps` and `buildMaterialCatalog` now recognise ambientCG's `_Opacity` map, so cut-out foliage and decal scans keep their coverage instead of rendering as solid cards.
- Capture runs no longer leak a ~150 MB temp Chrome profile each. `shoot`, `drive` and `perf` delete their profile on every exit path — success, throw, watchdog and SIGINT — `bun run shoot` reclaims profiles a killed run left untouched for three hours, the capture guard sweeps the profile of a run it force-kills, and the generated `browser.mjs` harness for game repos removes its own. A capture also refuses to start when the temp drive has less free space than it needs.
- The post-processing chain, graphics settings and textured terrain material keep a stable identity across renders. Before, every render rebuilt the effect composer and recompiled shaders, which dropped games to single-digit FPS.

### Numbers

- Test fixture at 600 rpm, 30-round magazine: 2.9 s to empty; 17.0° pitch and 3.1° yaw climb over the magazine; back to half climb in 0.37 s and within 5% in 0.6 s; bloom 2.0 → 8.0 → 12.0 mrad (cap) by the tenth shot, settling to base 0.63 s after the last shot.
- A hitscan and a projectile contact at the same distance return the same damage (20 body, 30 on a 1.5x head zone) through one damage model; a 60 m/s projectile crosses 20 m in 0.33 s and drops 2.45 m in 0.5 s at 9.81 m/s².

## 0.25.0

### Migrate

- **Bump lockstep SDK packages to `^0.25.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.22.0`; `@jgengine/github` is `0.12.0`.
- The frame panel from `@jgengine/react/hudPanel` is now `HudSurface` in `@jgengine/react/hudSurface` (`HudSurfaceProps`, `HudSurfaceElevation`). `HudPanel`, `HudPanelProps` and `HudPanelElevation` from `@jgengine/react/hudPanel` stay as deprecated aliases for this release. The `HudCanvas` layout slot keeps the name `HudPanel`.
- Engine HUD control sounds (button click/hover, panel and window open/close) no longer turn off with `notices: false` or `notices.cues: false`. Set `audio.hudCues: false` to silence them; `notices.cues` now covers notice sounds only.
- The notice stack now defaults to z-index 85 (was 45), above `ModalHost`. Pass `notices.zIndex` to put it back under a game overlay.

### Added

- `HudSurface` takes `as` (`section`, `aside`, `nav`, ...) to render as a landmark.
- `notices.offset`, `notices.avoidHud` and `notices.zIndex`, with matching `NoticeStack` props. The offset adds to the device safe-area inset.
- `ctx.game.notices.push({ text, silent: true })` shows a notice without its sound.

### Changed

- The engine stylesheet (`@jgengine/react/styles.css` and the injected copy) sits in the `components.jgengine` cascade layer, after a `theme, base, components, utilities` layer order, so plain game CSS and Tailwind utilities override engine buttons and panels without `!important`.
- `HudSurface` padding comes from `--jg-panel-padding` (default 12px); the `padding` prop sets that variable instead of an inline `padding`, so a game class or media query can override it.
- `HudButton` labels wrap (`white-space: normal`, line height 1.2, 4px vertical padding) instead of overflowing on one line.
- The shell renders the notice stack inside the HUD theme root, so notices use the game's `hudTheme` colours and font.
- A top or bottom notice stack moves clear of `HudCanvas` panels and touch controls registered in its column (Harbor Heat's courier card, action bars).

### Fixed

- Flowing `HudCanvas` panels now register their layout region once their anchor region mounts; before, most never registered, so layout collision checks and region consumers missed them.

## 0.24.0

### Migrate

- **Bump lockstep SDK packages to `^0.24.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.21.0`; `@jgengine/github` is `0.11.0`.

### Added

- `buildGroundCoverageMask`, `createGroundEdgeFade` and `applyGroundEdgeFade` (`@jgengine/shell/terrain`) fade any ground material out at the outer edge of a merged mesh footprint, leaving seams between layers inside it crisp.

### Changed

- City studio ground (roads, junctions, block land, sidewalks, medians, drives, parks, crops, curbs, markings) now fades into the terrain where the town's ground ends instead of stopping at a hard edge. `meta.edgeFade: false` on the city volume keeps the old hard edge.

## 0.23.0

### Migrate

- **Bump lockstep SDK packages to `^0.23.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.20.0`; `@jgengine/github` is `0.10.0`.

### Added

- `player_pose` agent-bridge verb (`@jgengine/shell`, `window.__jgengineAgent`, `drive --rpc '{"method":"player_pose"}'`) returns the local player's position and heading, and `debug_snapshot` carries the same `player` field, so a drive can prove movement from position deltas instead of screenshots.
- `sim_advance` agent-bridge verb (`@jgengine/shell`, `drive --rpc '{"method":"sim_advance","seconds":30}'`) steps the mounted sim driver — input, movement, `onTick`, behaviors, bound actions — in 0.05 s frames without rendering, so a headless drive can reach waves, shifts and timers that never arrive at software-rendered frame rates. Capped at 120 s per call.
- `time_scale` agent-bridge verb sets the game clock's timescale and reports `{ timescale, now }`.
- `GameContextObjectEntry.scenery` (`@jgengine/core`): an object kind placed once and left alone. Its runtime placements now render through the shared instanced static-object batches that editor-authored props already used.
- `BUILDING_STYLE_PROBABILITIES` and `resolveBuildingProbabilities` (`@jgengine/core/world/buildings`): each `BuildingStyle` now carries facade-part probabilities, so an industrial yard shows few windows and roof plant, a ruin loses awnings and clotheslines, coastal fronts gain awnings. `building({ probabilities })` overrides any part.
- `debug_summary` agent-bridge verb and `debug_snapshot` `detail: "summary"`: fps, frame/sim ms, draw calls, triangles, entities, warnings and top culprit in about 1 KB. `drive --rpc '{"method":"debug_snapshot"}'` prints the summary; `"detail":"full"` keeps the full snapshot.
- `modelLoadStarts()` (`@jgengine/shell/render/modelLoad`) counts model streaming bursts for capture quiescence checks.
- `jgengine` warns when it is not the CLI the caller meant: a global install run inside a jgengine checkout, a project-local `jgengine` shadowed, or a CLI built for an older SDK than the project installs. Harness runs print the CLI version and harness revision.
- `withGraphicsQualityOverride` and `parseGraphicsQuality` (`@jgengine/core/settings/graphicsSettings`) pin a graphics tier for one session without persisting it.
- The player shell reads `?quality=<tier>` and pins that tier for the page. `bun run shoot`/`drive --quality draft` use it (low tier, half size) for fast iteration shots; `--quality full` stays the default for PR evidence.
- `jgengine doctor` flags a silent game: one that neither declares `defineGame({ audio })` cues nor calls `ctx.game.audio.play`/`loop`. Four reference games shipped without a single sound; the fix text points at procedural `SoundDef` synth patches, which need no asset files.
- `drive --key mouse0:<ms>` (also `mouse1`, `mouse2`) holds that mouse button over the play canvas, matching the shell's pointer binding codes, so hold-to-fire weapons can be driven headlessly. Keyboard codes are unchanged.
- `drive --right-click-at x,y` sends a native right click in CSS viewport pixels, so RTS move/attack/gather orders and context actions can be driven headlessly; `--click-at` stays a left click.
- `ctx.scene.entity.selection` (`@jgengine/core`): the player's selected entities as a reactive `SelectionSet`. The shell pointer (`pointer.select` click / marquee) now writes it instead of private component state, so game HUDs can show the selected unit's card or a "3 units selected" summary and can set the selection themselves (select-all, control groups); the selection ring follows either way. Despawned ids drop out.
- `LightingConfig.iblAmbientScale` (default 0.5) and `LightingConfig.fallbackKey` (default true); `balanceLightingForIbl` in `@jgengine/core/render/lightingBalance` applies them.
- `DirectionalLightingConfig.shadowRadius` (PCF penumbra in texels, default 1.25).
- `DEFAULT_AO` in `@jgengine/core/render/postProcessing` and `mergePostProcessing` in `@jgengine/core/render/lookPreset`.
- Loaded GLB models get a default painted finish: flat-colour, vertex-colour and palette-atlas materials have their exported roughness/metalness/`envMapIntensity` normalized and compile with one shared object-space detail shader (mottling, wear, roughness break-up, micro-normal, cavity). Normal-mapped, cutout and transparent materials are untouched.
- `GraphicsProfile.surfaceDetail` (`"off" | "lite" | "full"`, `@jgengine/core/settings/graphicsProfile`): `"lite"` on low, `"full"` on medium and up.
- `ModelConfig.surfaceDetail: false` opts one model out; `defineGame({ surfaceDetail: false })` opts a game out.
- `createSurfaceRibbonMaterial`, `ribbonSurfaceFrame`, `resolvePathSurface` and `useSurfaceRibbonMaterial` (`@jgengine/shell/terrain`) shade ground ribbons with the terrain's procedural surface functions, plus wear and a noise-warped soft edge.
- `road({ surface })` (`@jgengine/core/world/features`) picks the carriageway surface; `false` keeps flat colour.
- `AuthoredScene`/`AuthoredPaths` `pathSurfaceDetail`/`surfaceDetail` props and path `meta.surface`, `meta.edgeSoftness`, `meta.wear`, `meta.surfaceDetail`.
- `EditableTerrain.gridBounds` (`@jgengine/core/world/terraform`) exposes the world extent of the paint/height cell grid.
- Painted terrain renders each layer's `surface` as its own procedural ground (grass, dirt, gravel, rock, sand, mud, snow, road) with albedo variation, roughness and micro-normals, blended along noise-warped, height-aware edges, in play and in the editor viewport. No game config or assets needed.
- `TerrainDetailConfig.surfaceStrength` (0..1, default 1) scales the per-surface response; `detail: false` opts a terrain out to the flat vertex-colour ground.
- `@jgengine/shell/terrain` exports `packTerrainPaintWeights`, `createTerrainPaintTextures`, `terrainSurfaceClass`, `createTerrainDetailMaterial` and `TerrainQualityContext`/`useTerrainQuality`.
- `ctx.game.notices` (`@jgengine/core/game/notices`, `createNoticeFeed`): a player-facing notice feed with info/success/warn/deny tones, stacking dedupe (repeats show ×N and refresh their lifetime), a cap, TTL, `onPush`, `retune` and `snapshot`/`restore`. Pure state on the context clock, so headless and hosted runs are unaffected.
- The shell mounts `NoticeStack` (`@jgengine/react/notices`) for every game: animated toned cards below top HUD bars, 14px text on `--jg-*` vars with fallbacks, no pointer capture. Configure with `notices: { placement, hidden, refusals, cues, cap, ttlMs }` or remove with `notices: false`.
- Refusal feedback: `validate` may return `{ reason, message }`. `reason` stays the machine code; a `message` is shown automatically as a deduped deny notice with the deny cue when the local player's command is refused, locally or from a host. `ctx.game.commands` emits a `command.refused` event for every refusal; `attachRefusalNotices` is the headless hookup.
- UI sound cues: `useUiCue()` / `playUiCue` (`@jgengine/react/uiCues`) play `click`, `hover`, `open`, `close`, `confirm`, `deny`, `notify`. Soft procedural defaults (`@jgengine/core/audio/uiCues`) mix on a new "ui" bus with a "UI volume" setting under master volume. A game sound named `ui.<cue>` replaces one; `audio.uiCues` overrides or silences cues (`false` for all).
- `notice_push` and `run_command` agent-bridge verbs for `drive --rpc`.
- `ui-feedback` dev probe (apps/dev) exercising refusals, the feed and cues.
- `HudTheme` gains a type scale with a 12px floor, spacing, motion (durations and easings), elevation (layered shadows and a lit top rim), and hover/press/focus/disabled state tokens; `hudThemeVars` emits them as `--jg-type-*`, `--jg-space-*`, `--jg-motion-*`, `--jg-ease*`, `--jg-elevation-*`, `--jg-highlight`, `--jg-hover-*`, `--jg-press-*`, `--jg-focus-ring`, and `--jg-disabled-opacity`. `@jgengine/react/hudTokens` reads them in inline styles (`hudType`, `hudSpace`, `hudTransition`, `hudElevation`, `hudFont`, `HUD_FOCUS_RING`).
- Engine stylesheet `@jgengine/react/styles.css` (also `ENGINE_STYLESHEET` / `useEngineStyles` in `@jgengine/react/engineStyles`): every `jg-*` keyframe the shared blocks and registry use, `[data-jg-*]` interaction states, quiet hover/press/focus defaults for plain HUD buttons, and reduced-motion handling for `prefers-reduced-motion` and `[data-reduced-motion="true"]`. The `jgengine create` template imports it.
- `HudThemeRoot`: the shell wraps every `GameUI` in it, applying `defaultHudTheme` and the stylesheet. `defineGame({ hudTheme })` swaps the root theme; `hudTheme: false` opts out.
- Primitives `HudButton`, `HudPanel` (open/close transition via `usePresence`), and `TweenNumber` / `useTweenedValue` with an up/down delta flash; `useReducedMotion` in `@jgengine/react/accessibility`.
- `HudInteractProvider` / `useHudInteract`, plus `cue` and `onInteract` on the primitives, windows, modals, and `ActionButton`: one seam for a UI sound system.

### Changed

- Static-object batches group same-model props per 64 m cell from 3 placements (was 24 m cells and more than 8), so sparse city dressing such as palms, lamps and benches shares draws. Harbor Heat's opening drops from 5,310 to 4,650 draw calls.
- `building()` districts with a non-generic `style` generate a different facade mix than before (previously a style changed colours only). `generic` is unchanged.
- `bun run shoot/drive` and the scaffolded/bundled `shoot.mjs`/`drive.mjs` parse one shared flag table: `--click-at`, `--right-click-at`, `--width`, `--height`, `--port`, `--size`, `--settle`, `--param`, `--probe`, `--reload`, key chords and mouse holds work in both; unknown flags fail instead of being ignored or taken as a game id; dev-runner-only flags are refused by name in the portable harness.
- Play-mode capture readiness waits until staged `--state`/`--run` commands have ticked, their entities mounted and model streaming stopped, and never declares ready over "Loading game view…". A rejected staged command fails the capture.
- Play-mode captures of a game with a start menu and no `capture.play` click its start action and report what they clicked.
- Monorepo tests, type-checks and `bun run gen` resolve `@jgengine/*` to package source through the `paths` in `tsconfig.base.json` (`bun run gen:source-paths` keeps every workspace tsconfig in sync), so a fresh checkout or worktree is testable with no build. Published packages are unchanged: `exports` still point at `dist`, and the built output is byte-identical.
- The export manifest is computed from the files each package's build emits, not from `dist` on disk, so it can no longer drift with a stale or partial build. `bun run ship:preflight` now fails with the fix (`bun run gen`) when any generated artifact is stale.
- `bun run agent:worktree -- <name>` produces a clean, ready worktree locally and in cloud sessions: it links dependencies, `Games` and the dev runner's ignored assets from the main checkout instead of installing and building. `agent:bootstrap` only installs; pass `--build` to also build dist.
- The procedural IBL probe has a zenith/horizon/ground gradient and an HDR sun disc plus a broad lobe, with the horizon leaning toward the ambient colour, at intensity 0.45 (was a 2-colour gradient at 0.35), so forms get a lit and a shaded side from IBL alone.
- Under IBL (every look but `"flat"`), authored ambient is halved, and lighting with no directional light gets a soft shadowless key light (intensity 0.6, tinted by the ambient colour).
- A post chain that omits `ao` now runs `DEFAULT_AO` on medium tier and above; `ao: false` still opts out. `NEUTRAL_POST_PROCESSING` no longer sets `ao: false`.
- Styled look presets deep-merge their post stages under a partial game `postProcessing` instead of being dropped whole.
- Shadows use `PCFShadowMap` instead of `PCFSoftShadowMap`, which three r182 compiles as unfiltered basic shadows (stair-stepped edges), with a default radius of 1.25 texels on configured and cascaded lights.
- GTAO samples are tiered: 8 on medium, 12 on high (was 16), 16 on ultra.
- A model `material.rim` override now chains the material's live program cache key instead of a snapshot.
- Authored paths now render as dirt (or their `meta.surface`/kind) with wheel ruts and edges that fade into the terrain, instead of flat-colour ribbons with hard cut edges.
- `road()` carriageways, curbs, sidewalks and welded junctions, and city studio ground (streets, block land, sidewalks, drives, parks), shade with matching procedural asphalt, gravel, concrete and dirt. City volumes opt out with `meta.surfaceDetail: false`.
- Terrain noise and micro-normal GLSL moved to shared exports in `terrainSurfaceShader.ts` so terrain and ribbons compile the same code.
- A terrain without `detail` now renders with default procedural detail instead of flat vertex colour.
- Slope rock no longer fades with `detail.strength`; it follows `surfaceStrength`.
- The textured terrain path's normal map is applied in view space (it was added to the view-space normal as a world-space offset).
- The `low` graphics tier takes a cheap terrain surface path without micro-normals.
- The shell plays UI cues for every engine HUD primitive (`HudButton`, `HudPanel`, `Window`, `ModalHost`, `ActionButton`): activate → `click` (or the primitive's `cue` when it names a cue, e.g. `cue="confirm"`), button/action hover → `hover`, panel open/close → `open`/`close`. Off with `notices: false` or `notices.cues: false`; `audio.uiCues` silences single cues. `uiCueForInteraction` (`@jgengine/react/uiCues`) exposes the mapping.
- `CommandRejection`, `CommandValidationError`, `CommandResult`, `RunCommandResult` and `TransportRunCommandResult` carry an optional `message`; the ws hosts forward it. Results without one are unchanged.
- `defaultHudTheme` is the engine's default look: a system UI font stack led by Inter, layered translucent panels with a lit rim and hairline borders, and gradient slots.
- `Window` / `PanelHost` render on `HudPanel` with enter and exit transitions and a `HudButton` close; `ModalHost` fades its backdrop and lifts its dialog, with an exit; `ConfirmDialog` / `PauseMenu` use `HudPanel` and `HudButton`; `HudFrame` and `ActionButton` read the font, elevation, focus, and motion tokens.
- Text under 12px in `@jgengine/react` and `registry/jgengine` now uses the type scale's `xs` step (12px floor).

### Fixed

- Games on the `rts` and `topDown` camera rigs hear positional sound again. The spatial-audio listener sat at the camera, 35–50 m above the ground, so the default 24 m falloff silenced every ground event even at screen centre; for these rigs it now sits where the camera looks at the ground. Chase, first-person and other rigs keep the camera-anchored listener.
- The bundled `jgengine drive` and `jgengine shoot` harnesses let Chrome pick a free debug port instead of guessing one from the clock, so they no longer attach to another Chrome that holds the same port. An interrupted drive or shoot (SIGINT/SIGTERM) now closes its Chrome instead of leaving it running.
- `jgengine doctor` flags component folders reached through a `tsconfig` path alias (such as a shared `@/components/ui/*` registry) that game code imports but `src/index.css` never scans with `@source`. Their Tailwind classes were silently not generated, so those HUD blocks rendered tiny and unstyled; the fix text names the exact `@source` line to add.
- `jgengine doctor` now flags a 3D game that sets neither a `look` preset nor `postProcessing`; such a game renders the unstyled `"neutral"` look with no tone-map grade, AO or bloom. Its fix text no longer claims cinematic is the default. Write `look: "neutral"` when unstyled output is deliberate.
- `jgengine create` scaffolds name `look: "cinematic"` explicitly, so a new 3D game starts graded instead of on the unstyled default.
- Generated city districts (`@jgengine/shell` `CityRenderer`) fade their developed ground to transparent over its outer 9 m, so it melts into the terrain or authored paint beyond it instead of ending in a hard, stair-stepped green rim.
- City districts (`@jgengine/shell` `CityRenderer`) draw a crossing of gravel lanes as gravel instead of a dark asphalt slab; a crossing that touches any paved street stays asphalt.
- The published CLI and scaffold browser driver restore game keyboard focus before key holds and wait for a rendered frame under the held key. Menu clicks and slow headless rendering no longer silently swallow movement inputs; failed frame sampling releases held keys.
- After a click on a control that then unmounts (a start screen's "Play", a menu's last button), `@jgengine/shell` returns focus to the play surface instead of leaving it on `<body>`, so gameplay keys work without an extra click on the canvas.
- `sim_advance` (`@jgengine/shell` agent bridge) yields to the browser every ten simulated seconds, so models for entities spawned mid-advance mount as in play; a 30 s advance through Beacon Bastion's first wave previously left every raider with health bars but no model.
- Generated city districts no longer lay a checkerboard over their ground. The developed-ground fabric drew one flat quad per 2.5 m cell with a per-cell random roll that flipped neighbours between zone bands; it is now one shared-vertex sheet coloured from smooth noise, so yards and lots blend.
- Authored terrain paint (paths, clearings, painted layers) now blends smoothly between neighbouring cells in the runtime ground and the editor sculpt view, instead of snapping each vertex to its 2 m cell and drawing grid-aligned stair-step edges. Unpainted cells blend toward the caller's base ground color, so painted edges fade into the terrain palette.
- A terrain detail `material` no longer overrides the ground's authored roughness with its roughness map. The map now varies roughness around the authored value, so scanned ground maps (roughness near 0.45) stop giving dry terrain a wet specular sheen; games no longer need to lower `material.strength` to stay matte.
- A terrain without a `waterLevel` no longer paints the detail shader's beach sand band. Previously the band defaulted to water at height 0, so any flat terrain near 0 (a park, a meadow) rendered as beach; six reference games set `detail.waterLevel` far below the world only to avoid it. Set `waterLevel` on the terrain or `detail.waterLevel` to keep a shoreline.
- Atomic vitals bars (`HealthBar`, `ManaBar`, … in `@jgengine/react`) cap their frame at a quarter of the bar height, so thin bars (e.g. `height: "4px"`) show their fill instead of only the two 2px frame lines.
- Built-in world health bars (`worldHealthBars`, `@jgengine/shell`) span about 1.4 m at the entity's on-screen scale (28–112 px) instead of a fixed 112 px, so top-down and RTS cameras no longer paint bars ten times wider than the units under them.

## 0.22.0

### Migrate

- **Bump lockstep SDK packages to `^0.22.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.19.0`; `@jgengine/github` is `0.9.0`.

### Added

- Editor-authored grass exposes blade width and bend ranges, tuft radius and normal lift, using the same bounded grass renderer as environment fields. Existing scene documents keep their previous shape and lighting defaults.
- The full `jgengine skills` install includes `game-improvement`, with an ordered implementation workflow and checks for meaningful decisions, authored terrain and grass, and proven play quality.

### Fixed

- Grass volumes sample terrain at their authored world position and subtract their placement height once, keeping translated patches rooted on slopes instead of buried underground.

- Authored grass respects spawn, objective and road/path clearances with feathered regrowth outside the usable footing. Diagonal paths keep their own corridor clear without clearing the entire surrounding rectangle.
- Runtime environment ground now renders editor-authored terrain paint surfaces, layer tints, opacity and weighted blends through the same sampler as the editor. Legacy unpainted terrain keeps descriptor shading. Layer texture tiling, roughness and triplanar controls remain serialized authoring metadata; this fix does not add layered PBR texture rendering.

## 0.21.0

### Migrate

- **Bump lockstep SDK packages to `^0.21.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.18.0`; `@jgengine/github` is `0.8.0`.

### Added

- Imported model animation can explicitly follow game speed controls while preserving the rig's automatic clip roles. Character clock and automatic-role controls survive authored scene edits, undo, save and reload; existing real-time playback remains the default.
- `readAuthoredAnimation` and `createAuthoredAnimationReader` consume saved marker animation for game-owned character mappings, with live document caching and located repairs for invalid configuration. Authored animation replaces game defaults; omitted or invalid overrides retain those defaults.

### Fixed

- Selected game-clock model animation samples locally accepted clock progress, keeping gait and clip time aligned with clamped and fixed simulation steps. Saved clock offsets cannot seek playback; external clocks without the capability receive a repair warning. Real-time playback and unbound previews retain their behavior.
- Entity-bound imported animation holds its graph and mixer while the game clock is paused or has zero timescale, retaining queued triggers and playback for resume. Unbound previews keep playing.
- Clear inferred character gait across placement discontinuities and driven actor motor state on despawn, preserving graph timing, ordinary hydration and pre-spawn restores.
- Character Inspector and Graph panels show located errors in malformed saved animation overrides and wrap long diagnostic paths and block silent partial repair during unrelated edits. Explicit replacement remains undoable, and valid authored extensions and variant order survive editing.
- Static authored character placements use the shared strict animation reader, matching inspector and dynamic-character validation. Malformed saved overrides preserve catalog animation defaults and report located repairs without changing the document or placement.

## 0.20.0

### Migrate

- **Bump lockstep SDK packages to `^0.20.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.17.0`; `@jgengine/github` is `0.7.0`.
- World health bars and nameplates now hide behind blocking world geometry by default. Use `occlude: false` for intentional reveal overlays; distance is measured from the render camera. Nameplates suppress machine identifiers unless an explicit `resolveName` supplies a display label.
- Character controller standing and explicit crouch heights must be at least `2 * radius`, and crouch height must not exceed standing height. Invalid dimensions and control ranges now throw with repair guidance instead of creating a capsule below the declared feet.
- Await `useGame().commands.run(...)` when showing command results: authoritative shells now wait for host acknowledgement; offline calls remain synchronous. Trusted host execution stays on `ctx.game.commands.run` / `runAs`.
- Remove manual `ctx.time.advance` inside `ctx.sim.advance` callbacks. Simulation advances it once and supplies scaled gameplay time as the callback's third argument; prediction opts out with `{ advanceTime: false }`.
- Await hosted `save()` and Node `flush()` at persistence boundaries. Games can retire mirrored restore logic after migrating legacy saves; retain projections the UI reads. State never present in an old save cannot be reconstructed automatically.
- Direct hosted-runner callers must join or resume a member before submitting its input; frames from nonmembers are ignored.
- `MaterialMaps.ao`, `MaterialMaps.roughness`, and matching terrain roles are optional. Pass the resolved maps directly to material seams or check for a URL before loading it. Native ambientCG pulls no longer advertise unprovided KTX2 maps.
- Default offline `ctx.game.save` now reports error when local storage is unavailable, denied, or full. Check `status()` before displaying saved; `await save()` alone does not imply success. Explicitly injected backends keep their policy. Choose `persist: { storage: "memory" }` or `memorySaveBackend()` for intentional ephemeral/headless saves.
- Slot index publication remains a separate operation: a payload may succeed before metadata fails, without cross-key rollback. Use the gameplay device-save-status recipe for checkpoint adoption and truthful UI.
- Custom `createQuestJournal` reward dependencies and `applyQuestRewards` appliers with multiple item rewards must supply an all-or-reject `grantItems` callback. Legacy single-item callbacks remain supported; multiple items without a batch callback reject before writes.
- When constructing a complete `StreamingSettings` object, add all required fields: `maxConcurrentLoads: 4`, `maxResidentBytes: Infinity`, and `residentEvictionOrder: "oldest"`. Callers using `Partial<StreamingSettings>` or spreading `DEFAULT_STREAMING_SETTINGS` inherit these defaults. Existing streaming fields remain required in complete objects.
- `SpatialGrid.forEachPair` now throws `RangeError` for negative or nonfinite distances. Pass a finite, nonnegative distance.
- Explicit `StreamingSettings` objects need `maxConcurrentLoads` (default 4). Cancelled loads keep a concurrency slot until settlement; loaders must settle after cancellation and an `unload` hook releases stale successful loads.
- `setEnvironment` now rejects invalid environment data before changing editor state, history, or live revisions. Use supported `day`, `dusk`, or `night` presets and valid authored fields; rejection reports the existing document path diagnostic.
- Pass the admitted `userId` to direct `WorldGameHost.getServerView()` reads after awaiting `joinServer()`. Reads without successful admission return `null`.
- `WorldGameHost.stop()` now permanently closes admission and ticks, drains accepted operations and saves. Await its completion and construct a new host to restart; failed saves reject the same repeated stop promise.
- Consumers asserting an exact GameContext snapshot key set should include the always-on `pursuitBehaviors` key; empty worlds contain `{ version: 1, instances: [] }`. Replace game-local pursuit/home/cooldown maps with the descriptor when its direct movement and target policies match the game.
- Shaped-grid dimensions must be positive safe integers; footprint cells and placement origins must be finite safe integers. Empty or fractional footprints and invalid quarter-turn rotations reject before overlap checks.
- Bare APIs keep their existing default discovery behavior. Opt into incremental mode only if every membership/position write reports its ID or invalidates. Subsets with a broader position resolver must report explicit candidate presence; omitted presence asserts membership when the position resolves. Unresolved candidates retain a fallback proportional to their count. Direct runtime entity mutation requires `invalidateSpatial()`.
- Scene rays require finite origins, direction magnitudes and nonnegative finite `maxDistance`; invalid rays throw `RangeError`. Bound each caller's range instead of passing `Infinity`. World-solid cell sizes must be finite and positive.
- Use `RegenShield.refill(amount)` for pickups and scripted point gains. Numeric `restore(amount)` remains a deprecated compatibility alias; `restore(state)` loads saved shield state.
- Custom asset import files now require safe relative POSIX paths, printable roles and plain JSON metadata; `classifyAssetFile` returns `null` for unknown bytes even when the filename has a supported extension.

### Added

- `EditorSession.transaction(commands)` stages heterogeneous authoring commands atomically with indexed validation errors and one undo/redo step.
- Editor prefabs retain validated static bake settings with local collision solids and named clearance volumes; `set_prefab_static_bake` supports atomic authoring, persistence and undo/redo.
- `@jgengine/assets/staticPrefabBake` exports pinned static prefab source models with preserved transforms, materials, reachable textures and provenance, plus deterministic budgets and collision metadata.
- Catalog models and extras support `anchor: "origin"`, preserving composed asset coordinates through shared rendering and collider resolution. Unmapped objects use the same catalog lookup in both paths, allowing origin-only game rendering maps to be removed.
- Named, versioned player scene saves through injected `CreatorDocumentStorage`, with optimistic revisions, schema validation, catalog permissions and explicit work budgets.
- Production `GameHost` creator configuration controlled by game-owned menus, with shared Create/Edit/Save/Reopen and fresh isolated document-snapshot playtests.
- Named particle emitters reuse the existing director, pooled simulation, and shell renderer. Start/stop preserves live particles; burst, seed, bindings, and output remain retunable and serializable. Capacity checks reject standing-emitter overflow explicitly.

- Disc, sphere, and cone spawn volumes compose with the shared force-field sampler. Explicit environment influence shares live wind and physical field envelopes; cosmetic pools follow simulation pause and time scaling. Particle collisions use bounded scene queries and consume-once cosmetic events; bounded child impacts cannot recurse. Graphics settings cap visual pools without changing physical authority.

- The existing renderer supports instanced streaks, flakes, flames, smoke, ribbons, and ground ripples alongside point output. Caller-selected projectile and flock models follow authoritative simulation positions; their existing model owner handles animation and cleanup. Habitat gameplay actors remain game-owned.
- Scene markers can store `meta.hiddenNodes` as exact node names, shared by runtime props and editor material previews. Omission preserves model defaults; an empty list clears the selection. Nonempty selections keep automatic props on individual renderers, and generic scatter rejects unsupported node hiding before cloning.
- Optional live travel in `createProjectileSystem` captures launch pose, applies injected environmental acceleration with per-shot response, target masks and force caps, sweeps each authoritative segment, and settles hits, range misses and lifetime expiry once. Active poses and detached snapshot/restore support caller rendering and replay; active and retained-shot budgets bound work and memory. Settlement reports and `projectile.settled` events carry the matching `shotId` and detached actual effect results so game policy can run after contact. Legacy immediate settlement remains available.
- Shared `physics/forceVolume` sampling covers signed sphere/box attraction, directional forces, independently tuned vortex spin and axial lift, attenuation, target masks and caps without renderer state. Authored physical fields and cosmetic particles reuse the sampler.
- Authored weather profiles, absolute schedules, transitions and local wind zones share one renderer-free sample with gameplay and precipitation.
- Persistent bounded surface wetness, snow, heat and exposure support snapshots, restoration, retuning and injected storage.
- Weather rendering consumes simulation time, clips particles against the nearest 32 authored shelters and supports bounded ground ripple pools.
- Bind editor-authored reusable material assets to named slots on instanced BuildingKit models, preserving omitted native materials, independent caller styles, and owned resource cleanup.
- Animation inspector playback rate, hold, single-clip seek and loop controls, and variant selection through existing scene metadata/RPC.
- Game-chosen movement catalog schemas, partial tuning with repair diagnostics, memoized document readers and stable live bindings that preserve game callbacks and physics policy.
- Optional live maximum climb grade in caller-declared movement catalog schemas, preserving the game's height sampler and existing default policy.
- Optional positive heightfield character collision height, authored through ordinary movement catalog controls and live bindings while preserving game policy, model proportions, camera placement and other controller dimensions.
- `resolveRigNode` (`@jgengine/shell/render/rigNode`) resolves an instance's bone or Object3D slot by unique runtime/original imported name with structured diagnostics. Resolve once per model/slot change, preserving animated hierarchy, local offsets and clone isolation.
- Supplied-only movement catalog controls for existing jump buffering, coyote grace, jump release, apex/fall gravity and landing recovery. The collapsed Jump response group saves and applies live tuning while preserving game policy, with located diagnostics for invalid values.
- Shared player movement exposes cached, read-only grounded, vertical velocity and crouch telemetry through indexed possession ownership. Unknown actors and restored movement report no telemetry until a physical step completes.
- Model graph playback feeds resolved `grounded`, `verticalSpeed` and `crouched` parameters without writing the entity blackboard. `readModelAnimationParams` supplies the same behavior to custom render hosts with a reusable output dictionary; custom movers retain their authored parameters when motor telemetry is unavailable.
- `playerMovementTelemetry(ctx, entityId)` (`@jgengine/core/movement/playerMovement`) exposes the last shared motor’s grounded, vertical velocity and crouch state through indexed ownership. It reuses a read-only view and returns `null` for unmanaged, unstepped or despawned entities, after restore/forget, and when another authority changes the motor proposal; it does not write animation stores each frame.
- `suspendPlayControls` composes independent player-control suspension leases across overlapping menus without pausing shared simulation.
- Anonymous authority fixtures accept validated, separate frontend and realm ports with an explicit frontend websocket endpoint.
- Relay Courtyard declares a persisted compact or detailed readout through the shared HUD settings framework.
- Core content reference and declared-source progression validators report located repair guidance across game-owned catalogs. Closed-source analysis detects unavailable prerequisites; intentional seeded cycles remain valid.
- Native recipe catalog validation checks item/station/unlock references, quantities and duplicate inputs without imposing economic or output-benefit policy.
- `useDialogBehavior` (`@jgengine/react/dialogBehavior`) shares dialog focus entry, Tab cycling, Escape close requests, and restoration with caller-owned screens; `ModalHost` uses the same behavior.
- `resolvePeerShellMultiplayer` can host a playable authoritative GameContext world with injected persistence, signaling, peers and tick scheduling; `close()` awaits its save boundary.
- `createWorldGameHost` supports a serialized `slotsPerServer` player cap while allowing existing members and spectators.
- `bindCommandTransport` / `dispatchCommand` provide the shared UI command boundary; `possession.ownerOf` resolves unique recorded ownership.
- A source-owned anonymous authority fixture reports separate compiled frontend and realm revisions and preserves throwaway progress through reload.
- `GameContext.state()` / `restore()` capture detached authoritative state without an offline save backend; hosted runners expose `state()` and `commit()`.
- Hosted sessions persist player identity and bounded command retry receipts alongside world state, expose `hasPlayer()` and `persistenceError()`, and accept stable operation ids.
- `disposeModelScene` (`@jgengine/shell/render/modelRender`) releases a `cloneModelScene` instance's materials and bone textures while preserving loader assets and separately owned attachments.
- Asset streaming supports a retunable `maxResidentBytes` target and deterministic oldest/largest eviction within the frame unload budget. Pinned and retained assets remain protected; byte pressure overrides idle grace and small-asset caching.
- Streaming residency diagnostics expose protected and over-budget bytes, unknown-size loads, eviction totals, and record counts. Explicit `forget(id)` removes only settled, inactive, unowned history so demand churn can bound metadata without silently invalidating record consumers.
- `assets budget` scans custom/imported or pulled GLBs offline and reports per-model bytes, stored triangles, and image dimensions with configurable CI limits and actionable failures.
- `readGlbMetrics` and Node `createAssetBudgetReport` / `checkAssetBudget` expose the same inspection; reindex records successful inventory in optional `IndexEntry.metrics`.
- The 3D shell batches compatible authored catalog props above eight placements per spatial cell into instanced model draws, preserving each chosen model, material group, shadow mode, collider measurement and object id for picking. Interactive, custom, animated, composed, transparent and mirrored models retain individual renderers; culled placements leave the submitted instance set.
- Bounded SFX reservations (default 64 simultaneous playbacks) with configurable total/per-sound caps, priorities, reject/steal policy, injected allocator storage, snapshot/restore, and live retuning.
- React exports `observeBrowserSuspension` and `useBrowserSuspension` for configurable blur, hidden-page and pointer-lock loss notifications. Games retain their pause and modal policy; listeners detach on cleanup and the hook invokes the latest callback.
- Compose deterministic timed away missions with pure expedition dispatch, bounded offline settlement, finite supply consumption, death, item capacity, and safe return transitions (`@jgengine/core/work/expedition`). Caller-owned policy and content use a persisted RNG cursor and plain saveable state.
- Apply caller-authored elapsed-time loot curves with `timeScaledRarity` (`@jgengine/core/game/lootModifiers`), retaining original and effective odds in loot pipeline provenance.
- Generated item registries accept caller-owned synchronous storage and a saved allocation sequence; `statePages` and `restorePages` stream detached save batches through atomic replacement while preserving the existing whole-state format.
- `validateQuestCatalog` (`@jgengine/core/game/questCatalog`) checks authored quest ids, references, quantities, and blocked prerequisite dependencies with explicit external unlock and quest-start declarations, optional caller catalog lookups, and located error/warning diagnostics. The `quest-authoring` CLI recipe validates bounded generated batches against caller-owned canonical facts.

- Snapshot and freeze reviewed authoring inputs across async generation so caller or generator mutation cannot replace canon, catalog references, or batch limits.
- Resolve a bounded staffing snapshot into a production rate with `stationOutputRate` (`@jgengine/core/work/staffedStation`), using caller-owned stats, base output, per-stat scaling, and efficiency. The pure function composes with existing production and serializable game state.
- Key-value cells and local save backends accept optional storage failure observers and `errorMode: "throw"`, exposing original read/write/remove errors while retaining their legacy fallback defaults.
- Asset streaming `retune` merges streaming policy; setting concurrency to zero pauses new loads.
- Pure compatible footprint merge/split with retained source identities and unequal capacities, JSON roundtrip, connected partitions and optional linear work budgets. Grid reservation commits, upgrade pricing and arbitrary cell-cut allocation remain caller-owned.
- Add read-only native material assignment preflight for selectors, UVs, construction prerequisites and scoped authored texture-role restrictions.
- `ModelConfig.hiddenNodes` and `useModelInstance` support per-instance named visual subtree selection before placement measurement, retaining imported rigs, clips, textures and cached source visibility. Missing or ambiguous names warn without hiding arbitrary nodes; malformed lists reject before model or texture allocation. Changing the selection replaces only that instance, while equivalent list contents retain its clone.
- Simple model and authored surface map configs now expose all 20 supported `MaterialTextureRole` URL fields. Composed model dependency discovery preloads advanced physical maps while preserving the seven legacy roles' loader order and ignoring unsupported roles. Authored surface keys include advanced URLs, so retuning reloads them and equivalent map ordering retains owned resources.
- Declarative `PersistConfig.migrate` forwards the existing runtime-save migration hook, allowing games to convert or reject incompatible old world snapshots before restoration. Matching versions bypass it; omitting migration preserves compatible mismatch loading.
- `HostRouter.drain()` waits for accepted message handlers and subscription reads; call `close()` first to prevent further work before owned host persistence teardown.
- `pursue` behavior data schedules bounded target acquisition, solid-aware chase, cooldown effects and return to spawn through the existing behavior lifecycle; optional threat uses the shared threat table.
- `StaticShapeInstances` batches document-derived boxes/cylinders by PBR, sampler and shadow policy, with owned resources and conservative transform bounds. `AuthoredSurfaceMaterial` and `useAuthoredSurfaceMaterial` compose shared material overrides, cloned texture-role color spaces, configurable wrapping/anisotropy, normal scale and optional metre UVs.
- `jgengine upgrade --plan/--apply [--to x.y.z]` reviews and updates existing root workspace SDK catalogs from verified published versions, preserving game declarations, editor overrides, and unrelated text; installation and lockfile updates remain a separate step.
- `jgengine doctor --workspace [root] [--json]` checks installed SDK identities across the root and declared workspace owners with bounded manifest enumeration, explicit incomplete coverage, and separate resolver checks for distinct directories sharing a manifest.
- Discoverable shaped-grid placement operations and bounded first-fit search with caller-ordered rotations, row-major positions, and distinct no-space/budget results.
- Bare spatial APIs can opt into notified incremental indexing with `incremental: true` and `updateEntity(id, candidatePresent?)`. An explicit presence flag supports candidate subsets independently of the position resolver. Entity stores offer a committed-ID/membership hook before existing subscribers.
- `ScrollRail` supplies native horizontal scrolling, visible overflow navigation, and keyboard focus reveal for caller-owned controls.
- `WorldGameHost.unload(serverId)` explicitly saves and releases idle world references. It refuses players, spectators and resident members, retains a world after save failure, and reloads through the session factory on a later join.
- `createWorldSolids` supplies optional `inRay` candidates through crossed hash-grid cells, preserving live layers and oversized blockers. Sparse or unrepresentable traversals fall back to cached AABB ray tests without dropping geometry. Optional caller-owned work counters expose cell, entry and bounds costs. Scene queries use it automatically; `inBox`-only adapters remain compatible.
- Heightfield walking supports an optional `movement.maxClimbGrade` uphill rise/run limit, with axis sliding and accepted-position foot grounding. `movement.climbGradeHeight` preserves game-specific terrain sampling independently of effective ground height. Omitted configuration leaves existing movement unchanged.
- `jgengine find --json --limit N` returns bounded ranked capability and recipe matches with total, partial-match, and truncation metadata. Invalid options now produce actionable errors.
- `validateDialogueGraph` diagnoses broken references, duplicate ids, unreachable nodes, and dialogue without a route to an ending using iterative linear traversal. Authoring guidance separates structural validation from story review.
- `freezeNarrativeCanon` and `validateNarrativeBatch` (`@jgengine/core/game/narrativeAuthoring`) keep reviewed revisions separate from drafts and locate explicit character/fact/chronology and dialogue graph issues within caller batch budgets.
- `jgengine recipe narrative-authoring` composes detached authoring snapshots, bounded repair attempts and caller-injected generation plus explicit semantic review; it never selects a model or changes canon.
- Serializable physical material assets, texture metadata and exact named mesh/slot assignments preserve imported materials and maps unless explicitly overridden.
- Shared physical surface rendering, fuzzy sheen, directional woven appearance, hair-card ribbons and coverage/backscatter approximations; geometry and simulation remain separate.
- Materials workspace edits six family-specific groups and advanced maps, previews neutral or document lighting, and shares editor undo, save and typed RPC with runtime.
- Native glTF material inventory and export diagnostics preserve packed channels, extensions and attribution; resource accounting and appearance adapters consume wetness/exposure without changing global lighting.
- Typed slot-inventory RPC reports actual loaded model prerequisites; opt-in projected-overdraw measurement preserves native coverage and restores rendering resources.
- Portable `jgengine drive --click-at x,y` sends ordered native left clicks to canvas targets in CSS viewport pixels, with finite nonnegative coordinates checked against device or custom viewport bounds before browser launch.
- Typed game definitions can inject context-aware projectile sweeps, acceleration and bounded target sources. Each world supplies its own context; custom acceleration overrides the authored sampler, while omitted providers retain existing defaults and engine-owned time.
- Pursuit can use a live-world `defineGame({ pursuit: { eligible } })` callback to authorize nearest, retained, explicit and forced targets without a game-local brain loop. Cold saves retain progress and receive the loading world's injected policy; the existing player-role default and effect acceptance checks remain unchanged.
- Core ability kits, item-instance registries, event meters, accumulator meters, and regen shields expose detached `state()` and `restore(state)` contracts for saves and deterministic continuation. Ability HUD views remain compatible; saves preserve retuning, independent cooldowns, allocation sequence, meter latches, and shield grace timing.
- `EventMeter.tick(dt, decayEnabled)` lets caller-owned visibility policy pause decay and its delay; existing one-argument ticks behave as before.
- `registerFirstPersonMuzzle(camera, reader)` supplies a custom rig's live presentation muzzle with camera isolation, bounded reader work, and owner-scoped cleanup.
- `Magazine.retune` changes capacity and reload duration while preserving ammo, shared reserve storage, and elapsed reload time; capacity overflow defaults to atomic rejection with explicit discard/return policies.
- `ProjectileSystemDeps.rng` supplies deterministic per-pellet spread sampling; `maxPellets` bounds ray work (64 by default, configurable up to 256).
- `planSolidRoute` (`@jgengine/core/nav/solidRoute`) plans bounded local routes over indexed movement collision boxes, with exact endpoints, caller-owned terrain policy and explicit no-path/budget results.
- `findPathResult` adds explicit grid search-budget results; grid edges and smoothing accept a caller traversal policy. Existing `findPath` callers retain their return contract.
- `useModelInstance` (`@jgengine/shell/render/useModelInstance`) owns shared loading, isolated clone cleanup, bind-pose placement and animation for custom R3F renderers; `EntityModel` consumes the same hook.
- Switch existing camera rigs at runtime with a serializable data overlay, and apply vehicle board/leave patches through shared helpers with explicit rider identity and caller camera policy. Seat occupancy exposes snapshot/restore/reset and an unseated-safe driven-vehicle query.

### Changed

- Root-motion graph states now extract travel and render horizontally in place. The renderer no longer writes entity poses outside the movement/collision authority. Hosts that relied on that behavior must advance the headless graph, transform its `rootDelta` with `takeRootMotion`, and resolve requested movement through their controller. Graph previews can pass `output.rootMotion` to `createGraphPose.apply` for the same policy. This does not provide automatic collision-aware root-motion locomotion or retargeting.
- Animation graph root-motion flags include influencing outgoing states. Root deltas still sample current-state travel; blended collision-authoritative root travel remains unsupported by the automatic renderer.
- `GrassField` partitions its existing seeded tuft stream into at most 64 shared-template draws. Each render camera skips offscreen or fully distance-faded chunks while preserving the density/budget prefix, transforms, material and shadow policy. Chunk geometries are disposed on replacement/unmount.
- GameContext lazily persists pursuit home, targets, cooldown, scheduling, threat and lifecycle state in `pursuitBehaviors`, including cold save/reload and AOI replication. Legacy snapshots without this module remain accepted.
- Upgrade reports include Rapier and Navbake, reject corrupt installed metadata, and distinguish missing migration notes or an unverified notes target from an up-to-date published SDK.
- Runtime entity radius/arc queries update individual grid entries after pose and membership writes instead of rebuilding or enumerating the whole population on every query. Cold invalidation and large membership bursts rebuild lazily; save/replication hydration still derives the index from authoritative entities. Immediate subscriber queries observe committed membership and poses.
- Asset authoring guidance keeps palette selection and visual identity game-owned while reusing catalog, import, validation, and rendering tools.
- Quest journal kill and collection credit uses active objective indexes instead of scanning the quest catalog. Party credit still reaches nearby members when the killer has no quest; replacement definitions and restored saves refresh indexes. Re-register after editing objective targets, items, or party sharing. Quests activated during credit receive subsequent events.
- Editor RPC (`decodeEditorBridgeRequest`, `@jgengine/editor`) now rejects any top-level request field the method does not declare with `unknown field "<name>" for method "<verb>"` instead of ignoring it and returning `ok: true`.

### Fixed

- World overlays use camera-to-anchor visibility through the shared object/wall/terrain query, hide depleted health and invisible entities, and bound nearby sampling and occlusion rays per refresh.
- Editor command patches no longer leave partial edits or multiple live publications when a later command fails. Stable-id upserts and already-current edits succeed without duplicate placements or new revisions.
- Rejected creator edits leave document history and live-sync publication unchanged; durable save failures remain visible and approved catalog placements avoid persisting raw asset URLs.
- Edit mode preserves the game's authored models and placement overlay alongside editor guides; save acknowledgements, dirty state and pending failures survive Play/Return.
- Live authored prop moves/removals update previews while unrelated edits preserve game-initialized prop state.
- Creator budgets include bounded collision child pools; play clock control preserves native canvas rendering without postprocessing.
- Player shell replacement, exit and failed initialization dispose the owned runtime context exactly once, including environment resources when game disposal throws.
- Ballistic sweeps now test complete segment chords, so thin cover between samples blocks projectiles. Translating sphere/box sweeps handle targets crossing a projectile path between endpoint poses; authored cover uses centerline projectiles, with radius-aware collision available through an injected sweep.
- Headless, hosted and shell authority integrate player movement with scaled game time and retain queued motion while paused or at zero timescale. Fixed owners opt into full authoritative durations for normal, voxel and free-flight movement; standalone movement retains its historical frame clamp. Explicit hosted client prediction retains its input-step duration.

- Fixed-step movement now uses scaled game seconds without the standalone browser-frame clamp; pause keeps queued motion and leaves physics unchanged.
- Settlement events include matching shot IDs and detached actual effect results, so game-owned policies can settle once on the struck target.
- Authored primitive surfaces and static shape batches now decode sheen-colour and specular-colour maps as sRGB, using the shared texture-role semantics. Numeric maps remain linear and loader-cached textures remain untouched.
- Shared precipitation uniforms preserve per-layer wind when the provider has no wind configured.
- Fire grids restore fuel/heat state, retune live wind/rates and reuse a bounded heat workspace; fire animation supports simulation time.
- Lightning animation supports authoritative start/time values and seeded flicker for pause and replay.

- Authored runtime state shares the fixed game clock, preserves fire/flock poses on retunes, restores atomically, and publishes explicit appearance signals without renderer ownership.
- Games select force targets through `installEnvironmentMotion`; caps and masks are game policy, and paused simulation does not queue forces.
- Fire tools and opted-in rain cooling extinguish burning cells while preserving fuel; authored fire bindings stop spawning when the area stops burning.
- Typed `get_simulation` / `set_simulation` authoring persists weather, schedules, wind zones, emitters, fields, fire areas and habitats through ordinary undo, save and import/export.
- Reject numerically unsafe force magnitudes and wind coupling before authority or caller buffers mutate.
- Forward game-selected visible path kinds so flock/patrol guide routes need not draw as ground roads.
- Changelog checks validate every existing release fragment before missing-base, skip-marker, or source-free exemptions. Malformed notes now fail CI even when they predate the checked diff; pure refactors still need no new note.
- Incomplete saved locomotion mappings no longer create an animation graph with undefined clip names. A named single clip remains active while idle is unconfigured; otherwise the rig retains its bind pose. Missing walk mappings hold the explicitly named idle until repaired. Diagnostics report incomplete roles and validate the effective playback mode, including explicit graph precedence.
- Explicit empty locomotion mappings survive playback edits and clearing the last role, so incomplete characters do not silently switch to the model's first clip. Choosing Single clip still replaces the mapping.
- Rendered animation variants use independent deterministic streams per model instead of advancing gameplay RNG. Mounting, hiding or culling animated models cannot change gameplay random outcomes. Each remount restarts its visual stream; callers whose clip selection affects gameplay should own a headless animation runtime, injected RNG and saved state.
- Preserve held-pose playback settings and all one-shot variants when editing an authored character's animation. Graph previews include variant clip durations and retain the shell's in-place root-motion policy.
- Equivalent freshly created character animation configurations retain mixer playback, pending triggers and per-instance visual variant state across React rerenders. This fixes store-driven custom characters such as Deepward’s imported Fitter restarting their gait on every update. Actual clip, held-frame, mapping or graph edits still reconfigure and clean up the owned mixer; authored one-shot order is preserved.
- Generated locomotion graphs give death priority and retain its held pose when later hit or attack events arrive. One-shot clip arrays survive graph conversion and JSON parsing; seeded runtime choices are recorded in snapshots.
- Animation layers keep independent mixer actions when they use the same imported clip. Crossfades keep the outgoing animation advancing at its own rate and loop policy. Full-cycle clip events are retained, multiple missed cycles coalesce, and muted contributions do not emit events.
- Model animation diagnostics identify missing clips, empty layer masks and unsupported root-track assumptions. Invalid explicit single clips retain the bind pose instead of looping the rig's first clip.
- The first locomotion edit after single-clip playback seeds the imported rig's semantic roles while preserving authored playback and variants. Editor previews and runtime agree on incomplete idle/walk mappings, with repair hints.
- PhysicsWorldBackend capsule casts allow separating or tangent motion from machine-scale starting contact, so a valid character proportion can jump and move on its first authored-floor frame. Entering contact, real overlap, walls and ceilings still block movement; the numerical tolerance scales with the struck face's world coordinate.
- Headroom overlap checks use the same numerical contact tolerance, allowing a restored crouched character at exact floor height to stand and jump immediately while retaining real penetration and low-ceiling blocking.
- Foot IK rejects missing, ambiguous, overlapping or disconnected leg chains with named diagnostics instead of deforming unrelated rig branches. Automatic pelvis correction now uses the common ancestor of resolved legs; explicit pelvis selection is preserved.
- Disabling, replacing or unmounting foot IK restores its previous joint corrections without overwriting newer animation poses, and replacement resets pelvis smoothing.
- Shared heightfield player movement stops jumps at blocking ceiling undersides and lands on crossed object tops, including thin bounds and full authoritative simulation steps. Blocked curb and terrain rises retain valid headroom and recheck alternate-axis wall collision instead of penetrating a roof and ejecting the player sideways.
- Heightfield descent and landing recovery now follow the accepted terrain/object support rather than prematurely landing at the previous support height. Exact-clearance contacts remain walkable at translated coordinates; explicit height and `beforeCommit` policies retain their authority.
- Resolve imported attachment slots by a unique original authored name when GLTFLoader sanitizes the runtime name, including `handslot.r` on the supported Knight and Rogue Hooded rigs. Exact runtime names retain priority; missing or ambiguous references report repair guidance instead of attaching to an arbitrary node.
- Explicit foot-IK joint, pelvis and look-at names use the same original-name fallback. Knight and Rogue authored leg chains retain the same walking corrections as their runtime-name configurations; ambiguous, disconnected and overlapping chains still warn and are skipped.
- Capsule movement uses the same resolved timestep as its shared jump integrator: standalone stalled frames retain the movement clamp, while authoritative game-time steps integrate in full. Game position callbacks still receive their original frame timestep.

- Supported crouch input now overrides sprint and suppresses jumping. Capsule standing waits for headroom; `CharacterController.setCrouch` preserves standing and jumping on the same frame.
- Physics-backed capsules share heightfield jump buffering, coyote grace, release shaping, apex/fall gravity and landing recovery. Legacy held-jump saves retain their latch.
- Resolved movement feel and gravity/jump tuning follow live declaration getters without resetting movement state.
- Automatic stair stepping requires grounded motion. Near-contact ground casts maintain the configured capsule skin gap.
- Rapier ramps preserve collider surface normals without stair forgiveness. Character casts exclude the character before choosing a blocker, so its own collider cannot hide a wall.
- Authored jump graphs can follow physical takeoff, apex, landing and buffered re-jumps rather than playing ground locomotion throughout a jump. Clip choices and transition policies remain game configuration; no rig-independent jump mapping is inferred.
- Short characters’ default crouch height fits their capsule diameter. Retuning proportions recomputes omitted defaults while preserving explicit crouch height, and live declared capsule field changes retune the shared motor before its next move.
- Editor clip and graph previews use the shared model-instance placement and cleanup lifecycle, preserving imported character root transforms and releasing instance bone textures without touching cached rigs.
- Preserve horizontal in-place playback while an outgoing root-motion clip still influences a crossfade, including interrupted fades and restored graph snapshots. Previously a dodge fading into idle could shift the visible rig away from its collision-authoritative entity.
- Forward authored-object kind exclusions through `defineGame` scene placement, allowing a game-owned player spawn to expose its catalog animation clips without also placing a static actor. Ordinary props and the default mob/boss exclusions retain their existing behavior.
- Input-owned chase cameras preserve their first acquired target's authored heading instead of starting at zero. The existing `camera.initialYaw` can override that one-time seed; later body turns remain independent of walking input.
- Shell settings clear held keyboard, pointer and analog input through close, context replacement and unmount, and preserve other owners' suspension.
- Shop stock preserves live entries when restoring a save with invalid entries or duplicate ids.
- Colour-grade LUTs survive graphics-quality and post-processing graph rebuilds. Disposing a grade pass releases its loaded LUT and any texture that finishes loading after disposal.
- Shared game presentations report render failures with an explicit display retry while retaining the live game context and realm connection.
- Terrain material retry clears only pending exact texture groups owned by the failed presentation, including failures before Suspense commits, while preserving successful caches.
- Player-facing recovery explains the failure plainly, keeps technical details collapsed, and restores game focus after a committed retry. A recovered draw retires only its handled presentation diagnostic.
- Keyboard, touch release, control suspension and shell unmount publish input outside the render loop. Discrete intent supersedes older pending input acknowledgements without replaying held controls.
- Invalid inventory slot indices and non-finite split amounts return a rejection instead of throwing or corrupting saved item counts.
- HUD-only shells mount the shared settings store, menu, audio volume bridge, and input rebinding runtime.
- Shell keyboard handlers preserve native HUD focus and button activation while retaining active gameplay bindings.
- Settings option buttons expose their selected state and group label to assistive technology.
- Preserve configured settings through the public gameKit definition into HUD and 3D shells, including custom actions, rows, menu layout, quick controls and explicit disabling. Saved player values continue to override configured defaults.
- Terrain blend authoring adds new layers and paints in one undoable edit; failed brushes preserve the document and redo history. Layer reorder/additions preserve existing blends by id.
- Terrain RPC rejects invalid brush/dimension/ramp input and oversized allocations with actionable diagnostics.
- Viewport placement honors grid snap, paths select along their segments, and multi-selection gizmos translate the whole group consistently.
- Oversized terrain brushes bound work to the actual grid; native standalone editor captures now arm the existing readiness handshake and report load failures.
- Terrain-only authored scenes now frame their footprint and relief without a dummy marker.
- Terrain mesh changes invalidate the on-demand viewport so brush/undo/RPC edits appear without another input.
- Grid placement now resamples the shared ground field after snapping XZ, preventing floating or buried markers, zones and path points on slopes; invalid heights leave authoring unchanged.
- Editor live-sync clients recover from evicted document history with a revision-preserving snapshot; reverse-channel RPCs replace stale runtime entities after evicted removals.
- Creator saves reject exhausted revisions before writing, preserving the readable last valid scene and original durable catalog.
- Catalog-backed editor markers use wireframe spheres and selection halos so their glyphs no longer fill over placed characters. Picking geometry, emphasis and logical marker glyphs stay intact, including legacy catalog bindings.
- Report existing editor scene/catalog module import errors before opening an editing session or saving. Only an absent optional module resolves to an empty document or catalog.
- Hosted HUD controls, registered target commands and per-actor input queues route to the authoritative host without mutating replicas before join.
- Hosted clock snapshots reach client clock hooks; prediction never advances authoritative gameplay time.
- Remote player and possessed-pawn deaths credit the resolved killer for XP commands, quests and loot.
- Hosted saves retain economy, clock, pose, progression, simulation and system save modules; client replication stays private. Legacy partial saves retain initialized missing modules.
- Hosted ticks advance game time. WS commands serialize per world, roll back registered state on rejection/failure, and acknowledge after persistence. Client operation ids avoid collisions between sessions.
- Async hosted saves and Node `flush()` / `close()` await storage. Convex queries project client state instead of exposing authoritative saves.
- Model instances retain skeleton and material sharing within each rig, reducing duplicate bone textures and skeleton updates without sharing poses between characters. Entity models, attachments and scatter release their owned resources.
- Model animation releases mixer bindings on replacement/unmount and skips paused or completed clip updates. Configuration changes refresh demand-rendered poses.
- Sprite atlas frames change UVs without repeatedly uploading the image.
- Cascaded shadows release lights, targets, GPU shadow maps and disposed model shader bindings. Streamed meshes retain authored shader hooks without a full-scene material scan each frame.
- Composed model assets start loading together through the existing cache. Material maps apply before their first GPU upload rather than after the parent model draws.
- Pointer hits on instanced meshes transform their surface normals through the instance matrix.
- Cancel pending audio before graph creation and release sample/synth graphs on completion, stealing, stop, failed load, graph construction/start failure, and teardown. Explicit retained-loop start requests can retry stolen/stopped voices. Preserve authored spatial gain without double attenuation and keep nonpositional cues flat.
- Generated item allocation checks attached storage for id collisions and rejects unsafe allocator sequences or exhausted numeric ids.
- Standalone `jgengine shoot` / `drive` wait for a declared capture-readiness handshake instead of accepting a canvas while the host is still loading. Legacy pages fail readiness when their canvas stays undersized or has an empty backing store.
- `jgengine doctor` resolves default and named Bun workspace catalogs before checking SDK version alignment, reports missing or malformed catalog entries, and recognizes hoisted SDK installs.
- `jgengine upgrade` reads hoisted installed packages, uses resolved catalog pins when packages are not installed, and identifies the workspace catalog that owns an upgrade. The command remains a read-only migration report.
- Model placement retains imported root transforms and normalizes bind bounds in the same frame used for ground contact and collision triangles.
- Shell culling retains independent records for always-visible rows, refreshes bounds after visual-scale changes, and prunes removed rows from shell and core tracking.
- Canvas mouse bindings now deliver brief presses, native mouse chords and held actions through the shared input tracker, release owned buttons on focus loss or suspension, and prevent a claimed mouse gesture from also firing the legacy hotbar action.
- Press edges survive release before a simulation sample and urgent neutral input arriving before a delayed press; each authoritative host or deterministic replay consumes an edge once.
- Owner resets always supersede delayed input, including an identical neutral release still carrying an unacknowledged press.
- Stateless hosted reconstruction retains input admission and pending presses in host-only save state, so replaying stored held intent never fires a consumed press again.
- Hosted input now compacts elapsed records in memory and retains departed admission marks within a configurable user/TTL window; `inputRetention` sets the policy and `inputStats()` reports retained storage. Explicit future replay edges and standalone input logs remain intact.
- Shared card and inventory drags resolve touch targets through pointer capture, ignore secondary pointers, finish releases outside, and retire capture/listeners on cancellation or unmount while preserving keyboard move, split, and card rotation.
- Keep inventory grid, toolbar, and custom HUD keyboard interactions from also triggering gameplay actions in the shell.
- `FullscreenMap` owns a single pointer, discards canceled route drafts, and retires gestures on close, tool changes and unmount. Native mouse chords and outside release preserve ownership; map clicks place waypoints while custom overlay controls keep their input.
- `useVoice` reads open-mic transmission from its controller on mount and gates newly captured audio tracks before publication using the current mode and mute state. Explicit voice policy remains consistent across transport readiness and updates with stable route arrays.
- `PushToTalkButton` owns one primary pointer or Enter/Space activation and releases its hold on cancellation, capture loss, interruption and unmount. Secondary touches, auxiliary mouse buttons and key repeats preserve the owner's session; caller-owned toggle and open-mic policy remain intact.
- Retire window title-bar drag listeners on blur, capture loss, and unmount, and keep secondary pointers from moving or ending another pointer's window gesture.
- Pinned generated choices now obey their step's compatibility constraints and backtrack earlier unpinned choices when needed.
- Material catalog URLs follow pinned source map availability, including absent AO in `ambientcg-metalplates001` and absent roughness in `ambientcg-gravel001`/`ambientcg-concrete001`. Pull rejects archives missing declared maps; terrain skips absent AO/roughness sampling and keeps authored scalar roughness.
- Strict key-value failures preserve the prior cell and rejected payload writes preserve the stored checkpoint. Save slot metadata failures now report the existing error status and callback, including asynchronous updates after load, instead of reporting saved. Whole-world load/hasSave/slot switches no longer treat a denied read as a successful cached load.
- Portable `jgengine shoot` and `drive` enable touch and coarse-pointer media queries for mobile device profiles and clear touch emulation for desktop.
- Portable `drive --click` scrolls text targets into view and requires an unobstructed, enabled point within the viewport and clipping ancestors. Text matching remains case-insensitive.
- Portable CDP requests time out instead of hanging; `--timeout` bounds screenshot capture as well as frame readiness, and closing a session rejects pending requests.

- Native click targets also resolve accessible labels and allow three stable samples on slower rendered pages within a bounded 15-second settling budget.
- Built-in quest rewards stage the complete item batch against inventory capacity and kind rules before committing, preventing duplicated earlier rewards on retry. Reentrant turn-in calls reject while rewards are applying.
- Spatial pair queries include qualifying pairs across every cell covered by the requested distance, including distances larger than the grid cell size.
- Standalone GameHost now exposes declared capture.probe to the portable playtest driver, with boot-scoped cleanup that cannot read a retired context or remove a replacement host hook.
- Streaming bounds outstanding loads across frames, cancellation, clear, and same-id retries. Clear releases resident resources, and stale completions cannot overwrite new demand.
- Host subscription refreshes now serialize reads, coalesce burst notifications, discard obsolete lifecycle completions, and advance world cursors only after sending a frame. Slow initial reads no longer block unsubscribe or commands.
- Flight gates again read the accepted same-frame stance once. Rejected height edits and thrown gates restore the driven pawn’s exact pose and retain queued motion and existing motor state (#1962).
- Active interaction prompt edits and replacements now notify subscribers when their content changes, keeping stationary HUD labels and command data current. Equivalent edits, inactive prompt mutations and unchanged frame selections remain silent; selection still recalculates on `resolve`.
- Scene entity `moveToward` and `moveTowardCommit` now stop at indexed world solids, matching player and walker collision without game-local blockers. Bare `SolidObstacleSource` adapters can inject the same bounded world-solid query.
- `defineGame` preserves legacy environment sky settings when editor sky fields override part of the sky, including clouds, haze and partial sun/fog values. Explicit game `backdrop.sky` remains authoritative.
- Editor environment commands store decoded copies of nested fields, preventing later input mutations from changing authored state.
- The monorepo editor CLI validates the full scene before saving and preserves existing file bytes when validation fails.
- Node file persistence now rejects chunk-directory read errors other than `ENOENT`, preventing host admission with an empty world when an existing checkpoint cannot be read. A missing directory still means no saved chunks; the same host can retry after backend repair.
- Agent governance, intake, improvement workflow and new-game briefings require shared SDK adoption or upstream fixes, tracked consumer cleanup and centralized workspace versions while preserving each game's unique content and presentation.
- Region split now refuses an output identity retained by the opposite partition, keeping successful split results eligible for the existing merge identity gate. Reusing an identity from the same partition remains valid.
- Distance thinning now collapses blade width and wind displacement along with height. Fully faded blades vanish instead of leaving flat wind-displaced triangles; partially fading blades narrow, while `keep = 1` grass retains its existing appearance. `distanceFade: false` still disables distance rejection.
- Reject invalid EntityModel assignments before texture allocation and release failed material replacements exactly once without changing imported resources.
- Release unreturned material clones and promotions when imported shader callbacks fail during asset or rim construction, preserving the original error and borrowed resource ownership.
- Attempt every owned replacement's cleanup during assignment rollback even when an earlier disposal callback throws.
- Custom first-person muzzle readers can release registrations during a read without invalidating iteration or revisiting disposed owners; each priority uses a snapshot of its newest 64 candidates.
- Node world-server shutdown now fences socket intake and ticks, drains accepted router work, and awaits the stopped host's final save. Repeated close calls share completion and persistence failure; interval-only `stop()` remains restartable.
- Failed WebRTC host offer acceptance now closes and releases its allocated peer connection immediately, while preserving existing peers and the original negotiation error. Hosts can accept a fresh offer after failure without retaining the failed connection until teardown.
- Failed hosted-world join saves and spectator promotions no longer consume player capacity. Initially resident host members retain their reserved seats; failed callers retain dirty player state for retry and must satisfy capacity again.
- Closing BroadcastChannel peer signaling now rejects pending offers instead of leaving guest bootstrap unresolved, allowing the shell to release its guest transport. Closed signaling refuses new work and ignores late answer completions; listener replacement and successful exchanges remain supported.
- Playable peer sessions retry transient world loads, reject unadmitted snapshot reads, and drain accepted router work before the final close save. Hosted-world admission becomes visible after its join save succeeds.
- Same-ID behavior updates and warm hydration reconcile changed/removed descriptors before subscribers without scanning the world; equal-data updates retain progress and lifecycle. EntityStore exposes `subscribeBehaviors`, and replaced running decision graphs release existing action ownership through an explicit exact-once `abort` lifecycle method.
- Warm pursuit-module hydration reuses the keyed behavior cache; cold initialization keeps its initial entity discovery.
- Custom model placement retuning uses the configured bind frame after root animation. Generated asset markers use shared static-shape batching while preserving material defaults and authored transforms; Field Station's bookcase requires only the published package upgrade.
- `jgengine doctor` now detects private SDK copies reached through linked consumers and SDK dependencies/peers, including distinct copies with identical versions; it reports resolver paths and a root catalog/reinstall action without changing files.
- Shaped grids reject nonfinite/fractional dimensions, origins, and footprint cells instead of admitting non-discrete placements.
- Shared settings categories remain reachable on narrow screens and short sidebars; dialogs share focus trapping, Escape policy, and return focus.
- The floating joystick capture zone yields to interactive HUD panels while touch action buttons retain their layer.
- Replacing a focused action preserves a keyboard entry point into the action bar.
- `useDialogBehavior` keeps focus trapping and the original opener when an open dialog replaces its root; portalled children retain focus and return to the replacement parent.
- WebSocket router joins that finish after disconnect now release their memberships through the reconnect grace period, reclaiming capacity without removing a live reconnect waiting on admission.
- Scene raycasts now include indexed world solids as physical, non-damage wall hits. Generated and authored cover blocks the same LOS queries as placed objects; existing wall filters, hit acceptance, and projectile obstacle policy apply.
- Keep compiled hair-card backlight uniforms owned by each cloned material so local environment exposure and baseline resets affect only that instance while preserving the original material and shader composition.
- Compose hair-card backlighting with Three.js physical lighting across the supported peer versions, including r186's updated diffuse energy compensation, while retaining native lighting and independently owned clone uniforms.
- Shared grade constraints resolve travel before foot height, avoiding the rejected-neighbor height retained by game-level position interceptors. The restriction also applies during jumps while preserving the vertical arc.
- Explicit `onDeath` drop rules with `when.reason: "any"` can spawn world items after non-player deaths. Ordinary tables remain suppressed without a player killer, and bag and currency grants still require one. Hosted players and uniquely owned pawns retain their own loot recipient rather than the host's inventory.
- `createAuctionBook` rejects duplicate explicit IDs with `duplicate-id` and skips occupied generated IDs, preserving active auctions and escrow.
- `AuctionBook.claimItem` rejects nonpositive and noninteger counts without changing collection items.
- Listing books preserve occupied listing IDs: generated IDs skip collisions, and explicit duplicates return `duplicate-id` without replacing retained goods.
- Invalid negative, fractional and non-finite item claims leave collection boxes unchanged; zero-count no-ops and valid integer claims keep their behavior.
- Resource ledgers preserve positive finite reserves and remainders below `1e-9` through settlement and JSON reload.
- `createTechTree` consistently uses the last definition for duplicate IDs, preventing prerequisite bypass and obsolete recipe or category exposure.
- `createUnlockPoints` preserves `__proto__` unlock IDs and paid costs across JSON saves and reloads. Restoration discards invalid saved costs instead of granting free unlocks, while retaining valid fractional and zero-cost unlocks.
- NaN item wear throws before tracker mutation; nonfinite repair targets return `null` instead of producing repair quotes.
- `createModularItem` rejects initial parts with unknown slots, incompatible categories, or duplicate occupancy using the same validation as `install`.
- Model material promotions and shared texture views have explicit ownership and cleanup; unused material-library maps are not loaded.
- Editor module-load coverage uses a present, nonempty authoring fixture instead of relying on SDK/example imports from an isolated Games checkout.
- Grid navigation recognizes a reached goal at the exact `maxNodes` expansion budget instead of reporting exhaustion.
- Created standalone projects pin the exact scaffolding CLI locally, independently of SDK dependency versions.
- Skill installs check the entire selected set before overwriting, preserve newer and unversioned differing copies unless `--force` is passed, and record portable CLI/SDK version metadata.
- Doctor reports when the running CLI targets an older SDK minor than the project's installed packages.
- Standalone 2D and `--no-editor` games omit the unused editor dependency, avoiding its unnecessary package and misleading styling finding.
- Projectile restore now rejects missing radius-aware collision providers, oversized pellet/sample counts and inconsistent flight counts before replacing active or retained owner state. Compatible cold restores preserve pending impacts, death rewards and every settlement within retention budgets.
- A supplied pursuit eligibility callback accepts targets only when it returns exactly `true`; missing returns, nulls, objects and Promises deny attacks. An absent callback retains the documented player-role default.
- Generated building boxes and kit meshes use spatially bounded instance batches, so distant parts no longer keep a whole settlement batch in view. The shell applies the existing resolved graphics draw distance and visibility overrides while retaining material groups, fitted geometry bounds and nearby offscreen shadow casters.
- AO and depth-of-field scene prepasses reuse the beauty pass's shadow maps instead of redrawing shadow casters; failed prepasses restore shadow update settings and overlay visibility.
- Depth-of-field resizing and cleanup use the pass lifecycle, fixing a crash on current Three releases and duplicate resource disposal.
- First-person tracers can start at a custom world-overlay weapon's real muzzle even when the stock viewmodel is disabled. Stock muzzle tracking is camera-scoped; authoritative projectile origins remain unchanged.
- Scene-backed projectile spread now samples independent cone rays, each respecting its own nearest receiver, hitboxes, line of sight, and cover. Zero-spread pellets hit the nearest receiver instead of cycling through center-ray targets; center-ray prediction and ballistic paths remain unchanged.
- Projectile firing copies angular aim, explicit ray vectors and selected origin-policy vectors before injected RNG runs; caller reuse cannot redirect delayed settlement.
- Restoring an earlier living entity into the same context clears its stale death marker, allowing future lethal effects to despawn it and resolve rewards once. Transient depleted victims captured during `entity.died` retain the duplicate-death guard.
- Composed systems retain each world's schedules and listeners when a definition hosts multiple contexts. Disposal drains all system and classic cleanup once despite errors or reentrancy, then rethrows the first error. Failed installation retires entered systems and acquired listeners while preserving the startup error; schedule/listener reinitialization starts a fresh lifecycle.
- Validate untrusted custom asset specs and preserve independent game-owned metadata; detect GLB/glTF, KTX2, MP3, WOFF2 and HDR formats from bytes with malformed/truncated header checks.
- Preserve hidden riders during entity hydration and provide a validated, copied velocity setter so dismounts clear stale movement without rewriting controller algorithms.
- Chase cameras can set `headingSource: "input"` to keep held strafe and backpedal movement from circling as the body turns. Vehicle body follow remains the default; runtime chase tuning can switch the source, and seat views retain body heading.
- Vehicle obstacle recovery no longer launches kinematic or force-model cars or reports recovery displacement as crash speed. Clamp results may include permitted `motion` displacement separately from their recovered endpoint; plain tuple callbacks keep their behavior.
- Atomic vitals bars retain palette fills under `hudThemeVars`, clear inherited skins in nested themes, and honor explicit `fill` colors over bar skins.
- Skinned model height, grounding, and fitted collision triangles now use the imported skin-applied bind pose. Creature rigs with nonidentity inverse bind transforms no longer render several times larger than their configured height.
- Retire microphone grants arriving after voice-hook unmount, channel/transport replacement, or a newer permission request. Stop replaced capture tracks, preserve accepted capture on transport changes or failed retries, reject ended audio grants and callbacks retained from retired channels, and handle publication failures without unhandled rejections or stale microphone updates.
- World bars and nameplates omit the local player's possessed body, including retained health anchors. Shared overlays mounted through a game's `WorldOverlay` now inherit the same renderer visibility context as the built-in overlays while keeping caller markup and explicit through-wall reveal policy.

## 0.19.0

### Migrate

- **Bump lockstep SDK packages to `^0.19.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.16.0`; `@jgengine/github` is `0.6.0`.
- `environment({ structures: building(...) })` buildings and `city` volume buildings now block the player, walking NPCs, `syncWorldColliders` physics backends and `populateNavGridFromSolids` through the new `ctx.world.solids`. Before, they were render-only. If a game re-authored generated buildings as placed scene objects to make them solid, drop those objects. To keep a set passable, pass `building({ solid: false })` or turn off `solid buildings` on the city volume. A mock `GameContext` needs `world.solids` (`createWorldSolids()` from `@jgengine/core/world/worldSolids`).
- `createAircraftDynamics` and `AircraftTuning` (`physics/flightDynamics`) are deprecated. They command rotation from rates with pitch clamped, so aircraft can't loop, and mass and stall are fixed. Move to `createRigidAircraft` (`@jgengine/core/physics/aircraftDynamics`):
  - `mass` → `massKg`, plus an `inertia` tensor (roughly `m·(span/4)²` for roll and `m·(length/4)²` for pitch and yaw).
  - `fixedWing`: `lift`/`stallSpeed` → left and right wing `surfaces` (area, `liftSlope`, `stallAngle`, trim `incidence`), a tailplane and a fin; `controls.pitch/roll/yaw` → `control` authority on those surfaces plus `controls.<channel>.rate`; `maxThrust` → `engine.maxThrust`; `drag`/`maxSpeed` → `dragArea` and surface `cd0` (top speed is where thrust meets drag); `sideDrag` → the fin.
  - `rotorcraft`: `hoverThrust` → `rotor.maxThrust`, `rotorResponse` → `rotor.spoolRate`, `groundEffectHeight` → `rotor.radius`; add `rotor.torque` and a `rotor.tail`, and pass `input.collective`.
  - `vtol` has no one-block equivalent: combine `rotor`, `surfaces` and `engine`, or keep the deprecated model for now.
  - `controls.stability` → `assists.autoLevel` or `assists.sas`; `afterburner` → `modifiers.thrustScale`; `airbrake` → `modifiers.dragScale`; `groundClearance` → `gear.height`.
  - Euler `rotation` → step `heading`/`pitch`/`bank` and `orientation`; spawn with `aircraftAttitudeQuaternion(heading, pitch, bank)`.
  - Tune against `measureFlight` with the `jgengine-world` recipe `flight-feel.md`.
- `KinematicVehicleTuning.chassis` and `.steering` are deprecated. Their heading is still commanded from steer, so a held steer at speed spins the car. For handling that matters, move to `createVehicleDynamics` (`@jgengine/core/physics/vehicleDynamics`). Map `massKg`/`engineForce`/`brakeForce`/`tireGrip`/`comHeight`/`trackWidth` to `massKg`, `powertrain: { kind: "direct", maxForce, maxPower }` or a gearbox, `brakeForce`, `front.peakGrip`, `comHeight` and `trackWidth`. Map `steering.wheelbase`/`maxAngle`/`highSpeedAngle`/`highSpeedAt` to the same `steering` fields plus a rack `rate`. Then tune against `measureHandling` with the `jgengine-world` recipe `vehicle-feel.md`. Plain `kinematicVehicle`, without those blocks, stays for karts, top-down and bumper cars.
- The default third-person orbit camera sits lower and closer (`initialDistance` 9 → 7, `initialHeight` 5.5 → 2.4), so the horizon sits near the top third and the sky is in frame. To keep the old framing, set `camera: { initialDistance: 9, initialHeight: 5.5 }` in `defineGame`.

### Added

- Split-screen viewports (#1686). `defineGame({ viewports })` draws one scissored viewport per joined local seat (`splitViewports`, `resolveViewports`, `viewportPixels` in `@jgengine/core/game/viewports`); extra seats follow their entity with a chase or top-down camera and walk relative to it. `ViewportHuds` (`@jgengine/shell/camera/Viewports`) renders a HUD root per seat. `bun run drive` gains `--param key=value`; `?gamepad=N` synthetic pads now hold Start. Dev demo: `couch`.
- `createPerception` takes `maxStimuli` (default 256; the oldest are dropped) and adds `forget(observerId, targetId?)`. Stimuli insert in time order instead of re-sorting on every push.
- Decision graphs (`@jgengine/core/ai/decisionGraph`) gain `invert`, `cooldown`, `wait` and `random` nodes and `memory` selectors/sequences that resume at their running child. `createDecisionGraphRuntime(graph, actions, { onAbort, rng })` calls `onAbort` when a branch pre-empts a running action, and `random` draws from the injected `rng` only. `running()` names the running action; snapshots now carry timers, memory and cooldowns (older `{ runningPath }` saves still restore).
- Decision graph behaviors keep a persistent per-entity blackboard: seed it with `blackboard`, write facts from perception or game systems through `behaviorControl(ctx).blackboard(id)`, and it round-trips through `serialize`/`restore`. `thinkInterval` ticks the graph every N seconds, staggered across siblings, with the elapsed time as `dt`. `registerBehaviorActions(id, actions, { onAbort })`; `random` nodes draw from `ctx.rng`.
- Editor Animation workspace (#1683). The rail button opens the bottom dock's new **Graph** mode for the selected placement, showing layers, states and transitions.
  - A scrub slider replays the graph runtime; parameter sliders and trigger buttons record presses on the timeline.
  - The rig at the camera focus is posed from the runtime's output.
  - Crossfade edits and **Store graph** save the graph to `marker.meta.animation.graph` as undoable edits.
  - Supporting APIs: `parseAnimGraph` (`@jgengine/core/anim/animGraph`) validates stored graphs, `animGraphFromConfig` (`@jgengine/core/anim/locomotionGraph`) resolves the graph a config plays, and `createGraphPose` (`@jgengine/shell/render/useModelAnimation`) poses a rig from graph output.
- `npx jgengine recipe` adds `world-drops`, `click-target`, `ability-bar`, `hitscan-weapon`, `rolled-gear` and `enter-vehicle`, each type-checked against the SDK.
- `SkinnedInstances` (`@jgengine/shell/render/SkinnedInstances`) renders hundreds of animated copies of one rig, each with its own clip, time offset and speed, in one instanced draw per material. Clips are baked once to a bone-matrix texture and sampled on the GPU. Shadows follow the pose. The layout and frame lookup are pure: `planBoneTexture` and `sampleBoneTexture` (`@jgengine/core/anim/boneTexture`). Dev runner demo: `bun run drive skinned-crowd`, with `skinned-crowd-mixers` as the one-mixer-per-character baseline (#1683).
- Continuous haptics (#1769). `ctx.input.haptics(userId)` returns named rumble channels (`createHapticChannels`, `@jgengine/core/input/haptics`): `set` a level per tick, `pulse` a fading hit, mixed by priority so an impact ducks the engine hum. The shell drives each local seat's pad with refreshed short effects and resets it when silent; channels are local only. The `handling` demo feeds engine, road and impact channels.
- Local seats for couch co-op (#1686). `defineGame({ localPlayers: { maxSlots, claimPrimary? } })` and `localPlayers(ctx)` (`@jgengine/core/runtime/localPlayers`): pads hot-join their own seat on a button press, the shell spawns each through `loop.onNewPlayer(ctx, { userId, isNew: true })`, routes that pad to the seat's own `InputSnapshot` (`local(slotId).input`), and moves its entity with the walk controller. The seat table has `snapshot`/`restore`/`retune`. `?gamepad=N` injects N synthetic pads for headless checks.
- Action contexts rebind live input (#1769). `ActionContextStack` gains `subscribe`, `version`, `ids` and `activeAxes()`; a context may carry `axes` and serializable `shaping`. The shell re-binds keyboard, touch and gamepad when a context is pushed or popped, without remounting: keys held across the swap keep counting toward their new action, and context-only actions reach `ctx.input`. `ActionStateTracker` gains `rebind` and `actions`. The `handling` demo drives through a `driving` context (F parks it), with pad codes on every action. Recipe: `jgengine-world/recipes/controls.md`.
- `createNavMeshQuery` (`@jgengine/core/nav/navMesh`) caches a nav mesh's adjacency and a uniform polygon grid, then answers `findPath`, `findPolygon`, `closestPoint` and `raycast` without full-mesh scans. Paths use a binary-heap A* priced by `areaCosts` per `NavMeshData.areas` id (non-finite blocks an area), stop at `maxNodes` with a `partial` route, and are straightened by a portal funnel. `retune`/`snapshot`/`restore` cover the costs, so a door or hazard can close an area during play. `NavMeshLink` gains optional `start`/`end` landing points.
- `createWeaponFeedbackSignals` (`@jgengine/core/combat/weaponFeedback`) turns `WeaponHandling.fire()` shots, their recoil kick and impacts into one-tick pulses (`shot`, `kick`, `impact`) plus the frame's `recoil`, `cameraKick`, `spread` and `ads`, keyed for `createFeedbackMixer` routes to camera shake, audio and rumble. It has `snapshot`/`restore`/`reset`.
- `measureWeapon` now reports `spreadByShot` and `shotsToKill`, fires in a given `stance` (e.g. ADS), and computes time-to-kill at a `range`. Each projectile (`pellets` per shot) counts by the share of its spread cone the `targetRadius` covers, scaled by the game's `damageAt(range)` falloff. The recipe is `jgengine-combat` `weapon-feel.md`.
- `defineGame({ gamepad: { deadzone, curve, triggerDeadzone } })` sets pad feel for the shell's gamepad poll (#1769). Triggers now go through `triggerDeadzone` and the same curve as the sticks, and a trigger resting inside its deadzone no longer holds its action. `resolveGamepadFrame` takes a browser `Gamepad` directly and an optional reusable `out` frame, and the shell poll no longer allocates per frame. `gamepadFeelOptions` resolves the config with the old defaults (axial `0.12`/`0.95`, linear).
- `measureFlight` (`@jgengine/core/physics/handlingProbe`) flies a `createRigidAircraft` through deterministic autopilot scenarios. It reports roll rate (deg/s), sustained turn rate, stall speed, climb rate, time to 90% throttle response and hands-off hover drift, so a flight feel target can be a test.
- `RigidAircraftTuning.assists`: flight assists on the same actuators as the pilot, so full stick stays the pilot's.
  - `sas` per axis (`0..1`) damps rates and holds attitude or heading hands-off with a learned trim. On a helicopter it holds heading against rotor torque.
  - `autoLevel`, `maxAngleOfAttack` and `maxG` limiters, and `hoverHold` for helicopters.
  - A `policy(context, command)` callback has the last word.
  - The step adds `command` and `limited`. `RigidAircraftState` gains the hold, trim and drift integrals.
  - T toggles assists in `bun run drive flight` and `flight-heli`.
- `RigidAircraftTuning.motor` makes `createRigidAircraft` a rocket.
  - Thrust follows `thrustCurve` over burn time. `propellantKg` leaves at `massFlow`, so mass and inertia fall and acceleration rises through the burn. The nozzle `gimbal` steers on the pitch and yaw inputs.
  - To stage, `retune` to the next stage: a new `motor` loads its propellant.
  - The step reports `massKg` and `motor` telemetry. `RigidAircraftState` gains `burnTime` and `propellantKg`.
  - Dev runner demo: `bun run drive flight-rocket`.
- Jump shape on `movement.feel`: `jumpCutFactor` (releasing jump early cuts the rise, so a tap hops), `apexGravityScale`/`apexSpeed` (hang at the peak), `fallGravityScale` (drop faster than you rose) and `landingRecoveryMs`/`landingSpeedScale` (a short slowdown and jump lockout after landing). All default to no change. `measureMovement` adds `tapJumpHeight`. Dev runner demos `walk-floaty` and `walk-weighty` differ only in these fields (#1772).
- `RigidAircraftTuning.rotor` turns `createRigidAircraft` into a helicopter: `input.collective` sets thrust, throttle spools the rotor, cyclic tilts the disc, and rotor drag torque yaws the body unless the `tail` rotor (pedal) cancels it. Thrust adds translational lift, drops while climbing through the disc, and grows in ground effect. The step reports `rotor` speed, thrust, torque, ground effect and translational lift. Dev runner demo: `bun run drive flight-heli`.
- `snapshot()`/`restore(next)` on `createGlideModel`, `createLodScheduler`, `createSimulationCuller` and `createSpatialIndex`, and `restore(state)` on `createGrappleSwing`, so gliding, rope swings, LOD throttling and the spatial hash save, load and replay bit-exactly (#1775).
- `snapshot()`/`restore(next)` on `createThreatTable`, `createAssistNetwork`, `createReputationLedger`, `createConcealmentSensor`, `createFreezeMonitor` and `createRecordingBuffer`, so AI aggro, faction standing and sensor dwell save, load and replay bit-exactly (#1775).
- Engine audio depth (#1770):
  - `ctx.game.audio.setLoop` takes `lowpass` and `highpass` cutoffs in Hz, ramped like rate and gain.
  - `SoundDef.doppler` pitches a retained loop by listener and emitter velocity; pass the emitter's with `setLoop({ velocity })`. The shell tracks the listener's from the camera. `dopplerRate` (`@jgengine/core/audio/doppler`) is the pure formula.
  - `createEngineLayers` (`@jgengine/core/audio/engineLayers`): N rpm-keyed engine samples crossfaded at equal power, with on-load and off-load sets picked by smoothed `engineLoad`. `play(ctx.game.audio, ...)` drives one retained loop per layer. It has `snapshot`/`restore`/`retune`.
- `snapshot()`/`restore(next)` on `createFootprintGrid`, `createWallDrawTool` and `createTerraformBrush`, so build-grid claims, wall drafts and brush settings save, load and replay bit-exactly (#1775).
- `createInputBuffer` (`@jgengine/core/input/inputBuffer`): buffered presses, coyote time, hold duration and double tap, with `snapshot`/`restore`. The walk controller uses it for `movement.feel.jumpBufferMs` (a jump pressed just before landing fires on landing) and `movement.feel.coyoteMs` (a jump just after walking off a ledge still fires). Both default to 0, so walking is unchanged until a game sets them (#1686, #1772).
- `snapshot()`/`restore(next)` on `createLootRegistry`, `createToastQueue`, `createVfxInstanceStore`, `createRoundState` and `createLookChannel`, and `restore(state)` on `createDragCapture`, so loot tables, toasts, retained VFX, match rounds and look/drag input save, load and replay bit-exactly. `RoundSnapshot` gains `pendingWinner` (#1775).
- `measureMovement` (`@jgengine/core/movement/movementProbe`) reports a walking character's feel from the same `movement.feel` and `physics` fields a game sets: time to top speed, stop distance, turn-around time, jump height, apex and air time, and air-control reach. Deterministic, for tests (#1772).
- Chase camera depth (`camera.chase`, #1770):
  - `fov.response` eases the speed FOV, and speed now comes from the followed entity's sim `velocity` when it publishes one, so FOV no longer jitters when render and sim rates differ.
  - `distanceBySpeed`, `pitchFollow` (boom follows body pitch on slopes), `lookBackAction` (hold to look behind), and `collision` (the boom stops short of objects, terrain and walls via `ctx.scene.raycast`; on by default).
  - `ctx.camera.kickFov(degrees)` queues a decaying FOV punch, tuned by `chase.fovKick`.
  - `ChaseCameraTuning` accepts `view` and the new fields; `nextChaseView` (`@jgengine/core/runtime/cameraDirector`) cycles chase, hood and cockpit.
- `createRigidAircraft` (`@jgengine/core/physics/aircraftDynamics`): a quaternion rigid-body aircraft with an inertia tensor, lifting surfaces (area, lift slope, stall, post-stall lift, drag polar) placed on the body, rate-limited control surfaces, an engine with spool and optional gear. Loops, rolls and stalls come from the surfaces. Same contract as `vehicleDynamics`: fixed substeps, `snapshot`/`restore`/`retune`, per-tick modifiers, and telemetry for AoA, sideslip, g-load, airspeed and stall. Dev runner demo: `bun run drive flight`.
- Graphics settings a player can feel: an Ultra tier, a frame-rate limit (V-Sync or 30/60/120/144 cap), texture filtering (anisotropy, now applied to every loaded texture), and an FPS counter. `GraphicsProfile` gains `anisotropy`; a game that overrides tiers with `defineGame({ graphics })` can set it per tier.
- `postProcessing.stylize` (`outline`, `bands`, `pixelSize`) and two new looks, `look: "comic"` (ink outlines + cel bands, Borderlands-style) and `look: "retro"` (pixelated, posterized). Same models, different art style.
- `PhysicsBackend.applyForce(handle, force, point?)` and `applyTorque(handle, torque)` act over the next step, and `BodyDesc.linearDamping`/`angularDamping` set damping per body. Both are implemented on `PhysicsWorld` (translation only) and Rapier, and covered by the conformance suite. `createVehicleBackendLink` (`@jgengine/core/physics/vehicleBackendLink`) collides a vehicle sim with a backend world: a kinematic chassis shoves props, `clampMove` stops the car at walls and slides it along them, and `lastHit()` reports the impact.
- `VehicleAssistTuning.antiWheelie` and `.antiStoppie` (`0..1`) cap drive and braking below the pitch-over limit (`g·b/h` forward, `g·a/h` braking). With `suspension` they also back off as the front or rear goes light. They report through `tractionLimited` and `absActive`.
- Motorcycles on `createVehicleDynamics`: a `VehicleDynamicsTuning.lean` block (`maxLean`, `leanRate`, `countersteer`, `directSteerBelow`).
  - Steer asks for a lean, capped at what grip can hold. The bar steers the balanced turn that lean needs, with a rider trim for tire slip and a countersteer tip-in.
  - The bar steers directly at walking pace, and there is no lateral load transfer.
  - The step adds `lean` (balance lean, `bodyRoll = -lean`) and `wheelie`/`stoppie` for all vehicles.
  - `measureLean` (`physics/handlingProbe`) reports steady lean, time to lean and counter-lean.
  - Dev runner demo: `bun run drive handling-bike`.
- `measureCourse` (`@jgengine/core/physics/handlingProbe`): understeer gradient (deg/g), skidpad g at a set radius, and the fastest clean slalom, from simple deterministic drivers. `measureHandling` adds `handbrakeRecoverySeconds`.
- `createWeaponHandling` (`@jgengine/core/combat/weaponHandling`): weapon feel as serializable state.
  - Recoil: a per-shot `pattern` or flat `pitch`, a random cone from an injected `random`, a `cameraShare` split between view punch and aim, `recoverRate`/`recoverDelay`, and `adsScale`.
  - Spread: `base`, `perShot` bloom, `max`, recovery, and stance/ADS multipliers.
  - ADS time, plus `snapshot`/`restore`/`retune`/`reset`.
  - `measureWeapon` reports first and tenth shot spread, burst climb, reset time, ADS time and time-to-kill.
- `createWeaponPresentation` (`@jgengine/core/combat/weaponPresentation`): per-weapon viewmodel hip and ADS offsets, viewmodel FOV, ADS zoom, sway from look speed, bob from the movement probe, and recoil kick, from a `createWeaponHandling` frame. `camera.weapon` hands that frame to the rigs: first person poses its viewmodel and adds the recoil to the look, and `shoulder` takes its ADS blend and recoil from the same frame. Dev demos: `weapon-handling`, `weapon-handling-shoulder`.
- `createFeedbackMixer` (`@jgengine/core/vfx/feedbackMixer`): declares how sim telemetry drives presentation. Routes map a signal through a piecewise `curve` with `attack`/`release` smoothing onto a named target, targets combine by `sum` or `max` over a `base`, and threshold `events` with cooldowns fire one-shots. No allocation per update, with `snapshot`/`restore`/`retune`/`reset`. `sampleFeedbackCurve` exposes the curve.
- `snapshotPlayerMovement(ctx, userId)` / `restorePlayerMovement(ctx, userId, snapshot)` (`@jgengine/core/movement/playerMovement`): per-player walk state (heading, velocities, jump latch, capsule controller) as a plain JSON copy, so walking characters predict and roll back like vehicles. A snapshot restored before the capsule exists is applied when it is created.
- `PlayerMovementConfig.feel` (`MovementFeelConfig`): `groundAcceleration`, `airAcceleration`, `groundFriction` (response rates, 1/s), `runMultiplier` and `crouchMultiplier` for the built-in walk controller and the voxel controller. They were hard-coded before, so every game walked the same. Omitted fields keep today's defaults.
- `createAxisShaper` (`@jgengine/core/input/axisShaper`): per-axis response shaping between `ctx.input.axis(...)` and a sim. Keys and sticks get separate profiles (deadzone, saturation, curve, rise/fall/return rates), plus an optional `scale(signal)` callback for speed-sensitive steering, with `snapshot`/`restore`/`retune`. `analogAxes(bindings, ctx.input.analog())` picks the profile per frame, and `shapeAxisValue` exposes the deadzone and curve math. The `handling` dev demo uses it for keyboard steering that eases in and self-centres.
- `VehicleDynamicsTuning.jump` (`speed`, `count`, `window`) with `car.jump()`, and `VehicleDynamicsTuning.air` (`pitchAccel`/`yawAccel`/`rollAccel`, `damping`, `maxRate`) with `modifiers.air` for air control, plus `applyAngularImpulse` for dodges and flips. The body angles use the true geometry of the tilt rather than a small-angle guess. `measureAir` (`physics/handlingProbe`) reports apex height and time, double-jump height and air rotation rates. Held brakes and the handbrake now grip statically at rest, so a parked car stays put on a slope.
- `VehicleDynamicsTuning.suspension` (`springRate`, `damperRate`, `reboundRate`, `travel`, `rideHeight`, `antiRoll`) and a `groundHeight` option on `createVehicleDynamics`. The car rides four sprung corners over terrain: heave, pitch and roll come from real springs, axle loads come from spring forces, slopes pull downhill, and the car can leave the ground and land. The step adds `airborne`, `landingSpeed`, `wheelLoads` and a terrain-relative `airOffset`, and `applyImpulse` takes a vertical `dvy`. `measureRide` (`physics/handlingProbe`) reports settle time, heave overshoot, brake dive, roll gradient and landing bounces. Without the block, behaviour is unchanged.
- `createKinematicVehicle` and `createAircraftDynamics` gain `snapshot()`/`restore()` (`KinematicVehicleState`, `AircraftDynamicsState`) for prediction, rollback and replay. `createAircraftDynamics` also gains `retune()`. The stateful-primitive ratchet no longer misses factories with a default parameter like `options = {}`, so 20 more handles without state-out are now tracked in its baseline.
- Boat sim `createBoatDynamics` (`@jgengine/core/physics/boatDynamics`) on the same tick/snapshot/restore/retune contract as the car. Drag climbs toward hull speed and a planing hull sheds it; the keel resists sliding sideways; a `rudder` needs water flowing past it (speed or prop wash) while an `outboard` swings its own thrust and turns at rest. `waterHeight(x, z, t)` for waves and `current(x, z)` for drift. `measureHandling` and `tickDrivableVehicle` take it as-is.
- Force-based car sim `createVehicleDynamics` (`@jgengine/core/physics/vehicleDynamics`): a two-axle model where yaw comes from tire forces, so understeer, oversteer, lift-off tuck, power slides, trail-braking and handbrake turns come out of physical knobs (mass, weight split, CoM height, per-axle Pacejka-style tires, roll-stiffness split, drive split, gearbox or direct drive, aero, ABS/TC/stability, caster self-align). Fixed substeps, `snapshot`/`restore`, `retune`, `thrust` modifier. `measureHandling` (`@jgengine/core/physics/handlingProbe`) drives any vehicle sim through fixed scenarios and returns 0–100, top speed, braking distance, lateral g, turn-in latency, sideslip, spin and lift-off numbers a test can assert. `tickDrivableVehicle` now accepts either sim. `drive --key` takes chords (`KeyW+KeyD:2000`). Dev runner demo: `bun run drive handling`.
- `jgengine create --2d` scaffolds a board game with no 3D scene, editor document, or models: a `board` world, `presentation: "hud"`, a store slot holding a `CellGrid`, arrow/WASD/Space bound to commands with held-key repeat, and a GameUI that draws the grid. `--3d` names the default. 3D-only flags (`--player`, `--ground`, `--scene`, `--from-scene`, `--shape`, `--no-world`, `--no-editor`) are refused with `--2d` instead of ignored.
- `@jgengine/core/puzzle/cellGrid` is documented public API (`cell-grid` capability): an immutable 2D board with `withCell`/`withCells`, `fullRows`/`clearRows`, `collapseColumns`, and `findRuns`. It was importable but tagged internal, so agents never found it.
- HUD-only games (`presentation: "hud"`) set `data-jg-capture="ready"` after their first painted frame unless a capture host owns the flag, so `shoot` and `drive` no longer time out waiting for a `<canvas>` that never exists.
- The built-in Graphics settings tab now exposes render scale plus ambient occlusion, bloom, depth-of-field, and SMAA toggles alongside the quality tier; picking a tier re-applies its defaults. `@jgengine/core/settings/graphicsSettings` (`readGraphicsSettings`, `applyGraphicsQuality`, `GRAPHICS_POST_STAGES`) resolves the stored choices onto the `GraphicsProfile` the shell renders with, and `SETTING_IDS` gains `graphics.renderScale` and `graphics.post.*`. Every game gets the rows in its existing settings menu with no wiring (#1688).

- Model triangle budget: the shared GLB loader counts each model's triangles as it lands and warns once per URL when one exceeds `DEFAULT_MODEL_TRIANGLE_BUDGET` (100k; retune with `setModelTriangleBudget`). `heavyModels()` lists the offenders and `debug_snapshot` reports them under `probes.heavyModels`, so a heavy prop shows up in evidence instead of only as a slow frame.
- `bun run drive <game> --state <name>` boots into a `capture.states` entry the same way `shoot --state` does, so a staged scene can be measured with `--rpc debug_snapshot` or probed instead of clicked together by hand.
- Building palettes take textured surfaces: a `BuildingPalette` part (and `BuildingKitPart.material`) may be `{ color?, maps?, repeat?, roughness?, metalness? }` with `maps` straight from `buildMaterialCatalog(...).resolve(id)!.maps`, not only a hex colour. Generated facade boxes and kit models tile the PBR maps per slot; unbound kinds keep their flat colour. `buildingSurfaceColor` / `resolveBuildingSurface` read either form.
- `CommandDef.parse` and `defineCommand` (`@jgengine/core/runtime/commandRunner`). `parse` turns the untrusted wire value into typed input once. The runner refuses a `null` result with `MALFORMED_COMMAND_INPUT_REASON` before `scope`, `validate` or `apply` run, so those stages only see parsed input.
- Bounded input readers in `@jgengine/core/runtime/commandInput`: `readInputNumber` (always finite, optional range and integer), `readInputString`, `readInputOneOf`, `readInputPoint2`, `readInputPoint3`, `readInputArray` and `isInputRecord`.
- `helpers.ensureJoined(ctx, { gameId, userId, serverId? })` (`@jgengine/convex/server`) joins a trusted actor through the `joinServer` path and returns its outcome, so host mutations can run commands for users with no live session. It is idempotent and pins no client session.
- `createTransportSessionId()` (`@jgengine/core/runtime/transport`) makes a fresh id for one client session's join/leave pair.
- `CommandDef.access` (`@jgengine/core/runtime/commandRunner`). `"server"` marks a command whose input carries host-decided facts. The public Convex `runCommand` mutation and WebSocket client messages refuse it with `SERVER_ONLY_COMMAND_REASON`, while `helpers.runCommand` and `host.runCommand({ trusted: true })` still run it. `GameRuntime.commandAccess(name)` reports it.

### Changed

- Graph playback no longer resets the rig's first bone to its bind position every frame. On rigs whose first bone is the hips, that flattened the walk bob in every clip. The root bone is pinned only while a `rootMotion` state is current (the new `AnimGraphOutput.rootMotion` flag), and only horizontally. Root travel now moves the entity along its facing and at the model's scale instead of in rig space, and its vertical part stays in the clip. The shell helper is `takeRootMotion` (#1683).
- Perception judges each sound or damage stimulus once per observer, from where the observer stood when it first observed after the stimulus. Before, every `observe` re-scored past sounds from the observer's current position, so a guard walking toward an old noise grew more certain of it. `PerceptionSnapshot` gains `heard` and `nextSeq`; older saves re-judge their stimuli once.
- Perception `loudness` now scales how far a sound carries (`hearingRange × loudness`) instead of only its confidence, so a gunshot is heard past normal hearing range and a footstep only close by. Loudness `1` behaves as before.
- `npx jgengine find` now surfaces seams that had no capability row: `vehicle-seats`, `drivable-vehicle`, `kinematic-vehicle`, `affix-roller`, `hitscan-shot`, `loot-beams`, `world-item-pickup`, `on-death-drops`, `pointer-commands`, `current-target`, `fire-input`, `global-cooldown`. `PointerConfig.moveCommand` JSDoc now says it receives the clicked entity (click-to-target).
- `npx jgengine find` matches whole stemmed words (`aim` no longer hits `claim`), drops stopwords, tries multi-word queries joined (`pick up` → `pickup`), returns the closest partial matches instead of "no match", and searches CLI recipes and skill recipe docs.
- `raycastNav` and `NavMeshQuery.raycast` step into the polygon a segment enters when it starts on or crosses a shared vertex, so a funnel path's corner-to-corner legs raycast clear.
- `bakeNavMesh` (`@jgengine/navbake`) now voxelizes geometry with recast: the walkable surface is eroded by `agentRadius`, cut under obstacles lower than `agentHeight`, and split at `maxSlope` and `maxClimb`, so a wall standing on a floor leaves a hole the path goes around. Call `await initNavBake()` once before baking; `navBakeReady()` reports it. Walkable triangles must wind counter-clockwise seen from above. Optional `cellSize`/`cellHeight` trade precision for speed. The editor warms the baker when a host starts.
- `findPath`, `closestPoint` and `raycastNav` on `NavMeshData` share the cached query: paths bend only at portal corners, stacked floors resolve by height, and raycasts walk polygon edges instead of sampling.
- Mouse look now asks for raw, unaccelerated mouse deltas (`requestPointerLock({ unadjustedMovement: true })`) in `createMouseLookTracker`, the first-person camera and the shoulder rig, and falls back to a plain lock where that is unsupported. The helper is `requestRawPointerLock` (`@jgengine/shell/input/pointerLock`).
- Foot IK (`ModelConfig.ik`) keeps each clip's swing lift and moves feet by their ground's height relative to the model origin, instead of pinning every ankle to the ground. It lowers the pelvis so the lower leg can reach, keeps soles above the ground where a clip dips below it, bends toward the animated knee, tilts planted feet to the ground normal and fades out while airborne. `ik: "auto"` finds the legs by bone name, and `pelvis`, `maxAdjust` and `alignToGround` are optional. Probes hit terrain and blocking objects. The math is `placeFeet` and `inferLegChains` (`@jgengine/core/anim/footPlacement`) (#1683, #1758).
- Sky and fog colors render as authored under any tone mapping. The sky dome and sky fog treat their colors as on-screen swatches and pre-invert the renderer's tone-mapping curve, so a `#cfe4f5` horizon no longer shows as grey `#a6b3c2` under the default `neutral` look (AgX). The dome now runs three's tone-mapping and color-space chunks, so it matches with or without a post chain. Looks with bloom (`cinematic`, `photoreal`) get a brighter sky that blooms more near the sun; raise `bloom.threshold` if that is too much.
- The sky's sun shadows lose their stair-step: one 2048 map over a 70 m box that leads the camera (was 180 m centered on it), texel-snapped, with a 6-texel PCF radius. Time-of-day, biome and `DayNightSky` suns use the same rig instead of three's default 10 m box.
- `jgengine create` seeds the sun lower and ahead-left of the spawn (bearing 128°, 20° up), so its glow is in the first frame.
- Scaffolded `scripts/shoot.mjs` and `scripts/drive.mjs` refuse a viewport that stays one flat color for 10 s instead of saving it; nothing is written and the command fails.
- `jgengine create` defaults to a 3D terrain world instead of an infinite flat slab. `editor.scene.json` seeds rolling hills as a terrain sculpt (deterministic per game id, flat around the spawn) and a bright `day` sky with a sun bearing and distance fog; `src/world.ts` renders them through `environment()` and `environmentContentFromDocument` and holds only the detail-shaded ground palette. Reshape the hills in the editor. `--ground flat` keeps the old `place()` slab (#1762).
- `GameRuntimeTransport.joinServer`/`leaveServer` take an optional `sessionId`. The Convex `joinServer`/`leaveServer` mutations and the WebSocket `join`/`leave` messages accept it (at most 128 characters). `useServerSession` and the shell's multiplayer sync send a fresh one per mount.
- Shared-topology `jgServerMembers` rows keep up to 16 live `sessionIds`. A leave with a `sessionId` ends only that session; the user leaves once none remain. A leave without one, or on a row without session ids, still leaves outright. Rooms topology is unchanged.
- The WebSocket router tracks sessions per connection and only leaves the host when no other connection of the same user holds the server.
- `helpers.resetPlayerProfile` (`@jgengine/convex/server`) now resets a player who has left the server but still has a profile, instead of throwing, so scheduled wipes and insolvency resets no longer abort a whole batch. A user with neither membership nor a profile still throws.

### Fixed

- The `combat-loop` recipe drained health with `stats.delta`, which never reaches the death pipeline, so copied enemies never died. It now damages through `ctx.scene.entity.effect` and a catalog `receive` map. The `loot` recipe now registers on `ctx.game.loot` and rolls from catalog `onDeath`. `KinematicVehicleTuning.camera` and `DRIVE_AXIS_BINDINGS` JSDoc now state what they actually do.
- Editor RPC `add_marker` and `set_marker` accept `catalogId`. It used to be dropped while the call still returned `ok`, so a headlessly authored `mob`/`boss` marker spawned nothing.
- Agent bridge `editor_summon` now resolves once the editor has mounted (up to 15 s). It used to return immediately, so editor verbs later in the same `drive` batch failed with "no live editor".
- Catalog `onDeath: { dropMode: "world" }` scattered drops with `Math.random`, so kill loot landed in different spots on replay and on each peer. The runtime now passes the world's seeded `ctx.rng` to the scatter.
- The chase camera no longer falls behind fast vehicles. Its spring eased the camera's world position toward a moving target, so the lag grew with speed (about 9 m extra at 220 km/h); it now eases the boom offset from the target (#1770).
- A `createVehicleDynamics` bike with both `lean` and `suspension` launched itself the moment it leaned: the springs read the lean as body roll across the narrow track. Springs now ignore lean, and lateral g keeps the same lag it has without springs.
- `@jgengine/rapier`: a body created with `mass` now weighs exactly that. It used to add `mass` on top of the collider's density-derived mass, so a 2 kg sphere of radius 0.5 weighed 2.52 kg, and every force, impulse and joint on a massed body was off by the collider's volume.
- `createVehicleDynamics` with `suspension` spawned and reset level even on a slope. The wheels started buried, so the springs fired the car into the air and it tumbled. It now fits pitch, roll and height to the terrain under its four wheels.
- A sky authored in the scene document (`editor.scene.json` `environment`, written by the editor lighting workspace) now renders in `place()` worlds. `defineGame` never passed `skyFromDocument(editorLayers)` to the renderer, so those edits were silently dropped. A game's own `backdrop.sky` still wins, and a document holding only point lights leaves the world sky alone (#1762).
- `jgengine create --scene starter` placed its props and goal behind the spawn, so the first frame showed empty ground and W walked away from the goal. The starter scene now sits ahead of the default +Z facing.
- A standalone game's `bun run desktop` failed with `jgengine: command not found` because the CLI is not a dependency; the script now runs `npx jgengine desktop`.
- `@jgengine/navbake` joins the published lockstep set. `@jgengine/editor@0.18.1` depended on it while nothing published it, so `bun install` on the released SDK failed. `check-release-set` (in `gate`, `check-types`, and the publish workflow) now fails when a published package depends on a workspace package outside the publish list or out of dependency order, or when the publish, version-bump, and changelog package lists disagree.
- Publish has been red since that change because a CLI packaging test still hardcoded the old publish list, so navbake never reached npm and `npx jgengine create` scaffolds failed to install. The test now defers the order to `check-release-set`.
- `jgengine-verify` was silently skipped by the skills installer for every new game: its `description` held an unquoted `: `, which is invalid YAML. `check-skills` now parses frontmatter as YAML and requires `name` to match the folder.
- `jgengine create --help` printed usage instead of scaffolding a game named after the first argument, and `create` rejects unknown flags rather than ignoring them. The usage lists `--player`, `--ground`, and `--scene`.

- `pickModel` / `resolveModelPlan` warn once in dev when a `ModelPick.fallbackModel` is not in the asset catalog (a typo or an undeclared pack), instead of silently shipping a fallback that can never take over.

- Volumetric clouds no longer cut through distant buildings: the proxy dome writes per-fragment depth at the raymarch's first cloud hit, so geometry between the camera and the cloud slab occludes the clouds instead of the dome radius deciding. `STUDIO_STAGE_POST` ambient occlusion is softer (radius 1.1, intensity 1.5) so tower silhouettes stop streaking.

- Separate SDK publication validation from external Games quality checks while retaining the full local and main-branch gates.

- Correct foot-IK endpoint recomputation after hip rotation; verify reachable targets and clamping without changing bone lengths.
- Restore release CI prerequisites and make catalog-loader tests independent of the external Games checkout.

- Closing a second tab, or a join that resolved after a remount, no longer evicts a user whose live tab still shows them joined, which made every later command fail with "Not a member of this server".

## 0.18.1

### Migrate

- **Bump lockstep SDK packages to `^0.18.1`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.15.1`; `@jgengine/github` is `0.5.1`.
- Declare `topology: "shared"` on runtime definitions. Shared Convex hosts migrate the bounded legacy roster into indexed membership and per-player session rows; use `getServerCapacity` for stable lobby subscriptions.
- Put every command's read scope on its definition. Omitted scopes now load the actor and no chunks; explicitly request whole-world reads only where needed. Trusted host mutations can compose writes through `helpers.runCommand`.
- Handle typed join outcomes (`ok` and `reason`) or use `useServerSession` and `JoinGate`. Pass runtimes to `jgengineCronSpecs`; tick registration requires an actual tick hook.
- Declare currency precision with `decimals` and remove game-side cent conversions. Public amounts stay in major units and wallet writes round through integer minor units.
- Replace custom presence, chat, territory and tick batching with `createWorldPresenceStore`, chat helpers, chunk territory, and `forEachOnlinePlayer`. Legacy claim backfills must finish before removing their old schema.

### Added

- Indexed shared membership and capacity rows, bounded neighborhood poses with 100 ms server/client gates, durable rate limits, authenticated diagnostics and bounded retention.
- Territory footprint planning and atomic placement claims, chat validation and headless chat/join/territory surfaces, elapsed accrual, safe quantities, and player-profile reset through the initializer.
- An explicit `--patch` release option.

### Added

- Added smooth interpolation for remote presence player poses.
- WS ping/pong messages and client RTT sampling on the backend object.
- Added skin-backed HUD chrome variables, font loading, and the procedural `HudSkinPreview` fixture.

- Core gamepad snapshot model, deadzone/curve resolver, and controller glyph labels.
- Shell gamepad source polls connected pads, merges analog actions, supports synthetic `?gamepad=1` input, and exposes `ctx.input.rumble`.
- Core action context stacks layer gameplay, menus, and passthrough overlays with snapshot/restore; shell action tracking now respects the active context.
- Shell audio now supports listener orientation and per-sound Web Audio panners. `setListenerPose` accepts `{ position, forward, up }`; bare positions remain supported for one release.
- Core perception tracks deterministic sight, hearing, occlusion, and bounded observation memory for AI.
- `syncWorldColliders` now keeps terrain and static object colliders synchronized with a physics backend.
- Core decision graphs provide deterministic selector, sequence, condition, action, and utility runtimes with snapshot/restore.
- Core behavior descriptors can run registered decision graphs at the interest-scheduler cadence.
- Core navigation meshes provide deterministic polygon routing, off-mesh links, closest-point queries, and nav raycasts.
- WS sessions now issue resume tickets and retain disconnected memberships for a 15-second grace window, allowing reconnects to rejoin without replacing player state.
- Shell entity sprites can play atlas-backed sprite clips while preserving raw texture sprites.
- Tilemap layers can render atlas-backed textured tiles as one instanced quad mesh with parallax.
- Core pixel-perfect orthographic frustum and pixel-grid camera snapping are available to shell cameras.
- Core sprite atlas adapters and deterministic 2D sprite clip playback primitives.
- `src/art-direction.md` scaffold and `check-art-direction` gate for created games.
- Host-authoritative shell sessions now expose each accepted world frame to the local prediction buffer, reconciling and snapping the possessed pose only when drift exceeds its threshold.
- Shared GLTF loader configuration for Draco and KTX2-compressed assets, with CDN defaults and renderer capability detection.
- Material map overrides now support metalness, emissive, and height maps on standard and physical materials.
- Presentation and editor documents now accept gradient, HDRI/EXR, and cube-map environment sources.
- Authored `light` markers now feed bounded point/spot punctual lights with an eight-light default budget and editor warning.
- Graphics profiles now resolve per-tier render scale, shadow budgets, culling distance, particles, cascades, and post stages; games can override them with `defineGame({ graphics })`.
- `defineGame({ physics: { backend } })` now adopts a capsule character controller for shell/headless movement and registers the backend simulation after movement, including floor collision and step-up behavior.
- Added the `physics-probe` dev scene for Rapier crate, ragdoll, and vehicle motion.
- Assets now expose import-spec validation and magic-byte classification for supported source files.
- Hosted WS sessions now support spectator joins, viewer roles, throttled low-priority snapshot modules, and default command rate limits.
- `@jgengine/navbake` bakes deterministic navigation meshes and editor documents can store named bake data.
- Core tactical queries derive cover points and score bounded candidate positions.
- Added createSnapshotBuffer for delayed snapshot interpolation.

### Migrate

- `AudioEngine.setListenerPose` now accepts `{ position, forward, up }`. Bare `Vec3` positions remain compatible for one release and use forward `[0, 0, -1]` with up `[0, 1, 0]`.
- **Hosted world stores are asynchronous.** Implement `HostedWorldStore.load()` and `save()` as `Promise`-returning methods and construct persisted sessions with `createHostedWorldSessionAsync`; synchronous tests can keep using `memoryWorldStore`.
- **Look presets now default to `neutral`.** Games that relied on the previous implicit cinematic
  sky/post stack should set `look: "photoreal"` (or keep `look: "cinematic"`) explicitly.

- **`ServerTickPlan.due` is `{ id, runs }[]` instead of a repeated id list.** `planServerTick` repeated a system id once per missed interval, and the obvious way to consume that — `due.includes(id)` or a `Set` — ran the system once while the returned anchor had already advanced by every repeat, silently dropping the catch-up work. A host now reads `runs` (or `tickRunCount(plan, id)`) and loops that many times.
- **`CommandDef.validate` / `CommandDef.apply` take a fourth argument, `nowMs`.** Existing implementations that ignore it are unaffected. Commands that took a `now` in their `input` should read this instead — the client supplied the old one.

### Changed

- Hosted game runner queues authoritative inputs by simulation tick and applies the in-force frame each fixed step.
- Create scaffolds now use a 60 Hz simulation with interpolation enabled by default.

- Rigged `ModelConfig` entries can opt into shell foot IK with named thigh/shin/foot bones; the driver raycasts the shared scene terrain, solves each chain, and fades corrections while airborne.

- Animation graph clips may carry root-bone position tracks; states with `rootMotion: true` return the sampled displacement and the shell applies it to the entity while restoring the root bone's bind translation.

- Added renderer-free two-bone, FABRIK, and look-at inverse-kinematics solvers to `@jgengine/core`.

- Shell model animation now routes states and one-shots through the shared animation graph runtime.

- CI now clones the external games repository with `GAMES_CLONE_TOKEN` and runs the game shape, content, front-end, art-direction, and feel gates against it; those gates fail loudly in CI when the checkout is missing while remaining no-ops locally.

- `jgengine create` now accepts `--player`, `--ground`, and `--scene` so generated games can choose their model, terrain ground, and empty or starter scene.

- **Directional shadow frustum config actually reaches the GPU.** `shadowCameraSize` on a single (non-cascaded) shadow light was silently inert: the shell wrote the bounds through R3F pierced props and nothing refreshed the shadow camera's projection matrix, so three rendered depth with its default ~10-unit box at the world origin — the reason no open-world game had cast shadows away from spawn. The shell now syncs the projection (and reallocates the shadow map when `shadowMapSize` changes). `bloom.threshold`'s doc also now states it measures raw HDR before the exposure stage.
- **CSM (`cascades > 1`) no longer over-lights streamed meshes or clobbers material shaders.** Cascade scoping is injected per material, but the scan ran every 30 frames, so a model that streamed in rendered lit by every cascade light at once (a cascades× sun) until the next scan — a sub-second flash on desktop, the whole shot on capture rigs. The scan now runs every frame behind a WeakSet, and `setupMaterial` chains with a material's own `onBeforeCompile` (rim light, detail maps) instead of replacing it.

- **The host's wall clock reaches the runtime hooks.** `RuntimeInitContext` (so `onInit`, `onNewPlayer`, and `onTick` alike) carries `nowMs`, the host timestamp for that call. Anything keyed to real time — a UTC date rollover, a `lastTickAt` anchor read by non-tick code, an RNG seed or `createdAt` stamp — reads it rather than accumulating `dtSeconds` against an epoch persisted in the snapshot, which drifts by whatever the tick loses to stalls and `maxCatchUp` clamping. `GameRuntime.hydrate` takes an optional `nowMs`; `tick`, `joinPlayer`, and `runCommand` take an optional trailing `nowMs`. All default to `Date.now()`, and the shipped WS and Convex hosts pass the timestamp they already computed to plan the tick.
- **Imported models cast and receive shadows.** `cloneModelScene` never set `castShadow`/`receiveShadow` on a GLB's meshes (only the magenta fallback box had them), so every catalog model in every game rendered shadowless however the lights were configured. Cloned models now default to `ModelConfig.shadows: "both"`; set `"cast"`, `"receive"`, or `"none"` per model (foliage cards, decals, viewmodels).
- **`jgengine create` installs `game-design` and `jgengine-ui` by default.** The minimal skill set was intake, editor, and verify, so a real project never saw the greenfield design contract or the UI art-direction template unless someone ran `jgengine skills --all`; `MINIMAL_GAME_SKILLS` now carries both.

### Added

- **Host-authoritative world replication over WS.** Hosted sessions expose revision-cursor pulls, so server subscribers receive compact world diffs and automatically recover with a fresh baseline after missed revisions; viewer-specific projections still receive per-viewer baselines.
- **Animation graph — `ModelAnimationConfig.graph` and `@jgengine/core/anim/animGraph`.** A serializable, layered animation state machine: `clip`, `blend1D`, and `blend2D` states, transitions as data (parameter comparisons, consumed triggers, exit times, crossfade durations), masked and additive layers, and clip events. `createAnimGraphRuntime` is headless and owns every clip's playback time and weight, so the shell's mixer only seeks and weights actions (`useModelAnimation` takes the graph path when `graph` is set) and a headless host or test advances the same graph without three.js. The shell feeds the entity's smoothed ground speed as `speed`, merges parameters from the entity blackboard key `ANIM_PARAMS_KEY`, arms triggers from `entity.animation` / `combat.hitReaction` / `entity.died`, and emits the new `animation.event` for clip events (foot plants, hit frames). `locomotionGraph()` builds the previous idle/walk/run plus one-shot behavior as one authored graph.
- **`PhysicsBackend` — one interface over interchangeable rigid-body solvers.** `@jgengine/core/physics/physicsBackend` defines bodies (`BodyDesc` with a box / sphere / capsule / convex / trimesh / heightfield shape union, quaternion rotation, angular velocity, layers and mask, CCD flag), joints, `raycast`, `shapecast`, `overlap`, contact events, and `snapshot`/`restore` with stable handles; `capabilities` says what a backend really does. `createPhysicsWorldBackend` puts the zero-dependency `PhysicsWorld` behind it (translation-only, non-box shapes collide as their bounding box, warned once), and `runPhysicsBackendConformance` is the behavioral contract any adapter runs. `createCharacterController` (`@jgengine/core/movement/characterController`) is the first consumer: a capsule collide-and-slide controller with step-up, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry, pure over any backend and retunable during play.
- **Fixed-step simulation with render interpolation — `defineGame({ simulation: { hz } })` and `ctx.sim`.** Every driver (shell frame, HUD-only, headless runner, authoritative host) now steps through one `SimLoop` (`@jgengine/core/runtime/simLoop`): a fixed `hz` accumulates real time and runs equal-dt steps behind a catch-up cap (`maxCatchUpSteps`, default 5) instead of spiralling; the default stays `"variable"` (one step per frame) so existing games are unchanged until they opt in. Under a fixed rate the shell renders entities at `ctx.sim.renderPose(id)` — the pose interpolated between the previous and current step (`@jgengine/core/runtime/poseInterpolation`), with teleports past `snapDistance` snapping — so a 20 Hz sim no longer stutters. `ctx.sim.tick()` counts steps and `InputFrame.tick` carries it over the wire; `ctx.sim.addStage({ phase, run })` inserts per-step work before movement, after movement, or after `onTick` (the hook a physics backend or netcode buffer registers through). System fixed-rate groups gain the same substep cap. For replay and rollback: `seededRng` streams expose `state()`/`restore()` (`rngStateOf`/`restoreRng` for any `ctx.rng`), `createSimSnapshotRegistry`/`createContextSimSnapshot` capture world, clock, loop, and rng cursor together, `createInputRecorder` logs frames by tick, and `HeadlessRunner` gains `snapshot()`/`restore()`/`tick()` — a test proves that replaying recorded input from a snapshot reproduces every entity pose.
- **City and street races share one generator.** `extractCircuitRoute` lifts a sealed-off race lap from a city street network; city rules, editor baking, playground modes, and the shell renderer carry the same street dials through the resulting race surface and junction dressing.
- **`ctx.particles` — the game-reachable particle seam.** Game logic queues one-shot bursts (`ctx.particles.burst(config, count)`) and keyed standing emitters (`attach`/`retune`/`detach`, with `follow` to track a scene entity) as plain serializable data (`@jgengine/core/vfx/particleDirector`); the shell auto-mounts a renderer that turns them into pooled GPU point clouds via the existing `createParticleSystem`/`ParticleField`, caps pool sizes by graphics quality (low 128 / medium 256 / high 512 per effect, degrading density instead of dropping effects), and reaps finished bursts. Dust, sparks, exhaust, and debris no longer require forking the shell; headless tests assert the requested effects straight off the director.
- **`resolveSourceWalkerStep(source, cache, position, stepX, stepZ, options)`** (`@jgengine/core/movement/solidObstacles`) — `resolveWalkerStep` with no `GameContext`, for movement rules that live in a pure package with no React or three.js. `SolidObstacleSource`, `ObstacleReachCache`, `createObstacleReachCache`, `sourceObstacleReach`, `sourceObstaclesNear`, and `slideStep` are public alongside it instead of `@internal`, so a game no longer has to push the resolver up into its app layer and inject it back down. `resolveWalkerStep`'s doc now states that it already resolves X and Z separately, so a caller doing its own axis-split must hand it one axis per call.
- **`tickRunCount(plan, id)`** (`@jgengine/core/time/serverTick`) — how many runs a plan owes a system, or 0 when it is not due.
- **`grass()` exposes the clump/lighting knobs its shader already had.** `GrassEnvironmentConfig` gains optional `bladeBend`, `tuftRadius`, `colorVariation`, and `normalLift`, forwarded by the shell's environment renderer — a world descriptor can now author bushy wind-combed scrub instead of the fixed lawn-blade look; defaults unchanged.
- **`TerrainDetailConfig.sweeps`** — the detail shader's two macro colour sweeps (sun-dried patches, cooler wet pockets) take per-world RGB multipliers instead of hardcoded meadow tints. The old green-leaning "lush pocket" default painted moss onto arid/ashen biomes; defaults are unchanged, so a desert world should set warm/neutral `sweeps` explicitly.
- **`@jgengine/rapier`** — a Rapier-backed `PhysicsBackend` adapter with native capsules, shapecasts, rotation, CCD, joints, and collision queries.

## 0.18.0

### Migrate

- **Bump lockstep SDK packages to `^0.18.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.15.0`; `@jgengine/github` is `0.5.0`.
- **Register the presence reaper and redeploy your schema.** `jgPoses` gains `sessionId` / `kind` / `label` / `revokedAt` (all optional) and a `by_updated` index, and `createPresenceFunctions` now also returns `reapIdlePresence`. Re-export it (`export const { list, sync, leave, reapIdlePresence } = createPresenceFunctions(...)`) and add `crons.interval("jg presence reap", { seconds: 60 }, internal.presence.reapIdlePresence, {})`. Without it nothing deletes the row of a player who closed the tab — `list` filtered stale rows at read time but the table grew forever. `jgengineCronSpecs()` carries the new entry, and every spec now names the `module` (`"runtime"` / `"presence"`) it is exported from.
- **`presence.sync` returns `{ pose, lastSeenAt, displaced }` instead of `null`,** and `presence.leave` marks the row revoked instead of deleting it (the reaper collects it once idle). A caller that ignored the return value is unaffected.

### Changed

- **`presence.sync` accepts a call with no `pose` at all,** which holds the stored position and moves only the liveness stamp. `createConvexPresenceSync` sends one itself when the pose gate has suppressed writes for 10s, so a standing player keeps their row alive without a pose write — and without the reaper collecting a live session.
- **`PresencePoseRow` carries optional `sessionId` / `kind` / `label`,** so a nameplate reads off the presence row instead of joining against users per frame.

### Added

- **`createPresenceFunctions` gains the policy hooks the pose lane was missing** — `resolveSpawn(ctx, { serverId, userId, kind })` for where a session with no row starts (async and ctx-bearing, so a spawn can read the world, replacing a hardcoded origin), `poseRules` as a function of the actor `kind` so an agent need not obey a human's speed cap, `idleCutoffMs` / `maxReapPerRun` for the reaper, and optional `sessionId` on `sync` / `leave`: a second tab is a second row, syncing revokes the others, and the displaced session learns it on its next call through `displaced: true`.

- **A command declares what it touches, so `runCommand` hydrates that and nothing else.** `CommandDef` gains an optional `scope(input, actorUserId)` returning `{ players?, chunkKeys? }`, evaluated before hydration; the registered `runCommand` mutation also accepts an explicit `scope` argument, which wins over the declared one. A game that never sets either keeps the whole-world default. `ServerLoopHooks.joinScope(userId, isNew)` does the same for `onNewPlayer`.
- **`GameRuntime.commandScope` / `GameRuntime.joinScope`** expose those declarations to any host, alongside the pure helpers `resolveCommandScope` / `scopeWithActor` / `commandScopeEscape` (`@jgengine/core/runtime/commandRunner`).

### Fixed

- **`runCommand` no longer reads the whole world on every command.** `LoadSnapshotScope` landed in 0.17 but only `helpers.loadSnapshot` accepted one, so the supported way to run a command — the registered `runCommand` mutation, a one-line delegation to `helpers.runCommand` — hydrated every member profile and every chunk however narrow the command was, and a game could only get bounded hydration by reimplementing the mutation. `runCommand`, `joinServer`, and `leaveServer` now hydrate a scope; a command whose `apply` dirties a player or chunk its scope never named is refused rather than persisted, since writing an unhydrated chunk overwrites the stored one with a blank. `flushSave` / `flushServerIfDue` / `tickActiveServers` / `flushDirtyServers` still hydrate everything, which is correct for what they write.

## 0.17.0

### Migrate

- **Bump lockstep SDK packages to `^0.17.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.14.0`; `@jgengine/github` is `0.4.0`.
- **A deleted player profile stays deleted.** When a game persists players (`save.scope` includes `player`), hydrating a member with no `jgPlayerProfiles` row no longer falls back to the copy of their state kept on the server row — that copy was resurrected and then written back as authoritative, so a world reset that cleared profiles but not servers silently restored every player's old balance. A game whose save scope excludes players (`"none"`, `"chunks"`) is unchanged: the server-row copy is still their state. If you seed members onto a server row without going through `joinServer`, write their profile row too.
- **`slotsPerServer` is capped at 256** (`JG_MAX_MEMBERS_PER_SERVER`). `createGameServerFunctions` now throws at wiring time on a larger value instead of accepting it and failing at the write that overflows the document. One server is one Convex document holding the world row, the roster, and every member's session state; a bigger number was never a bigger world. Shard across servers, or move world state into `jgWorldChunks`.
- **The tick cron only sweeps games that declare `loop.onTick`.** A game with no tick hook used to pay a full hydrate plus a full persist every second per running server, for a snapshot `runtime.tick` returned unchanged; it now costs nothing while idle. If you relied on the tick cron to autosave a hookless game, the 60s `flushDirtyServers` cron and the `flushSave` mutation still do it.

### Changed

- **Walking NPCs now stop at the geometry that stops the player.** `wander`, `patrol`, and `ctx.scene.entity.moveToward` consulted no obstacles at all, so a pedestrian walked through the building the player cannot enter. All three now slide their step against the scene's blocking physical colliders — the same query `stepPlayerMovement` reads. A patrol's path state still advances on its own clock (an authored route is not rewound by contact), but the entity can no longer end a tick inside a solid. Pass `avoidSolids: false` to `moveToward` for a mover meant to ignore the world (a flier, a camera rig).
- **`@jgengine/assets` verify fails a model source that contributes no index entries.** A model pack that has never been pulled is a pin nothing has ever checked — that is how a Street Pack archive sat in the catalogue titled "Downtown City MegaKit". Record why a pack is not pulled in the new `AssetSource.unpulled` string; materials and sprites pull on demand and are exempt.
- **`persistServerSnapshot` returns `boolean` and skips a write that would change nothing.** Every subscriber of a query that read a server row re-receives the whole result when that document changes, so an unconditional write on the 1 Hz tick re-pushed the entire world to every client once a second whether or not anything moved. `ServerPersistPlan` gains `changed` (`serverState` / `sessionPlayers` / `revision` / `any`) for hosts that write field-by-field.
- **`sessionPlayers` is merged over the stored map, not rebuilt from the snapshot**, and entries for non-members are pruned. This is what makes a partially hydrated snapshot safe to persist.
- **`tickActiveServers` is bounded** to `maxServersPerTick` (default 32) servers per transaction, oldest tick anchor first, instead of pulling every running server of every game into one transaction. `flushDirtyServers` is bounded the same way.

### Added

- **`solidObstaclesNear` / `resolveWalkerStep` (`@jgengine/core/movement/solidObstacles`)** — the engine's single answer to "what is solid here", now shared rather than re-derived per consumer. `solidObstaclesNear(ctx, position, reachX, reachZ)` returns the blocking physical colliders near a point through a bounded `inBox` broadphase; `resolveWalkerStep(ctx, position, stepX, stepZ)` slides a horizontal step against them with the player's axis-separated sliding. Reach for these instead of deriving obstacles from a game catalog's own solidity flags.
- **Bounded hydration and a client chunk read** — `loadServerSnapshot(ctx, server, runtime, { players, chunkKeys })` hydrates only the members and chunks a mutation will touch instead of a document read per member plus one per chunk, and the generated `getChunks` query lets a client read chunk state by key (capped at `MAX_CHUNKS_PER_QUERY`). Chunks were the documented escape hatch for world state too large for one row, but reads were unbounded and clients could not see them at all, so moving geometry there made things worse. `getServerMeta` returns lobby state (status, member count, revision) without the world payload.
- **`@jgengine/core/runtime/worldChunks`** — `chunkKeyAt` / `chunkCoordAt` / `chunkKeyOf` / `parseChunkKey` / `chunkKeysInRadius` at a declared cell size, plus `setChunk` / `updateChunk` / `deleteChunk`, which mark the key dirty so the host actually persists it. `chunkKey` was an opaque game-chosen string and the engine never wrote `dirty.chunks`, so every game reimplemented the coordinate mapping and a game that forgot to push a key silently never saved that chunk.
- **Placed objects cull to `replication.aoiRadius`.** `objects` is the one large spatial replicated set and had no projector, so a world with a few thousand placements sent all of them to every viewer. It now joins the same area-of-interest policy as `entities` and `stats`. Unset `aoiRadius` still replicates everything.
- **`createGameServerFunctions({ matchmaking: "singleton" })`** — one shared world per game: auto-match always targets the existing world and a join past capacity fails loudly, instead of quietly creating a second world the player would be alone in. `joinServer` also reconciles a stored `slotsPerServer`/`save` that has drifted from the host options, which previously made raising the option a no-op on worlds that already existed. `selectJoinTarget` (`@jgengine/core/runtime/hostPolicy`) is the pure decision behind it.
- **`GameRuntime.hasTick`** reports whether a runtime declares `loop.onTick`, so any host's scheduler can skip a server whose tick is a no-op by construction.

### Removed

- **Asset source `quaternius-downtown-city`.** It was titled "Downtown City MegaKit" but pinned at the Street Pack archive, which ships Blends/FBX/OBJ only — a pull found no models. Nothing referenced it.

## 0.16.0

### Migrate

- **Bump lockstep SDK packages to `^0.16.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.13.0`; `@jgengine/github` is `0.3.0`.
- **Placed objects now replicate.** `objects` joined the always-on baseline snapshot modules, so `ctx.hydrate` replaces the client's placed object set with the host's, the way `entities` already did. A game whose peers all run the same authored placement is unaffected; a game that places props on the client only, after hydrate, must move that placement to the host or keep it out of `ctx.scene.object`.

### Changed

- **`GeneratedBuilding`'s `kit` prop is now the `BuildingKit`**; the per-part React escape hatch it used to name moved to `partRenderer`, so `kit` means the same thing on `GeneratedBuilding` and `InstancedBuildings`. Rename the prop if you passed a `BuildingKitRenderer`.

### Added

- **`modelLoadFallbacks()` from `@jgengine/shell/render/modelLoad`** lists every model the shared loader diagnosed as broken and replaced with a magenta placeholder. A placeholder was only ever a console warning, so a shipped game could render one in its starting area and nobody found out until someone looked at a screenshot; a capture host or smoke test can now require the list to be empty. `clearModelLoadFallbacks()` resets it between runs.
- **`worldHealthBars.occlude` / `WorldHealthBars`+`WorldNameplates` `occlude` prop.** World bars and nameplates are a screen-space overlay drawn outside the WebGL depth buffer, so nothing in the scene can hide them and an enemy's health bar reads straight through the building it stands behind. Set `occlude: true` to drop a bar when world geometry blocks the line from the player to the entity. Default `false` preserves the always-visible behaviour every shipped game currently relies on.

- **`DustField` + `WeatherLayer` mode `"dust"` (`@jgengine/shell/weather`) — wind-borne particulate.** `WeatherLayer` could only do `clear`/`rain`/`snow`/`mixed`, so an arid, volcanic, ashen, or blown-out world had no way to put anything in the air. Dust drifts on horizontal wind with a slow vertical bob rather than falling; `groundBias` packs density toward the surface, motes fade near the top of the volume so recycled ones do not pop against the sky, and faster motes ride higher. Set `dustAlways` to keep it in the air alongside rain or snow instead of replacing them. Defaults `DEFAULT_DUST_COUNT`/`DEFAULT_DUST_DENSITY` sit beside the rain and snow pair.

- **Authored `city` volumes render their near-LOD facades with a `BuildingKit`.** `CityRules.buildingKit` names a kit; a game registers the bound art with `registerBuildingKit(kit)` from `@jgengine/shell/structures/buildingKitRegistry` and the scene document carries only the name, so a city stays portable and free of model paths. Unset or unknown names keep the palette blocks. This puts the whole `city` primitive — not just the `building()` feature — on the kit path.
- **`BuildingKit` from `@jgengine/core/world/buildingKit`** binds a generated building's facade part slots to real models, so procedural buildings render as an instanced asset kit instead of untextured blocks. `defineBuildingKit({ parts: { wall: ["Wall_A.glb", …] } })` maps a part kind — or a `${facade}.${kind}` slot — to ordered variants that `kit.variant` indexes, with `fit`/`offset`/`rotation`/`scale`/`tint`/`autoOrient` per variant. Kinds a kit leaves unbound keep rendering the palette block, so a partial kit is a valid kit, and `omit` drops a kind entirely. Pass one through `building({ kit })` or straight to `InstancedBuildings`; `buildingKitVariantCounts(kit)` feeds `generateBuilding` so its seeded picks span exactly the models the kit binds.
- **`generateBuilding`, `generateBuildingDistrict`, and `createBuildingConfig` are now documented public API** (`@jgengine/core/world/buildings`) and appear in `capabilities.md` under `building-generator`.
- **`capture.views` on `defineGame`** — named framings (`GameCaptureView`: `look`, `lookFrom`, `spawn`, `state`, `run`, `settleMs`) that `shoot <game> --view <name>` and `drive --view <name>` replay exactly, so a before/after pair is the same view by construction. `shoot <game> --list-views` prints what a game declares; an unknown name fails listing them, and any explicit capture flag still wins over the view's value.
- **`modelLoadIdleMs()` from `@jgengine/shell/render/modelLoad`** reports how long the shared GLB loader has been idle (`0` while a model is in flight). Capture hosts wait on it instead of guessing a settle delay, so an establishing shot no longer races streaming models.
- **Per-instance state on placed objects** — `SceneObject.state` (opaque `Record`), set through `ctx.scene.object.setState`/`patchState`/`place({ state })`. It survives move/rotate/setVisual, replicates and saves with the object, and round-trips as `RuntimeObjectRow.flags` via `toRuntimeObjectRow`/`fromRuntimeObjectRow` (`@jgengine/core/multiplayer`). A chest's locked flag or a machine's fill level no longer needs a game-owned map keyed by `instanceId`.
- **`slotInventory` is a working feature** — `ctx.scene.object.slots` gives objects whose catalog entry declares it real per-instance container contents: `layout`/`state`/`count`, `put`/`take`, and `transferIn`/`transferOut` between a player inventory and the object, all validated against the declared slot count, `accepts` kind filter, and stack limits, with neither side mutated on a rejection. Contents live on `SceneObject.slots`, so they replicate and save with the placement.
- **`loadServerSnapshot` and `persistServerSnapshot` are public** (`@jgengine/convex/server`), and `createGameServerFunctions` also returns `helpers` (`loadSnapshot`, `applyCommand`, `persistSnapshot`, `runCommand`) bound to the same runtimes and auth mode. A host mutation can now pair snapshot work with its own table writes in one transaction; the registered `runCommand` mutation delegates to `helpers.runCommand`, so there is one implementation.

### Fixed

- **Open storefronts are no longer holes through the building.** `generateBuilding` emitted an open ground-floor store as a lone `storefront` panel with no wall behind it, while every upper-floor window was correctly backed by one. Storefront and window materials render at 0.56 opacity and a generated building is a hollow shell, so those bays let you see straight through the building to the world beyond — and open stores only land on the street-facing facade, so a row of shops along a road was the worst case. The bay now emits its wall first, exactly as an upper floor does.
- **`joinServer`, `flushSave`, `flushDirtyServers`, and the `auto` autosave can write player profiles again.** All four cleared the snapshot's dirty set before handing it to the persist plan, which gates profile writes on exactly that set — so the functions named "flush" flushed the server row and chunks only, and an `onNewPlayer` grant (starting money, starting items) never reached `jgPlayerProfiles`. `joinServer` now persists the snapshot it just marked; the flush paths mark every player dirty first, since hydrating from storage resets the dirty set. `planServerPersist` also clears `dirtyAt` once everything outstanding is written, so a successful flush stops autosave from re-firing forever, and a profile patch no longer clobbers a host-maintained `dirtyAt`.

## 0.15.0

### Migrate

- **Bump lockstep SDK packages to `^0.15.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.12.0`; `@jgengine/github` is `0.2.0`.
- **`jgengine create` scaffolds a textured world by default (#1550).** `--world` is now the default and `--no-world` is the opt-out (matching `--no-editor`); `--world` is still accepted. The default scaffold previously emitted untextured proxy geometry, which the AGENTS.md it ships declares a failing result. `create` now also pulls the starter asset packs into `public/models` after install, and never fails the scaffold if that pull cannot run (offline, blocked proxy, `--no-install`) — it prints `npx assets pull starter` in next steps instead, and the emitted AGENTS.md documents the same command. Skip the pull explicitly with `--no-assets`.
- **Start game code at `@jgengine/shell/gameKit` (#1541).** Prefer that surface for `defineGame`, `GameHost`, stores, systems, authored-scene helpers, and HUD building blocks. `@jgengine/core/authoring` remains for pure core helpers the kit does not re-export but is no longer a competing start-here path.
- **Every game must now declare its run-phase story — silence is a gate error (#1337).** The run-phase
  contract used to be optional with a permissive default: a game that never published a phase read as
  `"playing"`, so the shell painted the touch dock over title/menu/results screens (Vice Isle #1329).
  `check-game-shape` now fails any game under `Games/*` that neither declares `lifecycle` in
  `defineGame({...})` nor calls `setGamePhase(ctx, phase)` somewhere in `src/`. To satisfy it, a game
  that runs live from boot with no menu/pause/end screens declares `defineGame({ lifecycle: "always-live" })`;
  a game with run states declares a `LifecycleConfig` or calls `setGamePhase` at its
  menu→playing→ended transitions. No runtime behavior changes for a game that already drove phases.
- **`generateCity` output is plot-first and no longer square by default.** Same options in, but the
  layout changes: lots derive from block-frontage plots (varied sizes), the street net clips to a
  seeded organic outline (`streets: { outline: 0 }` restores the legacy rectangle-filling footprint),
  and the landmark/interior-fill passes are gone — `content.landmarks` now dials the share of grand
  (block-scale) plots and `blockFill` dials frontage coverage/plot depth/park share.
  `ResolvedCityLot` drops the `interior`/`park` flags (no street-less interior lots exist anymore)
  and the `INTERIOR_LOTS_PER_BLOCK_CAP` export is removed; `GeneratedCity` gains `plots` + `parks`.
  A city consumer that only reads `lots`/`lotContent` (center/rotation/footprint/pieces) needs no
  code change.
- Walking-player object collision is now **on by default** (`PlayerMovementConfig.collideObjects`
  defaults `true`): placed scene objects with blocking colliders stop, support, and are stood upon by
  the player without opting in. A game that relies on walking through placed objects sets
  `movement: { collideObjects: false }` (or gives those objects non-blocking colliders).
- Ground drops taller than `movement.stepHeight` (default 0.4) now fall under gravity instead of
  snapping the feet down in one frame. Games that teleport-spawned players high above the ground will
  see them fall to it.
- **Projectile tracers are now opt-in and never drawn for ballistic shots.** `presentationEffects.tracers`
  defaults **off** (every other presentation channel still defaults on), so the shell no longer draws a
  straight muzzle→impact line for a game that never asked for one — `fireProjectile` is a generic seam
  (bullets, bolts, grenades, launchers), and a tracer only reads as a real shot for direct-fire weapons.
  A game that wants bullet tracers now sets `presentationEffects: { tracers: true }`. Even with tracers on,
  arced/exploding shots (anything ballistic — grenades, rocket/grenade launchers) never draw one, because a
  straight line through an arc is a fake beam. The `projectile.settled` event and `ProjectileSettleReport`
  gain a required `ballistic: boolean`; code that emits or consumes them directly must set/handle the field.

### Changed

- **Prerelease publishing (`0.15.0-next.0`).** Package versions now move together via `bun run version:prerelease` (`bun run check-version` verifies package versions, internal `@jgengine/*` ranges, and the `VERSION` mirror agree). The Publish workflow is `workflow_dispatch`-able from any ref and picks the npm dist-tag from the version, so prerelease builds land on `next` and never move `latest`. Install a prerelease with `npm i @jgengine/core@next`.
- **SDK remediation Phase 3a — AAA visual/sim ceiling items (#1545).** `ctx.rng` is a per-world seeded stream (loot tables default to it); post chains default to SMAA before tone-map; cinematic IBL is a procedural sky/ground(+sun) probe instead of RoomEnvironment gray box; directional lights support `cascades > 1` via three.js CSM; `stepFlock` no longer shares a module-global spatial grid across worlds.
- **SDK remediation Phase 1.2 + 2.3 — adoption ratchet + cheap seams (#1543).** `check-game-shape` now freezes module-level `export let` and local `Window`/`Bar`/`Chip`/`Slot` components via shrink-only `scripts/game-adoption-baseline.json` (new smells fail CI). Ships `grounded(ctx, x, z)` (`@jgengine/core/world/grounded`, also on `gameKit`), `DEFAULT_WALK_CODES` (WASD + Space on `@jgengine/shell/shellMovement` / `gameKit`), and a HudTheme-token default skin on `SettingsTrigger` (`SETTINGS_TRIGGER_CLASSNAME`) so bare triggers need no 8×8 className re-authoring.
- **SDK remediation Phase 0 — one start-here + correct UI guidance (#1541).** `@jgengine/shell/gameKit` is the sole happy-path game entrypoint; `@jgengine/core/authoring` no longer claims "Game code should begin here" (still a core helper barrel for pure exports the kit does not re-export). The whole-game recipe and scaffold `GameUI` header now match CLAUDE.md: compose shipped HUD building blocks (`StatBar`, `Hotbar`, `Coins`, …) and reskin via HudTheme — games own layout/terminology/art direction, not re-derivation. Studio Showcase and Tower Guard import kit-covered symbols from `gameKit`.
- **Kinematic vehicle drive feel** (#1515) — `createKinematicVehicle` gains a low-speed launch torque floor, softer reverse by default (`chassis.reverseForceScale`, default ~0.48 of engine force), handbrake rear-lock oversteer yaw, engine-braking that no longer fights powered reverse, and lighter ESC while handbraking. Existing chassis tunings pick this up with no migrate; override `reverseForceScale` only if you need the old full-force reverse.

### Added

- **`advanceStepCadence` (`@jgengine/core/audio/stepCadence`) — distance-driven footstep timing.** Pure state-in/state-out: feed it the ground distance a mover covered this frame and it reports how many steps landed and which foot led, so foley stays locked to distance instead of a wall-clock timer that desyncs the moment speed changes. Configurable stride, per-advance `strideScale` for crouch/sprint, and a shorter first step from rest so walking off is heard immediately.
- **`resolveMusicState` (`@jgengine/core/audio/musicState`) — combat-reactive soundtrack selection.** The engine could already crossfade themes but nothing decided *when*, so every game re-handrolled the threshold and got a track that flapped as one enemy walked in and out of range. Give it a tier ladder (intensity → theme) and it resolves the theme to play: escalation is immediate, de-escalation waits for the tier's `holdMs` and needs intensity to fall a `release` margin below the tier it is leaving. Pure and serialisable.
- **Synth voices take a `sustain` (`ToneVoice`/`NoiseVoice`).** Seconds held at full gain before the decay. Default 0 preserves every existing patch. Without it there was no way to author a *sustained* sound at all: every voice decayed to silence inside its duration, so a patch rendered once and looped as an ambient bed pulsed audibly once per loop. Set `sustain` to `duration` for a flat wind, rain, or machine-hum bed that loops seamlessly.
- **A street race is a section of a city, sealed off.** `extractCircuitRoute(network, rules)` (`@jgengine/core/world/raceCircuit`) lifts a closed lap out of an existing street network — the same asphalt the open world already drives on — and seals every side street leaving it. Returns checkpoints ready for `raceTrack()`, start/finish, grid, corners, kerbs, and sectors. Circuit-mode networks take the same path (their loop edges are the cycle). The city volume kind gains a Race group (`race`, `raceLapLength`, `raceArterial`, …) that resolves a world-space `ResolvedRace` on `ResolvedCity.race`; `CityRenderer` draws the lap surface, kerbs, start/finish, grid, and barrier seals. Playground adds a Street race mode over the same shared slider set.
- **Parked vehicles and junction signals on a city district.** `CityRules.parkedCars` / `signals` seed curb and pad fleets plus mast-arm signals (arterials) and sign posts (the rest); `ResolvedCity.vehicles` / `signals` hand them to `CityRenderer`.
- **`ModelMaterialOverride.rim` — a fresnel rim light on any entity/object model.** `{ color, strength, power }` adds a bright edge where the surface turns away from the viewer, patched into the material's own shader so it shadows, fogs, and tonemaps with the body rather than floating in front of it. This is the standard fix for an actor that shares a value band with its ground: no albedo choice separates a body from terrain lit by the same key, but an edge does. Omit for no rim (default).

### Fixed

- **Noise voices longer than the shared noise buffer no longer go silent partway through.** The 1-second buffer is sampled from a random offset, so any `NoiseVoice` whose duration exceeded the remainder ran off the end and finished in silence — invisible on short impact bursts, fatal for anything sustained. The source now wraps; the voice still ends on time.

- **CityRenderer junctions use the welded carriageway-union seam end-to-end.** District roads go through `buildTrimmedIntersections` + `buildIntersectionMarkings` with the same compact opts as StreetGeometryPreview / playground (`curbReturnRadius: 2`, `apronMargin: 0.25`). Plaza hull aprons, fallback discs, and fat zebra ladders are gone; race resurfacing stops at junction mouths so welded patches stay visible.
- **Denser urban fabric for metro/Manhattan districts.** Commercial class setbacks/spacing tighten toward a streetwall; slabs/towers pick L-plans and mid-block courts; dense blocks push footprints to the curb and paint courtyards as hard courts rather than lawn leftovers.
- **City and track are one slider surface again.** `CityRules` and the editor `generate_streets` bake now forward every `StreetNetworkRules` dial (`elevation`, `maxGrade`, `compactness`, `sidewalkWidth`, `outline`, `residentialBranches`); `check-street-rule-parity` fails CI if either drifts. Playground no longer hides layout sliders when you switch modes — modes are presets over one shared set.
- **City facades dissolve their window stripes before they alias into speckled static**, and ground floors now read as storefronts rather than repeating floor 30. Block interiors paint developed fabric instead of raw lawn, and crosswalks only land at real junctions.
- **`terrain({ detail: { material: { tint } } })` separates a ground texture's grain from its hue.** A detail material's colour map used to replace the terrain's authored colour outright, so the biome palette, macro sweeps, and slope-rock blend under it were painted over and a whole region flattened to the texture's single hue — despite the API documenting that it "blends with, never replaces". `tint` defaults to 1 (the existing behaviour, so no shipped game changes); drop it toward 0 to keep the authored colour and take only the map's light and shade.
- **`sky({ gradientExponent, cloudiness })` are now authorable — and `sky()` no longer silently drops them.** `gradientExponent` controls how fast the gradient reaches `zenithColor` off the horizon (default 0.65, unchanged): below 1 pulls the zenith hue down into the frame, which is what an eye-level camera in flat terrain actually needs — at the default the sky reads as one horizon-coloured wash however saturated the authored zenith is. `cloudiness` (default 0.35, unchanged) is the procedural cloud-band opacity.
- **A model that throws while rendering is now reported instead of vanishing (#1588).** `ModelFallbackBoundary` swapped in its fallback (usually nothing) and stayed silent: `probes.fallbacks` stayed empty, no warning was logged, and an entity simply disappeared. It now reports a new `renderError` fallback cause on every failed render and warns once with the model url and the caught error.
- **A failing or slow-loading piece of a part composition no longer erases the whole actor (#1588).** Parts, bone attachments, and material-map appliers each mount behind their own error/Suspense boundary, so one bad piece drops itself — the base mesh and healthy siblings still render.
- **`EntityModel` / `useModelAnimation` / `PartMotionRig` no longer require a running game (#1588).** They read the game context optionally, so `EntityPreview` renders a model in a character screen, inspector, or preview page instead of throwing `useGameContext must be used within <GameProvider>` and rendering nothing.
- **A model `animation` config naming clips the rig does not have now warns instead of silently posing wrong.** An explicit `ModelConfig.animation` always beats `"auto"`, and a missing state clip fell back to the GLB's *first* clip while missing one-shots were dropped — so a mistyped or wrong-pack clip name left an actor looping an arbitrary animation (on a KayKit rig, `1H_Melee_Attack_Chop`) with no death or hit reaction, and nothing in the logs. `useModelAnimation` now warns once per mount naming the missing clips plus the rig's real clip names, and points at `animation: "auto"`. No behavior change for a config whose clips all resolve.
- **City-scale player movement no longer freezes `pose` at ~3 fps (#1517).** Broadphase reach is split into capped horizontal vs vertical extents (tower height no longer inflates XZ/Y queries), `objectStore.inBox` bails to a linear object scan when the 1 m cell volume is huge, dense mesh-box colliders collapse to outer AABBs for walking, and `movement.frozen` skips the gather (seated drivers).
- **Generated intersections are compact carriageway unions, not plaza discs (#1511).**
  Apron pull-back is the projected crossing half-width only (opposite through-arms are ignored; curb
  return is no longer double-counted into the mouth distance). Exterior corners get small width-clamped
  returns; T far sides stay straight through-curbs; degree-2 turns form L-unions via the outer edge
  intersection. Sidewalk aprons offset along the local ring normal (not radially from the node), so
  corners no longer become giant octagonal bands. `buildIntersectionMarkings` still joins degree-2
  offsets and places stop lines at multi-arm mouths. Playground inspection hides buildings and does not
  render cuboid traffic. Deterministic fixture: `bun run shoot --fixture StreetGeometryPreview`.
- **Managed capture no longer steals Windows focus or waits on a background supervisor.** Daemon start
  launches hidden Chrome directly, while Git, taskkill, Vite, Chrome, and ffmpeg subprocesses all suppress
  console windows. Website query state hydrates before Three.js boot, preserving fail-fast route capture.
  Plot frontage is re-resolved after block-corner subdivision so buildings retain the correct street, and
  `lots.variety` now controls the deterministic plot-tier mix.
- **`extractBlocks` warns (dev-mode) instead of silently collapsing on wandered / arc-filleted streets (#1502).** Proximity welding assumes near-aligned centerlines; wandered arc-filleted streets weld into a degenerate face set (a real case went 38 streets → 2 faces with no signal, so the block fabric looked adoptable but was empty). `extractBlocks` now emits a `console.warn` when the extracted face count is implausibly low for the street count (≥8 streets yielding fewer than street-count/4 faces), pointing callers at `extractGraphBlocks` — which traces faces from the exact street graph and is robust against the centerlines that defeat welding. No-ops in a production build; no runtime behavior change.
- **`assets pull` / `assets add` default output dir now lands where the dev server serves models** (`@jgengine/assets` CLI, #1339) — inside the monorepo a bare `pull`/`add` previously wrote to a cwd-relative `public/`, so running it under `packages/assets` (or any subdir) dropped GLBs into a folder no game serves. It now defaults to the served root `apps/dev/public` when that exists (falling back to the historical cwd-relative `public` for out-of-monorepo consumers), so pulled bytes land in `apps/dev/public/models/<pack>` where the runner reads them. `--dir` still overrides.
- **The missing-model diagnostic now names the served target dir, and a bare `assets reindex` no longer disagrees with `assets pull` (#1499).** The runtime 404 / HTML-fallback diagnostic (`classifyAssetResponse` in `@jgengine/core/scene/assetDiagnostics`) told you to run `assets pull <source>` but never named where the bytes must land; it now points at the dev server's served models dir (`apps/dev/public/models` in the monorepo) and states no manual copy is needed. Completing #1339, a bare `assets reindex` / `assets reindex-sprites` (no dir arg) now defaults to that same served root via `resolveDefaultReindexDir` instead of a cwd-relative `public/`, so it can never scan an empty dir while `pull` writes to the served one. Explicit dir args are unchanged.

### Added

- **An `rng` that falls back to `Math.random` now says so (#1582)** — twelve core primitives (`createMobBrain`, `applyStatus`, `rollCheck`, `autoTarget`, `rollCapture`, `createLootRegistry`, `createRunDraft`, `createResourceNode`, `worldItem`, `selectLane`, `assignRoles`, `scheduleDelivery`) took an injectable `rng` and defaulted it silently, so a caller who forgot to pass `ctx.rng` got a simulation that could not replay, lockstep, roll back, or be pinned by a test — with no signal. They now route through `resolveRng` (`@jgengine/core/random/resolveRng`), which warns once per call site in dev and stays silent in production. The fallback itself is unchanged, so no behavior moves; four sites whose `rng` was a defaulted parameter now take it as optional, which is source-compatible.
- **The two parts a game front-end could not reach for (#1577)** — `useMenuRouter(root)` (`@jgengine/react/menuRouter`) is the screen stack behind title → save select → settings → credits: `open`/`back`/`replace`/`reset`, `path`, and Escape-to-back on by default; `menuStackReducer` exposes the same transition as a pure function. For credits, `creditsForSourceIds(ids)` (`@jgengine/assets/credits`) turns the pack ids a game already declares to `buildCatalog` into licensed, attributed sections, `mergeCredits`/`groupCredits` (`@jgengine/core/game/credits`) fold and shape them, and `CreditsScreen` (`@jgengine/react/creditsScreen`) renders a `CreditsDocument` HudTheme-skinned. Attribution is now derived from what the game ships instead of hand-maintained.
- **`ResourcePool` and `ComboPoints` can hand their state back (#1575)** — both gain `snapshot()` / `restore(state)` over a plain-data `ResourcePoolSnapshot` / `ComboPointsSnapshot`, so a save file, a server host, or a test can carry them across a rebuild. `ResourcePool` also gains `retune({ max, regenPerSecond, decayPerSecond })`, so a buff or difficulty change no longer needs a fresh pool (regen/decay were frozen at construction). Additive: existing calls are unchanged.
- **`HudTheme` now covers the whole `--jg-*` vocabulary (#1573)** — new `surface`, `status`, `font`, and `rarity` groups plus `-deep`/glow palette companions, so `hudThemeVars(theme)` emits all 53 tokens the shipped components read instead of the 28 the bars/frames/slots needed. One theme spread on a HUD ancestor now skins the `@jgengine/react` primitives and the `registry/jgengine` blocks together; previously a game had to author two disjoint token sets. New `hudTheme({ ... })` fills a theme from per-group partials, and `deriveHudTheme({ accent, surface })` builds a coherent full theme (including frames, troughs, and slots) from a seed pair. `ember` / `synthwave` / `fieldkit` join `HUD_THEME_PRESETS` with their registry values unchanged. Additive: existing themes and every rendered surface are unaffected.
- **Runtime-mutable arcade car tuning + hops (#1550)** — `KinematicVehicle.retune(tuning)` swaps a vehicle's whole tuning block while keeping position, heading, velocity and gear state, for vehicles whose stats change during play (installed parts, damage, debuffs). New tuning fields `hopGravity` (enables `vehicle.hop(impulse)`, reported as `step.airOffset` / `step.airborne` and applied on top of the ground sample by `tickDrivableVehicle`), `coastDeceleration` (flat coast road-load, versus `rollingResistance`'s proportional decay), and `reverseForceScale` (reverse push on the arcade path, previously reachable only via a full `chassis` block). New per-tick modifier `brakeScale`. All additive: omit them and every existing vehicle behaves exactly as before.
- **`assets pull starter` (#1550)** — one meta-id that pulls every pack the starter catalog needs (`kaykit-adventurers`, `kaykit-dungeon`, `kaykit-furniture`, `kaykit-city-builder`, `quaternius-stylized-nature`) into `public/models`, so a game does not have to name five pack ids to make `asset:` references resolve.
- **`CastConfig.moveDeadzone` (#1550)** — per-tick movement at or below this is treated as the caster's noise floor and not accumulated toward `moveTolerance`. A 3D game's pose jitters every frame from terrain-following ground snap, and because `moveTolerance` accumulates, feeding those raw deltas in silently cancels casts the player never interrupted. Declare the noise floor once in `begin` instead of filtering at every `tick` call site. Defaults to `0`, so existing callers are unchanged.
- **`LootDrop` + `lootDropsOf(data)` (#1550)** — the granted-reward row is now a named type, and `lootDropsOf` safely reads the drops out of a `loot.granted` payload that arrived as `unknown` (a `FeedEntry.data`, a replicated event, a persisted row), returning `[]` for anything not shaped like a loot grant and discarding malformed rows. Replaces the `entry.data as { drops?: ... }` cast that four call sites — three games plus `ToastStack`'s own default renderer — were each writing by hand.
- **`createRegenShield({ pool })` — stat-backed shields (#1550).** A `RegenShieldPool` port (`current`/`max`/`set`) lets a regen shield read and write storage the game already owns — an entity stat, a replicated store — instead of keeping the value privately, which is what blocked adoption in games whose shields live in the stat system that damage, HUD and replication all read. A pool-backed shield also detects *external* decreases between ticks and restarts its regen grace period, replacing per-entity snapshot bookkeeping. Adds `noteDamage()` (restart the grace period without moving the value, for hits the shield cannot see) and per-tick `tick(dt, { regenPerSecond, regenDelayMs })` rate overrides for talent- or buff-derived rates. The grace period now advances on simulated `dt` rather than wall clock, so a paused game no longer regenerates.
- **`ctx.rng` + `createGameContext({ seed, rng })` (#1545)** — deterministic per-world `[0,1)` stream for sim paths; same seed → same sequence across worlds/hosts.
- **Post `aa: "smaa" | "msaa" | false` (#1545)** — SMAA default on post chains (before OutputPass) for alpha-tested edge crawl.
- **Procedural sky IBL + directional `cascades` CSM (#1545)** — `EnvironmentLighting` bakes sky/ground/sun into PMREM; `cascades > 1` on directional lighting mounts cascaded shadows.
- **`grounded(ctx, x, z)` (#1543)** — returns `[x, groundY, z]` from the live world surface; stop re-deriving `[x, ctx.world.groundHeightAt(x, z), z]`. Import from `@jgengine/core/world/grounded` or `@jgengine/shell/gameKit`.
- **`DEFAULT_WALK_CODES` (#1543)** — stock WASD + Space map for the shell walk controller (`@jgengine/shell/shellMovement`, re-exported from `gameKit`).
- **`SETTINGS_TRIGGER_CLASSNAME` + default `SettingsTrigger` skin (#1543)** — HudTheme-token 8×8 chrome button; omit `className` for the stock look.
- **City generation now enforces plot clearance and planar streets (#1454).** Building plots clear
  one another and street corridors, building massing fits its plot, landmark footprints avoid roads
  and overlap, and generated street chords and branches cannot cross without a junction. Interior
  arterial stubs are demoted, dead ends receive cul-de-sac bulbs, and the playground exposes a
  linkable plan view plus plot spacing and variety controls.

- **`lifecycle: "always-live"` sentinel + `GameLifecycle` type (#1337).** `defineGame`'s `lifecycle`
  now accepts the string sentinel `"always-live"` alongside a `LifecycleConfig`. It is an explicit,
  truthful declaration that the game has no menu/pause/end screens and runs live from boot — runtime
  behavior is identical to declaring no lifecycle at all (no phase sync, no start/restart commands),
  but the intent is now *stated*, not implied by silence. `defineGameDefinition` resolves the sentinel
  to `undefined`, so downstream consumers still only ever see a real `LifecycleConfig` or nothing.
  `@jgengine/core/gameplay` exports the `GameLifecycle` union type. Pairs with the new
  `check-game-shape` run-phase requirement (see Migrate).
- **`debug_snapshot().probes.textureErrors` surfaces in-GLB texture-load failures (#1342).**
  `@jgengine/core/devtools/textureErrors` adds an allocation-aware collector
  (`armTextureErrors`/`reportTextureLoadError`/`resetTextureErrors`/`textureErrorsSnapshot`) that the
  shell's shared GLB loading manager feeds on every texture/image `onError`. Previously only whole-model
  catalog fallbacks (missing mapping/pack/scene) reached the `fallbacks` probe, so a model that resolved
  but whose textures 404'd read as a clean scene to `debug_snapshot` — the exact signal `jgengine-verify`
  tells agents to trust. The new `probes.textureErrors` list (`{ url, count }[]`) makes those failures
  visible so a texture-404'd scene can be treated like a model fallback. Dev-only (armed with devtools);
  a pure no-op in production.
- **Result/option types of public barrel functions are now re-exported (#1319).** The
  `@jgengine/core/gameplay` barrel re-exports `ChargeResult`, `ChargeOptions`, and `Overdraft`
  alongside `charge`/`chargeAll`/`canAfford`, and `@jgengine/core/combat` re-exports
  `DefenseResolution` (from `resolveDefense`) and `ResolvedShot` (from `resolveShot`). Consumers can
  now name these return/parameter types directly instead of resorting to `ReturnType<>`/`Parameters<>`
  gymnastics. Purely additive — no runtime or signature change.
- **Conflict-aware key-rebinding session.** `@jgengine/core/input/rebindSession` adds
  `createRebindSession({ actions | input, overrides?, now? })` — an observable key-remap
  editor over the existing action-binding model: it tracks the effective binding per action
  (default merged with `applyBindingOverrides`), exposes `rows()` with key glyphs + per-row
  conflict sets, groups every clash via `conflicts()`, drives click-to-capture rebinds
  (`beginCapture`/`capture`/`cancelCapture`, codes normalized), resets to defaults, and hands
  back `BindingOverrides` to persist, with `snapshot`/`restore`. React `KeybindingMenu` +
  `useRebindSession` (`@jgengine/react`) are the drop-in controls-settings surface — one row
  per action with key glyph, conflict badges, per-row and Reset-all, HudTheme-skinnable.
  Demo: `key-rebinding`.
- **Vendor / shop stock + grid.** `@jgengine/core/economy/shopStock` adds a serializable, observable
  `createShopStock({ entries })` — entries carry a free-string `kind`, a `price` in a free-string
  `currency`, a finite or unlimited (`qty: null`) count, and an optional `sellPrice`. `buy`/`sell`
  operate over a **caller-owned** `WalletState` (reusing the existing `wallet` model — `charge`/`grant`/
  `canAfford`), returning the debited/credited wallet for the caller to adopt; plus `restock`/`setPrice`/
  `add`/`remove`/`list`/`get`/`canAfford`/`subscribe`/`snapshot`/`restore`. `@jgengine/react`'s `ShopGrid`
  host + `useShopStock` hook render it as a token-themed grid of item cards (icon, price with currency
  glyph, stock count or "∞", afford-aware Buy, optional Sell) with a wallet balance readout.

- **Save-slot / profile select menu.** `@jgengine/core/game/saveSlots` adds `createSaveSlots(config)` — a
  serializable, observable index of per-slot *display* metadata (`{ id, name?, empty, savedAt?, meta }`
  with free-string `meta` the game fills: level, playtime, chapter, thumbnail ref, …) that complements
  `createSaveStore` (which owns the real payload). Ops: `write`/`clear`/`rename`/`get`/`list`/`mostRecent`
  (powers Continue) plus `subscribe`/`snapshot`/`restore`. `@jgengine/react/saveSlots` ships the drop-in
  `SaveSlotMenu` host (+ `useSaveSlots` hook) rendering the index as New / Continue / Load / Delete cards
  with meta chips and relative save times, HudTheme-skinnable. Demo: `save-slots`.
- **Event-log / kill-feed ticker.** `@jgengine/core/game/eventTicker` adds a thin, serializable,
  observable `createEventTicker({ now?, limit?, ttlMs? })` over the existing `appendFeed`/`pruneFeed`
  helpers: a single rolling, count-capped, time-fading list of free-string `{ kind, text, icon? }`
  entries (`push`/`recent`-with-`fade`/`entries`/`clear`/`subscribe`/`snapshot`/`restore`). `recent()`
  prunes expired entries and returns them newest-first with a `fade` `0..1` (age / `ttlMs`). The React
  `KillFeed` host + `useEventTicker` hook render it as a fading, newest-on-top stack of per-kind
  iconned, accent-colored rows over HudTheme tokens; `kill-feed` demo included.
- **Interaction prompt registry ("Press E to …").** `@jgengine/core/world` adds `createPromptRegistry()` — a thin
  observable, serializable store over the existing `resolveActivePrompt` resolver that owns positioned proximity
  prompts (`register`/`update`/`unregister`/`clear`/`all`), resolves the nearest in-range prompt as the player moves,
  and notifies subscribers only when the active prompt *changes* (so a HUD does not thrash per frame), with
  `snapshot`/`restore`. React `@jgengine/react` adds `InteractionPrompt` (a screen-anchored callout rendering the active
  prompt — key cap + label, gauge hold bar, or plain label, theme- and per-prompt-accent skinnable) and
  `useInteractionPrompt(registry, playerPosition)`. Demo: `interaction-prompt`.
- **Seeded trauma-based camera shake.** `@jgengine/core/vfx/cameraShake` adds `createCameraShake(config?)` —
  a serializable, deterministic camera-shake/impulse controller: `add(amount, kind?)` raises trauma `0..1`
  on impacts (free-string `kind` the game styles), `update(dt)` decays it, and `offset()` returns a pooled
  `{ x, y, z, pitch, yaw, roll }` kick (`trauma^exponent` × per-axis maxima × seeded value-noise) with
  snapshot/restore. `@jgengine/react` ships `CameraShakeMeter`/`useCameraShake` (trauma meter + kind label)
  and `@jgengine/shell` ships `ControllerCameraShake`, an R3F consumer that applies the offset to the active
  camera each frame so the view visibly shakes. Demo: `camera-shake`.

- **Observable wave/spawn runner + drop-in HUD.** `@jgengine/core/ai/waveRunner` adds
  `createWaveRunner(config)` — a thin, stateful, observable wrapper over the seeded `spawnDirector`
  that owns a `SpawnDirectorState`, ticks it from `update(dt, ctx?)`, forwards each `SpawnRequest` to an
  optional `onSpawn` sink (so the model never instantiates entities), and exposes a pooled `view()`
  readout (1-based `WAVE N`, wave progress `0..1`, budget/alert, spawned-this-wave/total, done) plus
  `forceNextWave`/`raiseAlert`/`subscribe`/`snapshot`/`restore`. `@jgengine/react/waveHud` adds
  `WaveHud`/`useWaveRunner` — a theme-skinnable panel with a big WAVE N label, wave-progress bar, and
  spawn/budget/alert readouts. Spawn-entry kinds stay free strings the runner never interprets.
- **Count-based combo / multiplier meter.** `@jgengine/core/combat/comboMeter` adds
  `createComboMeter({ windowMs, tiers?, dropStep?, multiplierPerTier? })` — an integer hit chain that
  climbs on `hit(kind?)`, resets a decay window each hit, and drops (to 0, or by `dropStep`) when the
  window elapses, driven by an injected `now` and/or `update(dt)`. Free-string `tiers` derive the active
  `tier()` and a score `multiplier()`, with `peak()`, a pooled `view()`, `subscribe`, and
  `snapshot`/`restore`. React `@jgengine/react/comboMeter` ships `ComboMeterHud` (big live count, tier
  label, draining window bar, multiplier — per-tier colored from a caller map) and a `useComboMeter` hook.
- **Off-screen objective / waypoint markers.** `@jgengine/core/ui/screenMarkers` adds a serializable,
  observable `createWaypointTracker()` (`set`/`remove`/`clear`/`all`/`subscribe`/`snapshot`/`restore`,
  free-string `kind`s the game styles) plus a pure, allocation-aware `layoutScreenMarker(projection,
  viewport, options?)` that passes an on-screen point through and clamps an off-screen or behind-camera
  point to the viewport edge with a bearing `angle` — the edge-clamp/arrow half that `layoutEntityFrames`
  culls. `@jgengine/react`'s `WaypointMarkers` renders on-screen pins and off-screen directional arrows
  with distance labels over any caller-owned `project` (e.g. shell `useWorldProjection`), skinnable via
  HudTheme tokens and a per-`kind` color map. Demo: `waypoint-markers`.

- **Scoreboard / leaderboard ranking.** `@jgengine/core/game/leaderboardRank` adds `rankLeaderboard(rows, options)`
  — a pure, allocation-bounded selector that turns raw leaderboard rows (accepts `LeaderboardRow[]` straight from
  `createLeaderboard().snapshot()`) into a render-ready ranked table: stable value sort (`desc`/`asc`), correct tie
  handling (`standard` → 1,2,2,4; `dense` → 1,2,2,3), `isTie`/`isLocal` flags via `highlightUserId`, and top-N `limit`
  — plus `medalFor(rank)` returning free-string `gold`/`silver`/`bronze` podium tokens. `@jgengine/react`'s reskinnable
  `Scoreboard` table renders it with medal-colored podium icons, a highlighted local row, and HudTheme `--jg-*` tokens.
- **Talent/upgrade tree from any unlock rule.** `@jgengine/core/game/talentTreeView` adds
  `talentTreeViewFrom(nodes, status, totals?)` — a general builder that places a node graph (branch/tier
  layout, prerequisite edges, learned/available/locked/maxed state) from a caller-supplied per-node
  `{ rank, allocatable }`, so unlocks can come from a currency threshold, a level, a quest flag, or
  nothing — not only point-spend. `talentTreeView(nodes, tree)` is now the point-spend adapter over it.
  The React `TalentTree` widget accepts a precomputed `view` (plus `showPoints`) alongside the existing
  `nodes`+`tree`, so the same widget renders a buy-with-points talent tree *or* a money-gated upgrade tree
  with no renderer change.
- **Floating combat text / damage numbers.** `@jgengine/core/ui/floatingText`' `createFloatingTextField` is a
  genre-agnostic, deterministic, allocation-aware field of world-anchored text pops (damage, crits, heals, XP/gold,
  status, barks): `emit({ position, text, kind?, color?, size?, rise?, drift?, lifetime? })`, `update(dt)` (rise +
  seeded drift + fade + swap-remove reaping), `active()` views, a capped pool that recycles the oldest, `clear`,
  `subscribe`, and serializable `snapshot`/`restore`. `kind` is a free string the presentation styles — no combat
  coupling. New `@jgengine/react`: `layoutFloatingText` (pure project/cull/depth-sort over the `EntityFrames`
  projector seam) and `FloatingText` (per-`kind` skinnable overlay). New `@jgengine/shell/vfx/WorldFloatingText`
  binds the live R3F camera and advances the field. New `floating-text` demo.
- **Codex / bestiary.** `@jgengine/core/game/codex`' `createCodex` tracks discovery over a fixed set of
  defined entries — `discover`, `isDiscovered`, category-filtered `list`, `categories`, `discoveredCount`/
  `total`/`completion`, an `onDiscover` seam, secret masking, and serializable `snapshot`/`restore`; the view
  list keeps a stable identity for React. New `@jgengine/react` `Codex` gallery: category tabs, discovered vs.
  locked cards (secret entries masked until found), and a completion header. First adopter: the apps/dev
  `codex` demo.
- **Generated street elevation** (`@jgengine/core/world/streetGenerator`) — new optional `elevation` (0..1 relief dial, default 0 = flat, byte-identical output) and `maxGrade` (default 0.07) rules: a seeded smooth field emits per-point `Street.heights` (grade-capped, loop-continuous across a circuit's start/finish) and a shared `StreetNetwork.elevationAt(x,z)` so renderers drape roads, junction welds, sidewalks, and building bases off one consistent surface. Distinct from `context.heightAt` terrain (bridges/tunnels unchanged).
- **Per-corner radius classes on circuits** (`@jgengine/core/world/streetGenerator`) — circuit corners now fillet from a seeded radius mix (hairpins near `minCurveRadius`, standard corners at 2-4x, 1-3 sweepers per lap at 5-8x, inversely correlated with turn magnitude) so track layouts show genuinely different corner radii instead of one pinched minimum.
- **`compactness` circuit dial — space-filling kart-style tracks** (`@jgengine/core/world/streetGenerator`) — 0..1 (default 0 = the open flowing hull loop, byte-identical): rising compactness lays the lap as the wall-follower cycle around a seeded direction-biased spanning tree on a corridor grid — provably one self-avoiding loop that folds back through its own footprint with parallel corridors at a clearance-safe pitch, switchback mazes, leaf hairpins, and a ≥3-pitch main straight — then runs the same spline/curvature-floor pipeline. At 1 a default-seed lap triples in length with 19-37 corners and ~61-77% footprint usage.
- **`blockFill` density dial** (`@jgengine/core/world/cityGenerator` / `buildingLots`) — 0..1 (default 0.45): rising fill packs street frontage toward a touching streetwall, deepens plots toward the block spine, and reserves fewer whole blocks as parks — Manhattan-dense cities at 1, sparse suburbs near 0. Block interiors are never filled with street-less buildings; every plot fronts a street.
- **Plot-first city generator** (`@jgengine/core/world/cityGenerator`) — `generateCity` now extracts closed blocks from the street network's exact graph (`extractGraphBlocks` in `world/cityBlocks`) and subdivides every block's frontage into size-tiered polygonal PLOTS (`small`/`medium`/`large`/`grand` — many different plot sizes per city) via the new `deriveCityPlots`. `GeneratedCity` gains `plots` (`CityPlot[]`, `plots[i]` pairs with `lots[i]`) and `parks` (whole-block open space). The landmark cluster-merge pass and the street-less interior fill are gone: a landmark is just a grand plot (same pass, biggest tier — `content.landmarks` dials its share, `0` disables), building class now follows plot size (wide plots pull towers/slabs, narrow plots pull rowhouses/houses), and every building stands on street-fronting land by construction.
- **`outline` street dial** (`@jgengine/core/world/streetGenerator`) — 0..1 (default 0 = the legacy full-rectangle footprint, byte-identical): rising values clip the lattice to a seeded organic boundary blob (radial harmonics), so every seed grows a differently-shaped city — lobes, bays, shaved corners — reduced to its largest connected component. `generateCity` defaults it to 0.4.
- **Residential branch streets and cul-de-sacs** (`@jgengine/core/world/streetGenerator` / `cityGenerator`) — new `residentialBranches` rule (0..1 share, default 0 = legacy alleys byte-identically) grows two spur kinds off the mains under the `branching` dial: service alleys (the legacy lane stubs) and RESIDENTIAL BRANCHES — winding 1–3-segment street-level side streets that may fork once, never cross existing roads, end in a turning-bulb cul-de-sac, and are lined with small/medium house plots down both sides by the plot pass (dead-end corridors carry their street/level through `extractGraphBlocks`). Dead-ending chains are never classified arterial, so no bulb-capped boulevards. `generateCity` defaults `branching` to 0.4 and `residentialBranches` to 0.6.
- **`TrimmedIntersections.junctionIndices`** (`@jgengine/core/world/roads`) — each welded junction surface now reports which input junction it belongs to, so renderers can color crossing patches by the junction's street level instead of one global junction color.
- **`trimBandAtJunctions`** (`@jgengine/core/world/roads`) — clip a sidewalk/parallel band polyline out of every junction apron (arm-derived radius widened by the band's half-width + clearance), returning the surviving sub-paths — so sidewalks end at crossings instead of sailing through them. The playground consumes it for `Street.sidewalks`.
- **Shared map drawings (multiplayer).** `@jgengine/core/world/sharedAnnotations`' `createSharedAnnotations`
  makes a map annotation layer collaborative: local `addStroke`/`addShape`/`addNote`/`remove`/`clear` apply
  then broadcast a serializable edit over the party feed (the same seam `createPingSystem` pings ride), and
  `apply` mirrors inbound edits from other players while dropping our own echoes. Ids are globally unique per
  client (`owner:n`) so two players' strokes never collide; transport-agnostic (`ANNOTATION_FEED_ACTION` +
  any replicated feed). Pings were already shared via `createPingSystem`; this completes the pair. Verified by
  a two-client sync test.
- **Terrain surface shading + layered grass wind** (`@jgengine/shell/terrain`, #1373) — plain (untextured) ground now reads as terrain instead of a two-tone gradient: `buildGroundGeometry`-based grounds (`ProceduralGround`, field ground) gain slope-aware darkening/desaturation and seeded, deterministic hash mottling, tunable via the new optional `TerrainSurfaceColorOptions` (`FieldGroundOptions.surface`; defaults in `DEFAULT_TERRAIN_SURFACE_COLOR`, strengths `0` restore the old flat lerp). Grass wind gains a layered rolling gust field (`GrassWindOptions.layered`, default on — three phase-offset directional waves so visible fronts sweep the meadow; `false` restores the single sine) plus a warm lit-tip sheen, and the default grass distance fade widens from 35–95 m to 55–150 m so meadows read expansive at no extra instance cost. `createProceduralGroundGeometry` now delegates to the shared ground builder (behavior-identical geometry, minus the duplication).
- **Debounced control commits: `useDebouncedCommit`** (`@jgengine/react/useDebouncedCommit`, #1372) — a live-mirrored, trailing-debounced commit binding for slider/number/color/text controls whose commit path is expensive (scene-document patches that regenerate streets, scatter, or grass). The control renders from the local mirror for instant thumb/readout feedback; the real commit fires once per pause (default 180 ms) and flushes on release/blur/unmount; external changes (undo, RPC echo) resync the mirror unless a drag is in flight. The editor's inspector fields (`SchemaInspector`, `SliderRow`/`NumberField`, vegetation and scatter-coverage density) and the shell settings sliders now commit through it, so one drag lands one patch instead of ~50.
- **Stage / objective banner.** `@jgengine/core/ui/objectiveBanner`' `createObjectiveBanner` is a genre-agnostic,
  serializable announcement queue: a game calls `announce({ title, subtitle?, kind?, holdMs?, inMs?, outMs? })` and the
  controller flies the classic transient centered title stamp ("WAVE 3", "VICTORY", "OBJECTIVE COMPLETE") in, holds it,
  and fades it out — one banner at a time — on an injected clock (`advance()`), exposing the current banner plus its
  phase (`in`/`hold`/`out`) and `progress` `0..1` for a renderer to animate. `kind` is a free label the game styles; the
  model never interprets it. Allocation-aware (the `current()` view is pooled) with `subscribe` + `snapshot`/`restore`.
  `@jgengine/react`' `ObjectiveBannerHost` subscribes, drives the clock per frame, and maps phase + progress to
  opacity/scale/translate, HudTheme-skinnable and colorable per `kind` via `kindThemes`. Demo: `objective-banner`.
- **Talent / skill-tree widget.** `@jgengine/react`'s `TalentTree` is a drop-in widget over the existing
  talent model (`@jgengine/core/game/talents`): pass the node definitions plus a live `createTalentTree`
  instance and it lays nodes out by branch column and prerequisite-depth tier, draws SVG prerequisite
  edges, styles each node learned/available/locked/maxed with an icon + rank badge, and fires
  `onLearn(nodeId)` on allocatable clicks. A new thin core selector `@jgengine/core/game/talentTreeView`
  (`talentTreeView`) flattens the model into a serializable per-node render view (tier, state, met/unmet
  edges) so the widget never re-derives topology or eligibility. HudTheme-token skinned; node ids and
  branches stay opaque game data.
- **Countdown / timer HUD.** `@jgengine/core/time/timerSet`' `createTimerSet` is a serializable set of named
  countdown/countup timers on an injected clock — one primitive for round timers, respawn clocks, and ability
  cooldown/charge (identical mechanics; `id`/labels are free strings the engine never interprets). Start, pause,
  resume, stop, reset, and read `{ remainingMs, elapsedMs, durationMs, progress01, running, expired }` per timer
  (allocation-aware `read(id, out)`), observe structural changes via `subscribe` and expiry edges via
  `poll`/`onExpire`, and `snapshot`/`restore` round-trip through a save. `@jgengine/react`'s `TimerReadout`
  (live digital mm:ss/m:ss.d) and `TimerRing` (SVG radial fill/drain), plus the `useTimerRead` per-frame hook,
  render it HudTheme-skinned — no hand-rolled interval math. See the `countdown-timer` dev demo.
- **Branching dialogue UI.** `@jgengine/core/game/dialogueGraph`'s `createDialogueRun` / `selectDialogueView` add a
  thin, serializable branching-conversation model over the existing `features.dialogue` open/close bridge — a graph of
  nodes (free-string speaker + line + choices that name the node they lead to) with choose-to-advance traversal, visited
  history, and snapshot/restore. `@jgengine/react`'s `DialogueView` (+ `useDialogueRun`) is the drop-in view: speaker
  name, an optional portrait slot, the current line, and clickable response buttons that advance the run — a game passes
  its dialogue graph and gets working node traversal and choice state with no hand-rolled walk. Genre-agnostic:
  speaker/choice `kind` are free strings the model never interprets, surfaced as `data-*` for game styling; HudTheme-skinned
  via `--jg-*` tokens. See the `dialogue` demo.
- **Damage-direction indicators.** `@jgengine/core/vfx/damageDirection`' `createDamageDirectionTracker` is a
  serializable, allocation-aware "hit-from" brain: `registerHit({ angle, intensity?, kind? })` (angle in radians,
  `0` = front, free-string `kind` never interpreted) turns each hit into a directional indicator that fades over a
  duration on an injected clock, with `active()` reporting live indicators at eased current intensity, optional
  same-direction merging, a bounded pool, `subscribe`, and `snapshot`/`restore`. `@jgengine/react`'s
  `DamageDirectionOverlay` (+ `useDamageDirection` hook) renders the classic red arcs flaring around a center reticle
  toward each recent hit, opacity/scale from intensity, color per `kind`/HudTheme, `pointer-events: none`. New
  `damage-direction` dev demo. Genre-agnostic, renderer-free core.
- **Turnkey day-night cycle.** `@jgengine/core/time/dayNightCycle`' `createDayNightCycle` is a genre-agnostic,
  serializable brain that advances a normalized day fraction on an injected clock and blends per-keyframe phase labels
  and tint/light colors (`DayNightKeyframe` → `DayNightSample`), with `pause`/`play`/`setSpeed`/`setDayFraction`,
  `subscribe`, `snapshot`/`restore`, and a `calendar()` adapter so it drops straight into any `{ dayFraction }` sky
  seam. The shell's `@jgengine/shell/environment` adds `DayNightSky`, a drop-in R3F presenter that drives the existing
  `SkyDome` shader and lights from the model each frame — wire one thing for a moving, color-graded day-night sky
  instead of hand-rolling a clock plus a color lerp. `phase`/`kind` strings stay free-form; the model never interprets
  them. New `day-night` dev demo.
- **Status-effect timeline HUD.** `@jgengine/react`'s `StatusEffectBar` renders active buffs/debuffs as a row of
  painted icons, each with a radial countdown ring and stack-count badge, driven off the existing status model
  (`combat/statusApplication`'s `StatusInstance`). A thin genre-agnostic core selector,
  `@jgengine/core/combat`'s `toStatusEffectViews` / `toStatusEffectView` (+ `statusEffectRemainingFraction`),
  adapts live `StatusInstance`s into serializable `StatusEffectView`s (`id`, free-string `kind`, `remainingMs`,
  `durationMs`, `stacks`) so the widget re-derives no ring math or timers. `kind` is never interpreted by the engine —
  the game supplies icon/color/label; icons come from the existing game-icons.net icon system. Demo: `status-effects`.
- **Cutscene / sequence director.** `@jgengine/core/scene/sequenceDirector`' `createSequenceDirector` is a
  serializable, genre-agnostic timeline: an ordered list of typed cues (`{ atMs, kind, payload }`) advanced by one
  injected clock, firing each cue once and in order as its time passes — even across a large `seek` — with
  `play`/`pause`/`seek`/`skip`/`stop` and `snapshot`/`restore`. The director only schedules and emits cues via
  `onCue`; it never interprets a `kind`, so the same primitive drives camera moves, dialogue, fades, or any game
  event without a genre kit. `@jgengine/react`'s `useSequenceDirector` runs the per-frame tick loop and exposes
  playhead/progress + controls, and `CutsceneLetterbox` is a reskinnable cinematic bars + caption + Skip overlay.
  See the `cutscene` dev demo.
- **Modal / confirmation dialog system + reskinnable pause menu.** `@jgengine/core/ui/modalStack`'s
  `createModalStack` is a genre-agnostic, serializable stack of opaque modal records (`push`/`pop`/`resolve` with a
  free-form result, `top`/`isOpen`/`depth`, optional injected-clock auto-dismiss via `tick`/`timeRemaining`,
  `subscribe`, `snapshot`/`restore`) that never interprets a modal's `kind` or result. `@jgengine/react`'s `ModalHost`
  renders the top modal over a dimmed backdrop with a focus trap and Esc/backdrop-to-cancel, `ConfirmDialog` is a
  generic two-button confirm/cancel dialog, and `PauseMenu` is a drop-in Resume + game-filled Settings/Quit slot list.
  All `HudTheme`-token-driven common parts — the game owns final layout, terminology, and skin. Demo: `pause-menu`.
- **Screen-state effects (postfx).** `@jgengine/core/vfx/screenEffects`' `createScreenEffects` is a genre-agnostic,
  serializable screen-feedback controller: a game triggers transient full-screen flashes and edge vignettes
  (`flash`/`vignette`) or sustained, optionally oscillating tints (`pulse`, e.g. a low-health breathe) — each a
  free-string `kind` the game styles, plus color, peak intensity, easing, and duration. It is clock-driven
  (`advance()` against an injected `now`) and allocation-aware (the `composite()` array and entries are pooled, so
  steady-state ticking never allocates), with `subscribe` + `snapshot`/`restore`. `@jgengine/shell/postfx`'
  `ScreenEffectsOverlay` subscribes to the model and paints the composite as pointer-transparent color-grade layers
  over the canvas. Demo: `screen-effects`. The model never interprets `kind`; flash/vignette/pulse are just
  parameterizations of the same data (region shape, decay easing, sustained oscillation).
- **Coach-marks / tutorial hints.** `@jgengine/core/ui/coachMarks`' `createCoachMarkSequence` is a genre-agnostic
  onboarding model: an ordered list of `CoachMarkStep`s (title, body, optional `anchor`/`placement`, and a data-first
  string `condition`), a persisted "seen" set so completed hints never re-show, condition-gating via `satisfy`/
  `setSatisfied`, `current`/`advance`/`dismiss`/`skipAll`, `isComplete`/`remaining`, `subscribe`, and serializable
  `snapshot`/`restore`. New `@jgengine/react`: `useCoachMarks` plus `CoachMark` and `CoachMarkHost` (anchored or
  centered callout with an "N of M" counter, a dimmed spotlight backdrop, Next/Skip, and a `CoachMarkTheme` reskin).
  New `coach-marks` demo.
- **Fast-travel network.** `@jgengine/core/world/fastTravel`' `createFastTravelNetwork` is a genre-agnostic
  discovered-destinations model: game-defined `TravelPointDef`s (world-XZ position, region, icon, `initial`)
  with per-player discovery tracking — `discover`, `isDiscovered`, distance-sorted `list(from)` and
  `destinations(from)`, `nearest(from, { exclude })`, a `canTravel` gate, `discoveredCount`/`total`, an
  `onDiscover` seam, and serializable `snapshot`/`restore` (re-seeds `initial` points). New `@jgengine/react`:
  `useFastTravel` (re-render on discovery) and `FastTravelMenu` (region-grouped, distance-labeled destination
  picker with a discovery counter and "you are here" flag). New `fast-travel` demo.
- **Generic particle system.** `@jgengine/core/vfx/particles`' `createParticleSystem` is a genre-agnostic,
  deterministic, allocation-aware particle emitter: a fixed pool, data-only `EmitterConfig` (rate, position,
  lifetime, speed, cone `spread`, `gravity`, `drag`, size/color/alpha over life), burst `emit(n)` and continuous
  `rate`, `update(dt)` integration with swap-remove reaping, Structure-of-Arrays render `buffers()` (positions,
  sizes, colors, alphas) clamped to the live count, injected-seed determinism, and serializable `snapshot`/
  `restore`. New `@jgengine/shell/vfx/ParticleField` renders it as a one-draw-call soft-point GPU cloud with
  per-particle size/color/alpha (additive or normal blending). New `particle-vfx` demo (fire / smoke / sparks).
- **Notification center.** `@jgengine/core/game/notifications`' `createNotificationCenter` is a persistent,
  read-tracked notification log (newest-first, capped, serializable, observable) — the durable counterpart to
  transient toasts: `push`, `list({ kind, unreadOnly })`, `unreadCount`, `markRead`/`markAllRead`, `remove`,
  `clear`, `snapshot`/`restore`. New `@jgengine/react`: `useNotifications`, `NotificationBell` (icon + unread
  badge), and `NotificationCenter` (scrollable panel with kind markers, relative time, unread highlight, and
  mark-all-read/clear). First adopter: the apps/dev `notification-center` demo.
- **Photo mode.** `@jgengine/core/ui/photoMode`'s `createPhotoModeStore` is a serializable, observable
  photo-mode state (active + hide-HUD) a game binds its capture flow to. New `@jgengine/react`
  `usePhotoMode` + `PhotoModeControls` (hide-HUD toggle, capture, exit). New `@jgengine/shell/render/
  sceneCapture`: `captureCanvas(gl)` reads the live frame to a PNG data URL (the shell canvas already sets
  `preserveDrawingBuffer`), `downloadImage` saves it, and `SceneCaptureBinding` (mount in `WorldOverlay`)
  hands the in-Canvas capture fn to the HUD's Capture button. The game reads `hideHud` to drop gameplay
  chrome for a clean shot. First adopter: the apps/dev `photo-mode` demo.
- **Welded street intersections** (`@jgengine/core/world/roads`, #1363) — `trimPathAtJunctions` cuts every road back to an arm-derived apron at each crossing (through-roads split in two) and `buildJunctionSurface` welds a curb-return-filleted junction polygon whose boundary vertices are bitwise-shared with the trimmed ribbon ends; `buildTrimmedIntersections` runs the whole pipeline for a street network in one call. Junctions read as real intersections instead of overlapping ribbons under a floating disc (`buildJunctionPatch` is deprecated but still works). The shell's authored-road and city renderers consume it out of the box.
- **`GROUND_DECAL_LAYERS`** (`@jgengine/core/world/roads`, #1366) — one owning table for ground-decal elevations (terrain < road = junction < marking < glow) replacing scattered per-callsite Y epsilons; ribbon/junction builders default from it, and markings/glow overlays pair it with renderer-side `polygonOffset` so road surfaces stop z-fighting at distance.
- **Real curves and sidewalks in the street generator** (`@jgengine/core/world/streetGenerator`, #1364, #1368) — corners are now sampled circular-arc fillets honoring `minCurveRadius` instead of single bevels; street hierarchy comes from sampled betweenness centrality with an arterial-connectivity repair pass instead of a length percentile; and boulevard/avenue/street chains carry `sidewalks` offset polylines (new optional `sidewalkWidth` rule, default 2).
- **Race-track synthesis in circuit mode** (`@jgengine/core/world/streetGenerator`, #1365) — circuits are grown from seeded scatter → convex hull → inward midpoint displacement with chicane/ess/hairpin corner templates, an enforced start/finish straight, deterministic self-clearance retries, and a pit lane that leaves and rejoins the loop at two real junctions — an actual track layout instead of a wobbled ellipse.
- **City lot content resolution** (`@jgengine/core/world/cityGenerator`, #1367) — `resolveCityLotContent` (or `generateCity`'s new `content` option) enriches frontage lots with their zone band, building class from cityContent's weighted mixes (street-level-biased), floors, and massing pieces; a deterministic landmark pass merges clusters of adjacent lots into a few block-scale `hall`/`arena`/`market`/`campus` buildings (dial: `content.landmarks`, default 0.04). `GeneratedCity` gains an optional `lotContent`; the bare-lot path is unchanged.
- **Quick menu takes many forms.** The radial menu grows into a full quick-menu family. `@jgengine/core/ui/radialMenu`
  gains a `RadialArc` (`startAngle`/`sweep`) so the wheel can be a partial **arc** (bottom half, quarter, ping bar)
  with bounds-aware selection. `@jgengine/react`'s `RadialMenu` now renders per-slice **hotkeys, count badges,
  cooldowns, and submenu markers**, and takes an `arc`. New `@jgengine/react` **`QuickMenu`** presents one item
  model in four forms — `radial` · `arc` · `list` · `grid` — with section headers, hotkeys, badges, cooldowns,
  disabled states, and **nested submenus** (drill in / back). First adopter: the apps/dev quick-menu demo (form
  switcher across all four).
- **Per-context touch control modes** (`@jgengine/core/input/touchScheme` `TouchControlsConfig.modes` + `@jgengine/core/input/touchControlsMode` `setTouchControlsMode`/`activeTouchControlsMode`, re-exported from `gameplay`, #1370) — a game whose verbs change at runtime (on foot vs driving vs flying) declares one named touch config per context; each mode's fields replace the matching base fields and gameplay switches with `setTouchControlsMode(ctx, "car")` / `null`. The shell re-derives the dock the same frame, so a runner on foot never sees flight or vehicle chrome. Blind derivation past 8 buttons now logs a dev warning naming the curation seams. Vice Isle (on-foot/car/aircraft) is the first adopter.
- **Analog action values through the input snapshot** (`@jgengine/core/runtime/inputSnapshot` `publishAnalog`/`value`/`analog`, `InputFrame.analog`, #1370) — a continuous source (the virtual joystick today, gamepad sticks tomorrow) publishes per-action magnitudes (0..1) alongside the digital held set; `ctx.input.value(action)` reads them, `ctx.input.axis(...)` samples them per bound action (so steer/pitch/roll bound to action names become proportional automatically), and the walk controller consumes them via `resolveMovementIntent`'s new analog path — a slight stick tilt walks slowly in that direction at speed scaled by deflection, instead of slamming a full-speed digital strafe. Serializable: analog rides `InputFrame` to an authoritative host unchanged.
- **Floating joystick variant + Settings selector** (`@jgengine/shell` touch dock, `@jgengine/core/input/touchScheme` `TouchJoystickVariant`, #1370) — two shipped stick behaviors, player-selectable under Settings → Controls → "Joystick": `floating` (default; the dock corner is a capture zone with a faint resting ring and the stick spawns centered under the thumb) and `fixed` (classic always-visible stick). Both share the analog vector math with a radial deadzone (travel rescaled from 0.16), replacing the old per-axis 0.42 digital thresholds as the movement path.
- **Radial / quick menu (weapon/emote wheel).** `@jgengine/core/ui/radialMenu` ships the pure geometry —
  `radialSlices`, `radialIndexFromAngle`, `radialIndexFromVector` (pointer/stick vector → slice with a
  neutral dead zone), `radialSlicePosition` — with slices centered from "up" clockwise. New `@jgengine/react`
  `RadialMenu` renders the wheel: pointer angle drives the highlight, the neutral hub closes, click/confirm
  fires `onSelect`; the game supplies option icons/labels and skins it (`highlightIndex` for controller/test
  control). First adopter: the apps/dev `radial-menu` demo.
- **Accessibility options.** `@jgengine/core/ui/accessibility`'s `createAccessibilityStore` is a
  serializable, observable store of genre-agnostic accessibility preferences — reduced motion, high
  contrast, `textScale` (clamped), colorblind mode, and captions — with `snapshot`/`restore`. Ships the
  `COLORBLIND_MATRICES` (`feColorMatrix` values for protanopia/deuteranopia/tritanopia/grayscale),
  `clampTextScale`, and a `reducedMotionDuration` animation gate. New `@jgengine/react` layer:
  `AccessibilityProvider` (exposes a `--jg-text-scale` CSS var, sets `data-reduced-motion` /
  `data-high-contrast` / `data-colorblind` / `data-captions`, and wraps the subtree in the selected
  colorblind filter), `useAccessibility`, `usePrefersReducedMotion` (OS `prefers-reduced-motion`), and
  `ColorblindFilters`. Input rebinding already existed; this fills the rest of the a11y surface. First
  adopter: the apps/dev `accessibility` demo.
- **Quest / objective tracker HUD.** `@jgengine/core/game/quest`'s `describeTrackedQuest(def, instance)`
  joins a static `QuestDef` with a player's live `QuestInstance` into a flat, renderer-free `TrackedQuestView`
  (title, status, labelled objective progress — `defaultObjectiveLabel` derives "Defeat 5 Wolves" and can be
  overridden). New `@jgengine/react` `QuestTracker` draws it as a compact HUD panel: per-objective checkmark +
  strike on complete, `progress/count`, and a thin progress bar, with `maxObjectives` truncation. A widget over
  the existing quest models — no runtime coupling to `QuestJournal`. First adopter: the apps/dev `quest-tracker`
  demo.
- **Localization / i18n.** `@jgengine/core/i18n` (also on the `@jgengine/core/ui` barrel)'s `createI18n`
  translates a message `Catalog` with active-locale lookup, a fallback-locale chain, `{param}`
  interpolation, and `Intl.PluralRules`-based `plural(key, count)`; it's observable so `setLocale` re-renders
  bound UI, and the catalog stays caller-owned static data. New `@jgengine/react` layer: `I18nProvider`,
  `useT()` / `usePlural()` / `useLocale()` hooks, and a `<Trans k=… />` component — every HUD string binds to
  the catalog and swaps live on language change. First fully-absent-system gap closed; genre-agnostic, no
  content shipped. First adopter: the apps/dev `localization` demo (EN / ES / 日本語 switcher).
- **Click-to-place custom marker kinds** (`@jgengine/core/scene` `definePlaceableMarkerKind` + `@jgengine/editor`) — a game declares its own logical marker kind (a stash, checkpoint, bounty…) in one call and it becomes a click-to-place tool in the editor's `+ Add` menu, grouped under its `category`: pick it, click the world to drop a marker, Shift-click to keep placing, tune its `fields` in the Inspector. The light path skips a full studio's schema/resolver boilerplate; the runtime is unchanged (the game still reads the kind off `editorLayers.markers`). The Add menu now groups registered kinds by their `addCategory` header instead of lumping every one under "Studios". Register from a module the editor loads (a game's `editorLayers` import graph). Pairs with the `add_marker` RPC verb: `definePlaceableMarkerKind` is the designer's click-to-place path, `add_marker` the scripted one.
- **`add_marker` editor verb** (`@jgengine/editor`, #1360) — the one-shot counterpart to `add_path`: place a bare point marker (a spawn, checkpoint, pickup, bounty, or a game's own custom kind) at `x`/`z` (optional `y`) in a single `--rpc`/MCP call, no `export_document`/`import_document` roundtrip and no hand-editing `editor.scene.json`. `kind` is required; `color`/`label`/`rotationY`/`meta` are optional and `meta` is validated against the kind's registered schema when one exists. Re-ids on a document-global id collision and returns the landed id, exactly like `add_path`. Use `place_asset` when the marker should carry a mesh; `add_marker` is for logical, mesh-free markers a game reads off the document by kind.
- **World-anchored entity frames (overhead nameplates / health bars)** (`@jgengine/react/entityFrames`, `@jgengine/shell/world/WorldEntityFrames`, #1266) — a data-first, caller-owned seam for floating frames over entities. `EntityFrames` takes a plain `{ id, worldPosition, ... }[]` plus a `project` (world→screen) and a `renderFrame` that composes the shipped `HealthBar` + a name (reskinned via `barTokens`/`HudTheme`); it owns only the reusable behavior — projection offset, off-screen/behind-camera culling, and nearest-on-top stacking. The pure `layoutEntityFrames` core is unit-tested in isolation and imports no React DOM/Three.js. R3F games get the projector for free from `WorldEntityFrames` / `useWorldProjection` / `projectWorldToScreen` (`@jgengine/shell`), which sample the live camera and compose the react primitive — overhead enemy nameplates/health bars from a game's own entity array in a few lines, with no `GameProvider` or entity-store mirroring. See the `jgengine-ui` overhead-entity-frames recipe. Complements the store-driven `WorldNameplates`.
- **Per-entity render hide: `SceneEntity.hidden`** (`@jgengine/core/scene/entityStore`, `@jgengine/shell`, #1299) — a new optional render-only flag on scene entities, settable at `spawn` and via `update({ hidden })`. When `true` the shell's entity marker skips the visual entirely each frame while the entity keeps simulating (position, stats, AI targeting, queries untouched). Built for seated vehicle riders whose character model otherwise pokes through the vehicle body; equally fits cutscene actors and stealth cloaks. Absent/`false` renders exactly as before.
- **`ChaseCameraTuning` covers the full driving-feel surface** (`@jgengine/core/runtime/cameraDirector`, #1299) — the runtime `ctx.camera.setChaseTuning` patch now also accepts `shakePerSpeed` and `velocityYaw`, so a game can keep its static `camera.chase` block as a calm on-foot baseline and overlay the whole speed-reactive package (speed→FOV, lead, bank, speed shake, drift-lag) only while a vehicle is piloted. Existing tuning patches are unchanged.
- **Draw on the map: annotation layer + FullscreenMap draw tool.** `@jgengine/core/world/mapAnnotations`'
  `createAnnotationLayer` is a serializable player-drawing layer — freehand `strokes`, area `shapes`, and
  pinned `notes`, all world-XZ — whose `routes()`/`zones()` project straight into the `routes`/`zones` props
  `Minimap`/`WorldMap`/`FullscreenMap` already render, so drawing needs no new renderer; `snapshot`/`restore`
  round-trip through a save. `@jgengine/react/map`'s `FullscreenMap` gains a `tool` prop (`"pan"` | `"draw"`):
  in draw mode a pointer drag paints a freehand stroke committed to `onStrokeComplete(points)` (with a live
  in-progress preview via `drawTone`/`drawWidth`), while panning and click-to-place are suppressed. First
  adopter: the apps/dev extraction-map demo (Pan/Draw/Clear palette).
- **`jgengine shoot` / `jgengine drive` CLI verbs** (`jgengine` CLI, #1262) — the browser capture and play-driving rungs are now first-class CLI verbs, so a standalone project (scaffolded with `npx jgengine create`, outside this monorepo) can screenshot and playtest its own game without any monorepo-only tooling. Run inside a project that already ships `scripts/shoot.mjs` / `scripts/drive.mjs` and the verb delegates to them (parity with `bun run shoot`); run in one that lacks them (an older scaffold, or one whose scripts were removed) and it materializes the same dependency-free harness the scaffold embeds and drives the project's dev server. Chrome/Chromium is the only capture engine — no Playwright and no npm deps travel with the published CLI; a missing browser fails with an install hint. Makes the "visual claims are screenshot-judged" rule enforceable for the exact outside users it targets.
- **Engine preview-fixture capture route** (`@jgengine/react` `PREVIEW_FIXTURES`, #1272) — a registry of deterministic, engine-level preview fixtures (the real exported components `HudThemePreview`, `BarsPreview`, `IconsPreview`, …) keyed by name, with `previewFixtureNames()` and `resolvePreviewFixture(name)`. The dev runner mounts one from a `?fixture=<name>` URL overlay and `bun run shoot --fixture <name>` screenshots it with no game boot and no hand-rolled `--url` page; `bun run shoot --list` (or `--fixture` with no name) prints the registered set. Register any additional deterministic preview component in `PREVIEW_FIXTURES` and the route and CLI pick it up with no further wiring — genre-agnostic, additive.
- **Per-capture player-spawn override** (`@jgengine/core/world/spawnOverride`, #1255) — `installSpawnOverride`/`clearSpawnOverride`/`readSpawnOverride` and the pure `parseSpawnOverride("x,y,z"|"x,y,z,yaw")` install a session-scoped spawn that `authoredSpawnPosition`/`authoredSpawnRotation` honor for the *default* player-spawn resolution only (an explicit marker `id` or non-`player_spawn` kind still reads the document), so a close-up capture can relocate the player without editing `editor.scene.json`. The dev runner installs one from a `?spawn=` URL overlay (mirroring `?cam=`), and `bun run shoot`/`bun run drive` gain a `--spawn x,y,z` flag. Default is no override, so nothing changes until one is installed.
- **Aerial editor camera in one call** (`@jgengine/editor`, #1256) — `camera_goto` and `camera_frame` take optional `distance`, `pitch` (degrees above the horizon; 90 = straight-down), `yaw`, and `height`, and the `EditorFocusTarget` carries them so the orbit camera repositions instead of only panning. `camera_frame` with a `pitch` but no `distance` auto-fits the document bounds into view (`orbitCameraPosition`/`frameDistanceForBounds` are exported and unit-tested), so composing a district aerial no longer takes many guess-the-y-offset round-trips or buries the camera in terrain.
- **Achievements / trophies** — `@jgengine/core/game/achievements`' `createAchievementTracker`
  is a serializable tracker for counter-goal (`progress`/`setProgress`) and boolean (`unlock`)
  achievements, with `score()`, `completion()`, an `onUnlock` seam (wire to a toast queue, feed,
  or sound), and `snapshot`/`restore`; its view `list()` keeps a stable identity so React reads it
  through `useSyncExternalStore` without re-projecting each frame. New `@jgengine/react` presentation:
  `useAchievements(tracker)`, `AchievementGallery` (unlocked/locked grid with counter progress bars,
  secret masking, and a completion/score header), and `AchievementToast` (unlock banner). Genre-agnostic
  and additive — no defaults or content shipped.
- **World-space pings** (`@jgengine/shell/world/WorldPings`, `@jgengine/shell/world/pingPulse`) — the
  in-scene side of a ping/marker: a bobbing downward arrowhead pointing at the spot, a ground ring, and a
  billboarded callout (glyph + `meta.callout`/`label`), colored by marker kind and fading in/out over the
  marker's lifetime. Reads a `MarkerSet` (typically the one a `createPingSystem` writes to), a marker
  source, or a static array — mount through `PlayableGame.WorldOverlay`. `pingsOnly` (default true) limits
  it to `meta.ping` markers; `renderCallout` overrides the chip. The pure lifecycle math (`pingOpacity`,
  `pingBobOffset`) is exported and unit-tested. First adopter: the apps/dev `world-pings` demo.
- **Difficulty-aware AI driver** (`@jgengine/core/ai/driver`, #1311) — `driveStep(state, dt, pose, goal, profile, tuning, rng, obstacleAhead?)` turns a vehicle pose + goal into the shared `AxisInput` for `tickDrivableVehicle`, with a `DifficultyProfile` (#1301) deciding how well the car drives: a reaction delay-line makes low tiers chase where the target *was*, corners shed speed instead of full-throttle orbiting, obstacles are perceived late by `speed × reactionSeconds`, steering wobbles by `executionJitter` resampled on an interval, and a wall-grinding car reverses out with counter-steer after a tier-scaled delay. `pathTargetAhead` walks a road/route polyline to a pure-pursuit lookahead point so the same brain follows authored streets and circuits. State is one serializable object; randomness injected; no world scans. First adopter: vice-isle cruiser pursuit, whose cops now drive by wanted level (3★ sloppy rookies → 5★ sharp interceptors) instead of `steer = error*2, throttle = 1`.
- **Effect primitives: named hooks for the recurring `useEffect` shapes** (`@jgengine/react/hooks`, `@jgengine/shell/render/useDisposable`, #1298) — `useTicker(hz)` re-renders HUD elements at a steady rate for time-derived readouts (cooldowns, cast/swing bars); `useDomEvent(resolveTarget, type, handler, options?)` attaches a DOM listener with automatic cleanup and a stable handler ref; `useRafLoop(onFrame, active?)` runs a cancellable requestAnimationFrame loop with delta seconds; `useAutoScroll(dep)` pins a log/chat/console panel to its newest line; and shell's `useDisposable(create, deps)` (+ `disposeAll`) memoizes a three.js resource or tuple and disposes it on change/unmount, replacing the hand-rolled `useMemo` + dispose-effect pair. Prefer these over direct `useEffect` for their shapes — a lint restriction on direct `useEffect` outside the primitives layer follows in a later phase.
- **Three-tier AI decision quality** (`@jgengine/core/ai/difficulty`, #1301) — one serializable `DifficultyProfile` (reaction time, decision noise, execution jitter, ability discipline, perception scale, plan depth/width) owns how well any opponent decides, with canonical `easy`/`standard`/`expert` tiers (`DIFFICULTY_TIERS`, `difficultyProfile(tier, overrides)`) and deterministic appliers at the universal decision seams: `advanceReactionGate` (delay acting on new information), `pickScored` (noisy argmax over any caller-scored option list), `executionError` (aim/lead/timing fuzz), `shouldUseAbility` (spend-or-hold specials from a scored opportunity window), and `planLookahead` (depth/width-bounded negamax or own-sequence search over a game-owned `moves`/`apply`/`evaluate` domain — greedy at easy, multi-ply tactics at expert). Composes with the existing substrates: scale `MobBrainConfig.aggroRadius` by `perceptionScale`, route `acquireTarget` scores through `pickScored`, gate `advancePursuit` swaps behind the reaction gate.
- **Authored per-placement rig animation applies in game** (`@jgengine/core/world/authoredObjects`, `@jgengine/core/scene/objectStore`, #1276) — a placement's editor-authored `marker.meta.animation` (role→clip states, `"auto"`/`"none"`, walk/run/fade, one-shot bindings; the stable contract from #1274) now flows through `resolveAuthoredObjects` → `placeAuthoredObjects` → `ObjectStore.place` onto the placed object's `ModelConfig.animation`, so the override applies at play time instead of being write+preview only. `AuthoredObject.animation`, `PlaceOptions.animation`, and `SceneObject.animation` are new optional fields; the new `markerAnimation(marker)` reader exposes the contract. Markers with no override are unchanged — animation still comes from catalog resolution.
- **`EffectResult` carries the slain entity's identity** (`@jgengine/core/combat/effects`, #1263) — a lethal `ctx.scene.entity.effect()` hit now returns `slain: { catalogId, name?, userId? }` on the per-target result, captured before the death system despawns the target. Kill credit / XP reads the victim's `catalogId` (its spawn kind) straight off the result — no game-side spawn-time registry mirroring instance ids to kinds. Non-lethal hits omit `slain`. The new `EffectSystemDeps.resolveSlainIdentity` seam is optional and additive, so existing effect-system compositions keep their exact shape.
- Interactive fullscreen map + player waypoints. `@jgengine/core/world/waypoints`'
  `createWaypointStore` is a serializable player-waypoint layer (place/track/clear,
  snapshot↔restore) that mirrors pins into a `MarkerSet` so every map surface renders
  them, and reports bearing/distance `guidance` for an on-screen arrow. New React
  primitives in `@jgengine/react/map`: `FullscreenMap` (wheel-zoom/drag-pan overlay
  that never fires a click after a pan), `WorldMapSurface` (the viewport-aware map
  `<svg>` now shared by `WorldMap` and `FullscreenMap`), `MapLegend` (marker-kind key),
  and `WaypointArrow` (HUD guide needle). A new `waypoint` entry joins
  `DEFAULT_MARKER_KINDS`.
- Editor RPC/CLI verb `add_path` (`@jgengine/editor`): author a new path/route into the scene
  document from an ordered list of ≥2 `{x,z}` (optional `y`) points in one call, without an
  `export_document`/`import_document` roundtrip. `kind` defaults to `route`; `meta` is schema-validated
  like `set_path`, and a colliding `id` re-ids in the document-global namespace.

### Changed

- **`check-game-shape`/doctor accept editor-import-graph modules by convention** (`packages/jgengine` `gameShape`, #1377) — a top-level `src/editor<Name>.ts` module (plus its colocated `.test.ts`) is now an allowed game-shape extra, matched by the `editor<Name>.ts` naming convention instead of an exact-filename enumeration. Fixes `editorKinds.ts` tripping the shape gate and stops the next editor-graph module from re-reddening `main` the moment it lands. No game needs changes; `editorLayers`/`editorCatalogs`/`editorKinds` and any future sibling are all recognized.
- **Road ribbons no longer self-intersect at bends** (`@jgengine/core/world/roads`) — `buildRoadRibbon` now miter-joins the inner edge of a bend and welds any residual fold when the local turn radius drops under the half-width, so dense arc-sampled corners render as one clean surface instead of a doubled bowtie. Straight ribbons and terminal cross-sections stay byte-identical (junction welds unaffected).
- **Junction surfaces stopped emitting sliver triangles** (`@jgengine/core/world/roads`) — `buildJunctionSurface` grouped approach corners globally by angle, which interleaved unequal-width approaches and produced shard/sliver fans; corners now stay grouped per approach with wrap-safe ordering and outward curb-return arcs, and the fan runs over an angle-monotonic simple boundary.
- **Street corner arcs sample at ≤~9° per vertex** (`@jgengine/core/world/streetGenerator`) — corner fillets previously stepped up to `maxTurnAngle` (~28°) per vertex and read as hard polygons at road width; both net and circuit corners now sample finely, and sidewalk offsets became true parallel offsets (outside arcs, inside miter-clamp + weld) that never pinch into the road surface.
- **Circuit centerlines are curve-first splines** (`@jgengine/core/world/streetGenerator`) — the folded layout polygon (hull + deep inward displacement, guaranteed hairpin/ess) now serves as control points for a periodic centripetal Catmull-Rom spline with a curvature floor at `minCurveRadius` (weighted Laplacian relaxation), so a lap is mostly flowing sweepers and continuous esses with 1-3 deliberate straights — not straight segments with corner caps. Fitted radii form a smooth continuum (~1.1-11x the floor); property tests pin curvature continuity, curved-share, and hairpin+sweeper coexistence.

- **Chase camera yaw is smoothed by default** (`camera.chase.yawResponse`, `@jgengine/shell` chase rig, #1370) — the boom now eases toward the followed body's facing with `1-exp(-response*dt)` smoothing (default response 5) instead of rigidly equaling it every frame, so a strafe-flipped facing arcs the camera around the character rather than teleporting it to the far side. Set `yawResponse: Infinity` to restore the legacy rigid follow; drift-lag (`velocityYaw`) keeps its own response as before.
- **Editor content-browser thumbnails no longer crash the editor when a GLB's textures fail** (`@jgengine/editor`, #1270) — `getGlbThumbnailState` (the `getSnapshot` behind `useGlbThumbnail`/`AssetThumbnail`) returned a fresh object literal on every call, violating the `useSyncExternalStore` stable-snapshot contract; React's post-commit consistency check then forced a re-render every commit ("Maximum update depth exceeded"), and the `GameUiErrorBoundary` blanked the whole editor chrome. It now reuses the prior snapshot object while a URL's observable state (idle/loading/ready/error) is unchanged, so a permanently-failing asset settles to a stable error/glyph fallback instead of looping. No API change; behavior only.
- **Offline autosave no longer starves in a living world** (`@jgengine/core/runtime/runtimeSave`) — `autosave` mode previously debounced by *resetting* its timer on every world change, so an open world that mutates every frame (AI, physics, the day/night clock) pushed the timer forward faster than it could fire and never persisted; a game that drove no explicit `checkpoint()` effectively never saved. Autosave is now a trailing timer: the first unsaved change arms it, later changes ride the same timer, and it captures the latest snapshot when it fires — at most one write per `autosaveMs` and always within one interval, even while the world never idles. No API change; behavior only.
- **Effect consolidation phase 2: subscriptions on `useSyncExternalStore`, dispose pairs on `useDisposable`** (`@jgengine/react`, `@jgengine/shell`, `@jgengine/editor`, #1304) — hand-rolled `subscribe(() => setTick)` re-render mirrors across the editor chrome/panels, shell settings (`useSettingsRevision`), camera director, WorldHud selection, and AuthoredScene live-document hook now ride `useSyncExternalStore` (fewer redundant render passes, identical update timing); ~15 shell renderers moved their `useMemo` + dispose-effect pairs onto `useDisposable`; derived-state/prop-reset/focus effects were removed in favor of render-time derivation and `autoFocus` (editor inspector fields, hierarchy active row, animation panel, play-control mirrors via the new shared `usePlayControl`, GamePlayerShell binding overrides — kills a one-frame stale-keybinds render on game switch; `GameViewport` now renders `data-jg-layout-collision` declaratively). No public API removals; `usePlayControl` (`@jgengine/editor`) is new.

- **Blocking colliders are now walkable surfaces, and collision is on by default** (`@jgengine/core`
  movement, #1402) — the walking controller treats placed objects' blocking colliders as ground:
  `collideObjects` defaults `true`; a ledge within the new `PlayerMovementConfig.stepHeight`
  (default 0.4) is stepped up instead of walling; a jump that clears an object's top **lands standing
  on it** (new `obstacleSupportHeight`, integrated in absolute space so the arc stays continuous over
  changing ground) instead of sinking inside the box and being rubber-banded out by depenetration; and
  walking off a drop taller than a step falls under gravity instead of teleporting the feet down.
- **Movement and vehicles no longer tunnel through solids** (`@jgengine/core`, #1402) — the shared
  axis clamp in `resolveObstacleStep` is now swept: a step whose target lands *beyond* a box's far
  face stops on the near face instead of passing straight through (fast cars, dashes/impulses, low-FPS
  frames, thin walls). The walking broadphase also derives its reach from the scene's actual largest
  blocking collider instead of a hardcoded 4 m bound, so the player no longer walks through the edges
  of wide buildings whose center point sat outside the query; and the kinematic-vehicle clamp passes
  collider `offset`/compound `boxes` through instead of silently dropping them.
- **Walking collision now depenetrates instead of locking.** `resolveObstacleStep`
  (movement) previously slid along box faces but had no escape once the capsule was
  strictly *inside* a solid box — its per-axis clamps returned zero on both axes and the
  player was stuck forever. It now pushes an enclosed capsule out along the box's
  shallowest face before sliding. Resting exactly on a face still reads as contact, so
  normal wall-sliding is byte-for-byte unchanged; only the previously-unrecoverable
  "wedged inside a building" case now frees itself.
- `import_document` (`@jgengine/editor`) now answers a missing or mis-keyed document param with an
  error naming the expected `json` param instead of surfacing a raw `JSON Parse error: Unexpected
  identifier "undefined"`.

### Removed

### Fixed

- **A missing/mis-served GLB no longer white-screens the whole game — it degrades to one placeholder primitive** (`@jgengine/shell/render/modelLoad`, #1340). A Vite dev server returns its `index.html` fallback (HTTP 200) for a missing `/models/*.glb`; `GLTFLoader` then throws parsing HTML as a GLB, and that failure surfaced as a rejected `useLoader` Suspense promise which does **not** reliably re-throw into a per-model React error boundary inside the react-three-fiber reconciler — so it escaped to the app-level `GameUiErrorBoundary` and blanked the scene. `sharedGltfLoader` (`DiagnosticGLTFLoader`) now probes a failed URL and, for a diagnosed broken asset (missing / HTML fallback / corrupt / unsupported), **resolves to a `createFallbackModel` placeholder box instead of rejecting**, containing the failure at the load seam for every consumer with no reliance on boundary recovery; a genuine parse error over valid-looking bytes is still surfaced. Emits a dev console warning naming the broken path. No API change; `useLoader(sharedGltfLoader, url)` benefits with no call-site change.

## 0.14.0

### Migrate

- **Nothing required** — this release is additive drop-in primitives, editor authoring, CLI discovery, and fixes. Bump `@jgengine/*` pins to `^0.14.0` (CLI `jgengine` is 0.11.0; `@jgengine/github` unchanged at 0.1.0) and rebuild.
- One behavior fix to know about: in a `rig: "chase"` game, on-foot movement and aim are now camera-relative (the chase rig reports its yaw to the shell like every other rig). If a game somehow relied on world-yaw-0 movement under a chase camera, that was the bug this fixes.

### Added

- One authoring gesture for scatterable coverage: `@jgengine/core/world/scatterCoverage` owns shared density/budget semantics (per-kind unit, requested→count→capped, one clamp-and-warn phrasing) for `grass_field`/`scatter`/`city`; the editor inspector leads each kind with the same Area → Assets → Density coverage section, scatter truncates to the shared 250k instance budget, and city single-sources its 2,600-lot cap.
- **Created games ship a drive/playtest script** (#1248) — `npx jgengine create` scaffolds now include `scripts/drive.mjs` (`bun run drive`): dependency-free headless play/testing of the running game — ordered `--click`/`--key`/`--wait`/`--shot`/`--rpc` steps against the agent bridge, plus `--playtest --strict` progress/softlock verdicts off `capture.probe`. Shared Chrome/CDP machinery lives in `scripts/browser.mjs`; `scripts/shoot.mjs` is unchanged in behavior, now a thin CLI over it.
- Editor: viewport clip preview for rigged assets (Animation dock "Clips" mode — pick a rig or placed instance, play/scrub/loop/speed any catalog clip) and an Inspector "Animation" section that authors a placement's `ModelConfig.animation` (role→clip dropdowns, auto/none, walk/run/fade, one-shot event bindings) as undoable `marker.meta.animation` edits. New subpaths: `@jgengine/editor/shell/clipPreview`, `@jgengine/editor/modelAnimationAuthoring`, `@jgengine/editor/ClipPreviewLayer`.
- **`npx jgengine find <intent>`** — active capability discovery: searches every shipped domain's `capabilities.md` (staged inside the CLI tarball, so it works regardless of which `@jgengine/*` packages a project installed) and prints the drop-in primitive + its import for an intent like `"toggleable window"`, `inventory`, or `minimap`. Scaffolded games are now briefed (in `AGENTS.md` and the `jgengine` intake skill) to reach for it before hand-rolling a HUD/inventory/window/rig.
- **`EntityPreview`** (`@jgengine/shell/render/EntityPreview`) — drop-in live 3D entity portrait for character screens, unit inspectors, and loadout viewers. Owns the nested `<Canvas>`, the `GameContextBridge`, and a `StudioStage` lighting rig with optional turntable / face-camera; the game passes its own `renderEntity` as children (or a native `model` + `instanceId`) and, bound to a live entity, the portrait walks/flinches/topples in sync with the world.
- **`GameContextBridge`** (`@jgengine/react`) — re-provide the running `GameContext` across a nested React reconciler boundary (the R3F `<Canvas>`), so a game building its own preview canvas no longer re-derives the bridge by hand.

### Changed

- **Chase rig reports its yaw to the shell** — `ChaseRig` now writes the camera yaw back to the shared yaw ref like every other player-facing rig, so on-foot movement and aim in a `rig: "chase"` game are camera-relative instead of frozen to world yaw 0 (WASD no longer fights the camera the moment it swings behind the player). Interior views (cockpit/hood/rear) report the vehicle heading.
- **Combat VFX no longer render as black squares under AO/DOF post-processing** (#1247) — GTAO/Bokeh scene prepasses skip overlay effects. Games with custom additive overlay effects opt out the same way: spread `POSTFX_OVERLAY_USERDATA` (`@jgengine/shell/postfx/postfxOverlay`) onto the overlay group's `userData`.
- **`PanelHost` windows stack above the HUD by default** (`@jgengine/react`) — the host establishes its own stacking context at `zIndexBase` (default 40), so open windows always paint over stat bars / nameplates / frames instead of bleeding through. Overridable per instance.

## 0.13.0

### Migrate

- **Nothing required** — this release is additive fixes, defaults, and new drop-in primitives. Bump `@jgengine/*` pins to `^0.13.0` (CLI `jgengine` is 0.10.0; `@jgengine/github` unchanged at 0.1.0) and rebuild.
- Two defaults changed in the player's favor, both overridable: **offline games now persist to localStorage automatically** (#1207) and **rigged catalog assets animate by default** via clip metadata + semantic clip roles (#1209). If a game relied on rigged models staying frozen or on saves being memory-only, opt out explicitly; otherwise you get better behavior for free.

### Added

- **Drop-in inventory grid, window manager, and character sheet** (#1220) — `@jgengine/react` ships `InventoryGrid` (real drag/stack/split), a toggleable-window system (`panels`: bag, character sheet, spellbook-style windows with `@jgengine/core/ui/panelModel` state), and a `CharacterSheet` paperdoll, all HudTheme-skinnable — wire in a line or two instead of re-deriving from raw divs.
- **Serializable runtime snapshots** (#1179) — capture/restore of live runtime state, including magazine state (`@jgengine/core/combat/magazine`) and stat modifiers, for saves and replication.
- **Portable weapon plumbing** (#1211) — `@jgengine/core/combat/weaponFire` composes trigger → magazine → spread → projectile/hitscan as data, no game-local glue.
- **Portable leveling** (#1206) and **portable damage-pool access** (#1208) — progression and damage pools exposed as genre-agnostic seams on the game context.
- **Named combat VFX presets** (#1204) — `@jgengine/core/combat/vfxPresets`: a visible attack is one line.
- **Runtime-measured render-bounds hitboxes** (#1227) — colliders wrap what the shell actually mounts, not the raw asset bounds.
- **Character motion feel** — procedural part motion for rig-less part-composed characters (#1222) plus squash & stretch and death splat (#1229).
- **City fabric** (#1191) — `streetGenerator` + `cityGenerator`, editor bake verb, street-aware lots, road junctions, grounding, and a web playground.
- **`jgengine recipe` CLI** (#1214, #1231) — vetted wired compositions, compile-pinned against the SDK, listed and copied from the CLI.
- **Editor/debug modes mirror to the URL** with a proper editor exit (#1203); game template ships a dependency-free WebGL screenshot script (#1202).

### Changed

- Offline games persist to localStorage by default (#1207); rigged assets animate by default (#1209).
- RTS camera pan inversion fixed (#1228); sprite raycast warning silenced and duplicate nameplate health bars removed (#1194); scaffolded F2+E editor Tailwind `@source` renders its theme (#1195).
- Outside-game DX: stat-pool no-op stability, camera-transparent decor, cleaner install/version output, headless UI-intent seam (#1223); new-game briefs direct engine bugs/gaps upstream (#1200).

## 0.12.0

### Migrate

- **Author games through `defineGame` from `@jgengine/shell`** — it is the single public authoring entry (and carries the define-game capability tag). Core's constructor was renamed `defineGame` → `defineGameDefinition` (`@jgengine/core/game/defineGame`); only hosts and tooling call it. If you imported `defineGame` from core, switch the import to `@jgengine/shell`. `GamePlayer`/`GamePlayerShell` are `@internal` now — `GameHost` is the documented mount, and it owns the editor summon (`?mode=editor`, F2+E, dev save endpoint), so delete any per-game editor bootstrap.
- **Declare your world with `world()`** (`@jgengine/core/world/place`): `world({ id, ground: { mode, size, surface? }, physics? })` with `flat` / `round` / `voxel` / `board` ground. New scaffolds emit a thin infinite flat place instead of the old 96 m meadow. `environment()` still works but is demoted to the legacy consumption path for editor-written scene data — **nothing required** for existing games; new worlds should use `world()` and author all dressing in the editor.
- Everything else is additive — stat pools, minimap marker sources, auction house, city/path generation, editor workspaces require no changes to existing games.

### Added

- **World = place: `world()` API** (#1178) — a world is substrate + laws, not a coded diorama. `@jgengine/core/world/place` ships `world({ id, ground: { mode, size, surface?, generator? }, physics? })`; size is discriminated by mode (flat `{x,z}` with `Infinity` axes, round `{radius}`, voxel generator domain, board `{x,y}` cells — TS and runtime both reject mismatches). `ground.surface` carries matter/feel laws (metal vs slime vs felt) read by the same rule systems; per-place `physics` resolves over the game default; multiple worlds per game are first-class. The shell renders place substrates (`PlaceScene`) under the engine default sky; seeds derive from world id + save via `seedForPlace`.
- **`@jgengine/shell/gameKit`** — happy-path authoring surface: authoring, mount, stores, systems, authored-scene helpers, HUD primitives, seeded rng in one import.
- **Portable generic stat pools** — `@jgengine/core/stats/statPool`: bounded named pools (health, energy, heat, ammo…) with the entity-stats integration rebuilt on them; recipe in skill `jgengine-combat`.
- **Auction-house economy primitives** (#1086) — `@jgengine/core/economy/auctionBook` (timed bid auctions, escrowed bids, outbid refunds, optional buyout, anti-snipe close extension, seller settlement minus house cut) plus market price history.
- **Portable minimap marker sources** — core world seam feeding game minimaps; recipe in skill `jgengine-ui`.
- **Unified seed-driven path network + city generation v2** (#1103, #1106) — streets/blocks/buildings with grid-ness, curviness, branching and block-size dials; zone bands, weighted building-class mixes, massing pieces, balance presets, junction-centered clustering, road-derived block polygons and parcels. Shell renders realistic roads: real intersections, curbs, lane lines, tunnels, and roads riding bridge decks.
- **Physical fidelity** — mesh-accurate hitboxes (shots pass through holes in concave models, #1075), auto-fit entity/object colliders from measured model bounds (#1072), movement obstruction reading fitted boxes + compound-box openings (#1077).
- **Editor overhaul** (#1110) — materials / lighting / scripting / animation-timeline / multiplayer workspaces, command palette, minimap bake wired end-to-end (#1036), real offscreen GLB thumbnails in the content browser, hierarchy drag-drop + keyboard + context menus + per-object visibility/lock, multi-select inspector, console RPC, asset drag-place, ortho projection, pivot modes, play chrome.
- **Editor authoring pipeline** — author entity definitions and starter catalogs (#1012), author catalogs/schema fields in the Data tab (#1043), promote an authored scene folder into a runnable game (#1011), authored triggers promoted to a shared primitive with goal/win rules (#1013), asset import rewrites the game's `game/assets.ts` (#1042).
- **Skills** — research-backed `game-design` and `level-design` skills (#962); every-game-owns-custom-UI invariant codified (#1148).

### Changed

- `npx jgengine create` scaffolds a thin infinite flat `world()` place; the editor `blankWorld` no longer pre-bakes seeded grass. Dressing belongs in `editor.scene.json`.
- Environment studio visual-quality pass — turf tufts, depth-aware water, relief soil, weathered poles; grass fields no longer rebuild geometry per frame (#1090).

## 0.11.0

### Migrate

- **Nothing required** — classic `loop.onTick` fan-out still runs after systems. Prefer moving per-frame work into `defineGame({ systems: [...] })` with `defineSystem` (`@jgengine/core/game/defineSystem`); install a system with `feature: "quest"` (etc.) instead of a redundant `features` flag. See skill `jgengine` → [reference-systems.md](.claude/skills/jgengine/reference-systems.md).

### Added

- **Composable game systems** (#842) — `defineSystem` / `compileSystemSchedule` / `composeGameLoop` / `installSystems`. `defineGame({ systems })` is the single authoring path: fixed / frame / interval / event-only / manual ticks, multi-subscribe channels, deterministic stage order + optional `before`/`after`/`dependsOn`, system-owned save / replicate / reset / dispose. Installing a system activates its `feature` without a separate flag. `GameLoop` gains optional `onReset` / `onDispose`. `ctx.game.registerSave` / `registerReplicate` for system modules. Adopters: tower-guard, claudecraft (runtime without a giant tick fan-out).

## 0.10.0

### Migrate

- **Nothing to change for existing games** — the shared mobile-composition system (viewport allocation, region collision, `--jg-*` viewport vars) is wired automatically by the shell for every game; `HudPanel`s inside a `HudCanvas` register themselves. Legacy `orientation: "landscape" | "portrait"` keeps its advisory dismissible-hint behavior unchanged.
- **Opt a driving/landscape game into the strict gate**: replace the advisory `orientation` with the contract form — `orientation: { mobile: "landscape-required" }` (rules: `any` · `portrait`/`landscape` advisory · `portrait-required`/`landscape-required` · `unsupported`). In portrait the engine now shows a polished `RotateDeviceScreen`, suppresses input, freezes the sim, and unmounts the HUD/controls until the device is landscape.
- **Declare HUD intent** where useful: `HudPanel` gains `priority` (`critical`/`secondary`/`tertiary`), `mobileBehavior` (`hidden` unmounts on phones, `transient` softens collision, …), `allowOverlapWith`, and `collisionGroup`. Optional — omitting them keeps prior behavior.
- **Mobile validation now also fails on overlap**: `bun run shoot <game> --device mobile|mobile-landscape|both` exits non-zero on forbidden inter-element collisions (not just viewport overflow), naming both regions.

### Added

- Shared mobile layout composition — `@jgengine/core/ui/gameLayout` (pure geometry: `resolveLayoutMode`, `intersects`, `overlapArea`, `detectLayoutCollisions`, `computeGameplayRect`, `LayoutRegion`, `GameViewportLayout`) and `@jgengine/core/ui/orientation` (`resolveOrientationRequirement`, `orientationGateActive`, `MobileOrientationRule`). `@jgengine/react` adds `GameViewportProvider` (mounted by the shell), hooks (`useGameViewportLayout`, `useGameLayoutMode`, `useGameOrientation`, `useReservedControlZones`, `useLayoutCollisions`, `useRegisterLayoutRegion`), and a headless `RotateDeviceScreen`. The provider tracks `window.visualViewport`, publishes `--jg-viewport-*` / `--jg-visual-viewport-*` / `--jg-safe-*` CSS vars, resolves the explicit layout mode, and detects forbidden region overlaps (dev `data-jg-layout-collision` + console diagnostic).
- Mandatory orientation contract — `PlayableGame.orientation` accepts `{ mobile: <rule> }`; `landscape-required`/`portrait-required` render an engine-owned rotate gate that blocks gameplay (input suppressed, simulation frozen, HUD/controls unmounted), self-correcting when the device is turned. Reduced-motion and safe-area respected; `visualViewport`-sized for mobile Safari/PWA.
- Reserved touch-control zones — the touch dock registers its joystick / action-cluster / utility rectangles (runtime-measured) as `control` regions, so HUD placement and collision validation know exactly where controls sit.
- HUD placement metadata — `HudPanel` gains `priority`, `mobileBehavior`, `allowOverlapWith`, `collisionGroup`, and registers as a collision-tracked layout region.
- Mobile-landscape shoot device — `bun run shoot --device mobile-landscape` (844×390) plus collision-gate reads that fail mobile validation on forbidden overlaps.
- Canyon Chase migrated as the reference landscape-required game — declares `orientation: { mobile: "landscape-required" }`, composes its HUD through coordinated `HudPanel` regions (pursuit distance critical, border secondary, survey map hidden on mobile, radio transient) instead of independent fixed corners.

### Changed

- First-person projectile tracers now originate from the weapon muzzle instead of the camera/eye centerline. Hit detection is unchanged (still crosshair-accurate) — only the drawn tracer's start point moved to the viewmodel muzzle, which tracks full aim yaw and pitch. Non-viewmodel games and enemy tracers are unaffected.

## 0.9.0

### Added

- Mobile HUD fit — design-resolution UI scaling now applies to every game by default: `HudCanvas` measures the live viewport and scales the whole HUD from `PlayableGame.hudFit.designSize` (default 1600×900, clamps `minScale`/`maxScale` 0.4–1) so desktop-authored layouts shrink to fit a phone instead of overflowing it; `hudFit.mobile` overrides tune the phone fit separately. Pure math lives in `@jgengine/core/ui/hudScale` (`hudScaleForViewport`, `resolveHudFit`, `rectOverflow`, `overflowingPanels`); the shell mounts `@jgengine/react`'s `HudViewportProvider` around `GameUI` so games need no wiring.
- Graphics → UI scale — a player-facing `graphics.uiScale` slider (0.5–1.5, default 1) multiplies the computed HUD scale on every platform; the same resolution system drives desktop preference and mobile shrink.
- HUD overflow gate — `HudCanvas` measures every `HudPanel` against the viewport at runtime and reports offenders on a `data-hud-overflow` attribute (plus a console warning); `bun run shoot <game> --device mobile|both` now exits non-zero naming the panels that escape the viewport.
- Automatic visibility & streaming defaults — an engine-level `VisibilitySystem` (exported from `@jgengine/core`) gives every scene renderer-agnostic culling and asset-streaming policy with no per-object wiring: distance culling, preload margins, hysteresis, delayed unloading, multi-camera awareness (including cameras excluded from streaming), and per-object overrides (always-visible, never-unload, disabled culling/streaming, custom render distance and preload margin).
- Self-hosted asset mirror — `DEFAULT_RELEASE_BASE` now points at this repo's rolling `packs` release instead of the upstream host; a catalog-driven `mirror-assets` workflow (weekly cron + manual dispatch) keeps the mirror in sync with the asset catalog.

### Changed

- Mobile HUD fit is on by default — design-resolution HUD scaling now applies to every game with no config change (previously opt-in via `platforms: ["web", "mobile"]`). A desktop-only game keeps the legacy fixed 0.85 compact zoom by declaring `platforms: ["web"]` (without `"mobile"`).

## 0.8.0

Transport-agnostic multiplayer (socket.io, WebRTC p2p, LAN/Fly adapters) plus grid/voxel
and puzzle primitives, HUD-only presentation, cumulative leveling, and a round of
controller/camera/sensor additions. **Breaking:** the `gameui` component kit moved out of
`@jgengine/react` onto the shadcn registry.

### Migrate

- Bump every `@jgengine/*` dependency to `^0.8.0` (the eight packages version in lockstep).
- Additive only, except for `gameui` — every other 0.7.0 API is unchanged; opt into any of the below by importing it directly.
- **Breaking:** replace any `@jgengine/react/gameui` import with the equivalent registry component (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), and swap `GameUiThemeProvider` for `--jg-*` CSS variables on a wrapper element. `GameIcon` and friends moved to `@jgengine/react/gameIcons`.
- `leveling({ thresholdMode: 'cumulative' })` is opt-in; the default `perLevel` behavior is unchanged.
- `defineGame.physics.gravity`/`jumpVelocity` are now read by the built-in kinematics controller every frame — if a game already set them expecting a no-op, jump/fall now actually reflects them; omit both to keep the previous defaults.

### Added

- Cumulative leveling — `leveling({ thresholdMode: 'cumulative' })` tracks xp as a lifetime total that resolves upward across levels and clamps at the max-level threshold once capped (#12).
- Direction-aware pool depletion — `EffectSystem.canReceive(instanceId, effect, magnitude?)` takes an optional signed magnitude; negative checks the opposite direction and returns `pools-depleted` only when every stat in the receive order is already at max, so heals reach fully-depleted targets (#168).
- Puzzle primitives — `puzzle/cellGrid` (uniform-cell boards: line-clear, match-3 cascade, run detection) and `puzzle/fallingPiece` (rotation-state shapes, ghost drop, lock delay, classic gravity/level/score curves) for Tetris/match-3 games (#166).
- Voxel field — `world/voxelField` (`createVoxelField`, chunked block lattice, neighbors/exposedFaces, 3D DDA raycast, dirty-tracked `chunkVersion`) for voxel games and instanced renderers; assert on `field.summary()` the way environment worlds assert on `summarizeEnvironment` (#166).
- `defineGame` games may omit `assets` (an empty catalog is injected); `PlayableGame.presentation: 'hud'` mounts no 3D canvas/camera/pointer for board/card/menu games; an `environment()` world auto-renders as the shell's backdrop when `PlayableGame.environment` is unset (#166).
- Declared-action intent board — `turn/intent` `createIntentBoard` for one-turn-ahead intents (Slay-the-Spire style): declare/peek/all/consume/clear (#168).
- `turnLoop` lifecycle hooks — `config.onTurnStart`/`onTurnEnd` fire on every `advanceTurn()`; `ctx.game.turn.loop(id, config)` lazily creates/returns a notify-wrapped `TurnLoop` so every mutation (`advanceTurn`/`advancePhase`/`advanceRound`/`spend`/`gain`/`refill`/`setOrder`/...) auto-bumps `ctx.version()` with no manual wiring (#163/#168).
- `ctx.game.store` — a reactive per-game keyed store (`set`/`delete`/`get`/`has`/`subscribe`/`mapSnapshot`/`arraySnapshot`) plus `@jgengine/react`'s `useGameStore` selector hook, replacing hand-rolled module-level stores for ad-hoc reactive game state; `ctx.game.cards.pile(id, config?)` lazily creates/returns a notify-wrapped `CardPile` the same way; `createCardPile` gained an `onChange` hook for headless use; `CommandDefinition.apply` may return void for side-effect-only commands (#163).
- Camera — `sideScroll` rig (fixed lateral follow for 2.5D platformers/beat-'em-ups), a `none` rig (no camera mounted; pairs with `PlayableGame.presentation: 'hud'`), `rts.pan: false` (static backdrop camera: no pan/edge-scroll/rotate/zoom, still re-centers on the follow target), and the `observer` rig now defaults to the local player when `bind` is unset (#167).
- Sensors + session — `sensor/concealment` (`colorDistance`/`concealmentScore`/`createConcealmentSensor`), `sensor/freezeMonitor` (`createFreezeMonitor`), `session/roles` (`assignRoles`); `createRoundState`'s `RoundConfig.teams` accepts per-team roles and an optional `winCondition` that `evaluate()` checks each tick, and takes an optional `phaseOrder` for arbitrary named phase cycles (`concludeRound`/`evaluate` settle only while the current phase is neither the first nor the last entry) (#151).
- Appearance replication — presence rows carry an optional per-slot appearance channel (cosmetic ids, hex tints, model keys) alongside pose, riding the existing pose message with no protocol bump; wire `ctx.player.cosmetics.get(userId)` into the outgoing pose (#151).
- `ctx.input` — a per-frame held-action snapshot (`publish(held)`/`isDown(action)`/`held()`) without bumping `ctx.version()`; action bindings gained `repeatMs` (repeat-fire while held); every command resolved from a bound action now carries `aim`; `pointer.secondaryCommand` runs a command on right-click off the same raycast as move/ping (#164).
- Object spatial queries + entity patching — `ctx.scene.object.at`/`inBox`/`raycast`/`raycastAll` over unit-box objects; `ctx.scene.entity.update(id, patch)` for name/position/rotation/role/movement/behaviors/meta; per-instance `renderObject`/`objectStyles` overrides; `pointerService.worldHitCenter()` + pointer-lock center-ray aiming (#165).
- Controller movement config — `PlayerMovementConfig` (`mode`: free/axis/grid, `axis`, `cellSize`, `collideObjects`, `beforeCommit` pre-commit hook) for the shell-driven walk controller; `defineGame.physics.gravity`/`jumpVelocity` are honored by the built-in walk controller (distinct from the standalone `physics/physicsWorld` rigid-body sim); `ctx.player.motion.impulse`/`setVerticalVelocity`/`setY`/`takePending` (`MotionIntents`); `entity.spawnPoseOf`/`resetToSpawn`/`resetAllToSpawn` (#162).
- Model material/animation + paint — `ModelConfig.tint`/`metalness`/`roughness`/`animation` (GLTF clip playback, paused pose holds); `PointerHit.uv` + `pointerService.sampleSurface()` for material-aware picking; `ctx.scene.entity.paint` runtime paint layer, auto-rendered via a per-instance canvas texture with no per-game wiring; remote-player appearance tint recolors the shell's default capsule (#151).
- **Transport pipe seam** (`@jgengine/ws/pipe`) — `createWsBackend` runs over any bidirectional string channel, not just a raw `WebSocket`: `TransportPipe`/`TransportPipeHandlers`/`TransportPipeFactory`, with `webSocketPipe(url, webSocketFactory?)` as the default. `createWsBackend({ userId, url?, pipe? })` — `url` stays the common case, `pipe` opens the seam to socket.io, WebRTC, and in-process loopback below.
- **Browser-safe authoritative host** — `createGameHost` and `memoryPersistence` moved from `@jgengine/node` to `@jgengine/ws/host` (zero Node dependencies; `@jgengine/node` re-exports both unchanged from `@jgengine/node/host` / `@jgengine/node/persistence`, so existing imports keep working). `@jgengine/ws/hostRouter`'s `createHostRouter({ host, authenticate?, poseRules?, positionHistoryMs?, chatRateLimit?, chatHistoryLimit?, chatMaxBodyLength?, now? })` extracts the ws wire-protocol session logic out of `createGameWsServer` into a transport-agnostic `HostRouter` (`connect(transport) → { handleRaw, close }`, `rewind`, `close`); `@jgengine/node`'s `createGameWsServer` is now a thin binding of this router onto the `ws` npm package, same public API. `loopbackPipe(router): TransportPipeFactory` connects a `createWsBackend` straight into an in-process router.
- **Socket.IO transport** — `@jgengine/ws/socketIoPipe` (`SocketIoLikeSocket` structural type, `socketIoPipe(socket)`, `createSocketIoBackend({ socket, userId, … })`) and `@jgengine/node/socketIoServer` (`attachGameSocketIoServer({ io, host, …router options }): { rewind, close }`, structural `SocketIoLikeServer`/`SocketIoLikeServerSocket`) ride the existing ws JSON protocol over socket.io's `send`/`message` frames — no socket.io dependency in either package's types. New `socketIo({ topology?, url? })` adapter in `@jgengine/core/runtime/adapter`.
- **WebRTC peer-to-peer** (`@jgengine/ws/peer`) — one browser tab hosts, authoritatively, with no server process. `createPeerHost({ userId, host?, runtimes?, persistence?, tickMs?, router?, rtc? })` runs a `GameHost` + `HostRouter` in the host tab (`backend` is the host player's own loopback connection, `accept(offerCode) → Promise<answerCode>` per guest); `createPeerGuest({ userId, token?, rtc? })` offers/connects from the joining side. `encodePeerSignal`/`decodePeerSignal` turn SDP into copy-pasteable base64url codes for manual cross-machine signaling; `broadcastChannelSignaling(room)` automates it for same-origin multi-tab play, with `announcePeerHost`/`joinPeerSession` wiring host/guest to a `PeerSignaling` in one call. New `p2p({ topology?, room? })` adapter (topology defaults `"private"`).
- **LAN adapter + Fly sugar** — `lan({ topology?, port?, path? })` in `@jgengine/core/runtime/adapter` resolves through `@jgengine/shell/multiplayer`'s `resolveShellMultiplayer` to `ws(s)://<page hostname>:<port ?? 8080><path ?? /ws>` derived from `window.location`, so any browser on the LAN auto-connects to whichever machine served the page — no URL configuration. `fly({ app, topology?, path? })` is `ws` sugar for a Fly.io deploy: resolves to `wss://<app>.fly.dev<path ?? "/ws">`. `apps/dev`'s Vite server now listens on the network (`server: { host: true }`) and exposes `?p2p=host` / `?p2p=join` query params wired through the new `resolvePeerShellMultiplayer({ gameId, role, room?, userId?, feedActions? })`.
- `ws()` (`@jgengine/core/runtime/adapter`) gained an optional `url` field, carried through by `resolveShellMultiplayer` (`args.url ?? adapter.url ?? default`).

### Removed

- **The `gameui` component kit** (`@jgengine/react/gameui`) — the themed HUD kit (bars, slots, feedback, meters, panels, screens, reticles, icons) and its `GameUiThemeProvider`/`useGameUiTheme` theming have been removed. **Breaking** for anyone importing `@jgengine/react/gameui` (or its subpaths/barrel). The components now ship as installable shadcn registry items at `https://jgengine.com/r/<name>.json` (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), styled with Tailwind + `--jg-*` CSS variables instead of a theme object. The icon catalog (`GameIcon`, `iconForAction`, `iconForItemId`, `isGameIconName`, `GameIconName`) moved to `@jgengine/react/gameIcons`. To theme, set the `--jg-*` variables on a wrapper element (the registry's `jg-theme` presets mirror the old `ember`/`synthwave`/`fieldkit` palettes) instead of wrapping in `GameUiThemeProvider`.

### Docs

- Added [CREDITS.md](CREDITS.md) crediting [achrefelouafi](https://github.com/achrefelouafi) for the MIT Three.js reference projects behind the procedural building, water, rain, and snow renderers, with links from the root, `@jgengine/core`, and `@jgengine/shell` READMEs.

## 0.7.0

The engine-gaps release — 22 system-level additions across turn/tactics, cards & boards,
crafting, survival, navigation & AI, camera rigs, physics & vehicles, traversal &
destruction, world items & building, map/HUD/ping, combat feel & abilities, audio,
interaction, sensors, embodiment, multiplayer depth, and objective/session machines.
**Every 0.6.0 API is unchanged** — the whole release is additive, so upgrading is a version bump.

### Migrate

- Bump every `@jgengine/*` dependency to `^0.7.0` (the eight packages version in lockstep).
- No code change is required — 0.7.0 only adds surface; no 0.6.0 API moved or was removed. Existing games keep the orbit/first-person camera, single-player-entity control, and every existing primitive exactly as before.
- Opt into any new system by importing it directly: a camera rig via `camera.rig` + its config block; a `sensor/*` probe with `@jgengine/shell/vision` renderers; an `ai/*` director over the `nav/` navmesh; `turn/*` + `tactics/*` for turn-based/grid games; `cards/*` + `board/*` for deckbuilders; `crafting/*` for recipes/production/farming; `survival/*` + `world/{envField,weather,realm}` for survival; `combat/{abilityKit,animationState,defensiveWindow,…}` for action feel; `physics/{vehicleBody,traversal,structure,ragdoll,…}` for vehicles/destruction; `session/*` for contested/round/downed/ring/extraction machines; and `multiplayer/*` for lag-comp/hidden-commit/matchmaking.
- `entityStore.update()` now also accepts `name`, so possession/form can retarget an instance's catalog id without despawn/respawn — no action needed unless you relied on `name` being immutable.

### Added

- **Turn-based & tactics stack** — six pure, renderer-free `@jgengine/core` primitives for turn-based, grid-tactics, and card games (XCOM, BG3, Into the Breach, Slay the Spire, Marvel Snap, Tactical Breach Wizards, Divinity surfaces).
  - `@jgengine/core/turn/turnLoop` (`createTurnLoop`) — initiative machine with configurable `phases` and per-turn action-economy `pools` (`{ id, max, start? }`) that reset when a participant enters their turn; `advanceTurn`/`advancePhase`/`advanceRound`, `spend`/`canSpend`/`gain`/`refill`, and `setOrder`/`addParticipant`/`removeParticipant`. Covers both Slay-the-Spire single-energy resets and BG3's Action/Bonus/Movement/Reaction set.
  - `@jgengine/core/turn/commit` (`createCommitController`, also `turnLoop.commit`) — three commit modes: `immediate`, `simultaneous` (sealed hidden submissions → `reveal()` on `allReady()`), and `rewind` (visible `pending()` → `rewind()`/`commit()`).
  - `@jgengine/core/tactics/tacticalGrid` (`createTacticalGrid`) — tile occupancy, `reachable(from, budget)` flood-fill, `path`, and `push(id, dir, { distance, chain })` discrete knockback-to-tile with chained collisions (Into the Breach).
  - `@jgengine/core/tactics/predictiveQuery` (`predictAreaEffect`/`predictArcEffect`/`predictTiles`) — a would-this-effect-hit query for pre-commit overlays and enemy-intent telegraphs, reusing the exact AoE/LoS targeting behind `ctx.scene.entity.effect` (new shared `resolveAreaTargets` in `combat/effects`) so predictions match what the effect actually drains.
  - `@jgengine/core/tactics/snapshot` (`createSnapshotStore`, `deepClone`) — cheap, repeatable turn-undo over registered `capture()/restore()` slices (the grid, surfaces, and turn loop all qualify), with a `push()/pop()` undo stack.
  - `@jgengine/core/tactics/surface` (`createSurfaceLayer`) — a stateful tile surface layer with its own `tick(dt)` and a data-driven combination matrix (`reactions: [{ when: [a, b], result }]`) — grease+fire, water+lightning — distinct from terrain/water.
- `@jgengine/core/combat/effects` now exports `resolveAreaTargets` (+ `AreaTarget`, `AreaTargetInput`), the shared in-radius→LoS→falloff→accept targeting that both `applyEffect` and the predictive query run, guaranteeing parity by construction. No behavior change to `effect`.

Pure, renderer-free primitives for card, board, and deckbuilder games — they sit **beside** the slot inventory, never replace it.

- **`@jgengine/core/cards/cardPile`** — a `cardPile` of named ordered zones (deck/hand/discard/exhaust) with seeded `shuffle`, `draw(n)` (hand limits + reshuffle-on-empty), `discard`/`exhaust`/`move`. Reuses the engine's seeded RNG (`pileRng`), so shuffles are deterministic under a seed. Slay the Spire, Balatro.
- **`@jgengine/core/cards/modifierPipeline`** — an ordered `{ source, apply(value) → value }` pipeline (`runPipeline` / `createModifierPipeline`) with an inspectable per-step `trace` (before/after/changed) for Balatro-style scoring readouts. Generic over the scored value.
- **`@jgengine/core/board/laneBoard`** — N lanes, per-side power aggregate + optional per-lane `LaneRule` modifier, with `laneOutcome`/`boardTotals`/`lanesWon`. Marvel Snap, Inscryption.
- **`@jgengine/core/board/timelineBoard`** — N slots each on an independent cooldown, `tick(dtMs)` resolving fires in expiry order (then slot index), multiple fires per slot per tick. The Bazaar auto-battlers.
- **`@jgengine/core/inventory/shapedGrid`** — a polyomino inventory variant: `Footprint` placement, `rotateFootprint`, `canPlace` overlap/bounds check, plus `gridAdjacencyQuery` (orthogonal/diagonal neighbors) feeding synergy effects, and `cellFromPoint` for pointer→cell snap. Backpack Hero, Tetris inventory.
- **`@jgengine/react/dragLayer`** — a 2-D UI-space drag/rotate/drop/snap gesture layer over the above: `useDragLayer`, headless `DraggableCard` (right-click rotate), `DropZone` (cell snap + active state), `DragGhost`.

- `@jgengine/core/crafting/recipe` — a recipe graph primitive: `RecipeDef { inputs, outputs, seconds?, station?, stationRange?, requires? }` turns inputs + an optional required-workstation-in-range + time into outputs. `craft` / `canCraft` consume and produce on an `InventoryState` atomically (rejecting `missing-inputs` / `no-station` / `locked` / `no-output-space`); `stationSatisfied` does the range check against placed `{ catalogId, position }` stations; `createRecipeGraph` indexes recipes by `producing` / `using` / `category`. For Valheim/Enshrouded-style workbench tiers, Tarkov hideout stations, Palworld base craft. (#71)
- `@jgengine/core/economy/techTree` — a prerequisite-gated tech tree that **generalizes flat `unlocks`** rather than duplicating it: `TechNodeDef extends UnlockDef` adds `requires` (prereq ids), a `recipe` payload, and `grants`. `createTechTree(defs)` wraps `createUnlocks` and gates `unlock(userId, id)` on prerequisites, exposing `available` (frontier) and `recipes` (payloads unlocked). Flat unlocks are just nodes with no `requires`. For Once Human Memetics / Abiotic Factor branching trees. (#72)
- `@jgengine/core/crafting/production` — `productionBuilding({ inputs, outputs, rate, power? })` plus `tickProduction`, which consumes buffered inputs and emits outputs continuously through game-time `dt` (so pause/fast-forward apply for free). `feedProduction` / `drainOutput` are the buffer I/O, `advanceTransport` slides items along a conveyor path, `resolvePowerGrid` powers demands greedily against a supply. For Palworld/Satisfactory/Factorio automation. (#74)
- `@jgengine/core/crafting/crop` — a `cropTile` soil-and-growth state machine (`tillTile` / `plantCrop` / `waterTile` / `advanceCropDay` / `harvestCrop`, with regrow and daily-water rules) that advances stages on the simClock day tick, plus `applyToolToTiles(tiles, center, pattern, apply)` with `squarePattern` / `diamondPattern` / `rectPattern` for watering-can/hoe AoE under the cursor. `createCropField` wraps a tile grid; `createDayTicker` reads day rollovers off `ctx.time.calendar()`. For Stardew/Coral Island/Palia farming. (#75)
- **Survival & environment layer.** Renderer-free `@jgengine/core` primitives for survival games (Project Zomboid moodles, The Long Dark / Green Hell survival, DayZ / Tarkov per-limb health, weather-driven games, Nightingale realm cards), plus the `@jgengine/shell` fire visuals.
  - `@jgengine/core/survival/decayMeter` — named `decayMeter`s (`createDecayMeterSet`) that drain/recover on game-time `dt`, refill from consumables/actions, raise threshold moodles, and take environment-driven rate modifiers (cold speeds warmth loss, toxic biomes drop oxygen).
  - `@jgengine/core/survival/moodle` — a stacking status-effect display distinct from numeric bars: `stackMoodles(...)` folds meter/ailment/buff moodles worst-first, and `createMoodleStack()` holds timed buffs (Valheim-style concurrent food buffs) that expire on `tick(dt)`.
  - `@jgengine/core/survival/regionHealth` — a multi-region health component (`createMultiRegionHealth`): per-part pools with vulnerability + vital death, a stacking ailment queue (bleed/fracture) that drains over time, and per-injury treatment items (`treat("bandage")`). Shares the moodle display via `ailmentMoodles()`.
  - `@jgengine/core/world/envField` — sampleable environment fields (`createEnvironmentField`): temperature, wetness, light-exposure, and ambient-light at any world position and time, with sky occluders, heat sources, and a day/night sun cycle. Meters and spawn gating read it renderer-free.
  - `@jgengine/core/world/weather` — weather → gameplay modifiers (`resolveWeather` → grip/visibility/structure-damage/chill/ignition/spread) over a game-owned table, plus a **coarse cellular** fire-spread grid (`createFireGrid`, not a fluid solver): wind-biased propagation, fuel burn-through, firebreaks, and rain/wetness suppression.
  - `@jgengine/core/world/realm` — runtime realm composition (`composeRealm`): assemble a played instance from a deck of modifier cards (major biome + minor weather/day-length/spawn cards) that override environment params and spawn tables, recomposing a sampleable environment field on top of the weather hooks.
  - `@jgengine/shell/weather` `FireSpreadLayer` — renders a `FireGrid`'s burning flames + scorched cells; a `survival-demo` game wires the whole stack (moodle stack, per-part body panel, condition meters, weather readout, spreading fire).
- `@jgengine/core/audio/audioFalloff` — the audio contract + pure distance→gain math. `SoundDef`/`AudioBusDef` catalog types, `computeFalloffGain(distance, config)` (`"linear" | "inverse" | "none"` curves), and `resolveEmitterGain`/`distance3` helpers. No Web Audio in core — this is the tested math the shell plays from.
- `@jgengine/core/time/beatClock` — a BPM tick signal, separate from `simClock`. `createBeatClock({ bpm, beatsPerBar? }, onBeat?)` advances on **game-time** `dt` and fires `onBeat` per crossed beat; `createBeatInputBuffer` auto-corrects an off-beat press to fire on the next beat tick (`nextBeatTime` is the underlying pure quantization). For rhythm-quantized combat (Hi-Fi Rush–style).
- `@jgengine/shell` positional audio — `PlayableGame.audio = { sounds, buses? }` + `entitySounds`/`objectSounds` (kind/catalog-id → sound id, same convention as `entityModels`) declare looping positional emitters; the shell (`shell/audio/audioEngine`, `shell/audio/AudioComponents`) owns the `AudioContext`, attaches the listener to the camera every frame, and drives emitter gain from the core falloff curve. Music/SFX buses are plain per-bus gain nodes. No per-game Web Audio glue.
- `@jgengine/ws/voiceChannel` — a thin, coarse voice-channel router riding the same transport/presence model (channel/falloff model only, no WebRTC media). `createVoiceChannelRouter(channels?)`: `join`/`leave` any number of channels at once, `updatePosition` for proximity falloff (reusing the core audio falloff curve), `setMuted`, and `resolveRoutes(listenerUserId)` — one `{ fromUserId, channelId, gain }` per shared channel, so a positional proximity channel and a flat-gain walkie/crew channel resolve simultaneously and independently.
- `@jgengine/core/interaction/skillCheck` — a moving-target-zone, timed minigame (`evaluateSkillCheck`, `skillCheckMarkerPosition`, `skillCheckZoneAt`) for casting/reeling, active-reload, and production minigames from an `item.use` handler.
- `@jgengine/core/interaction/qte` — a timed-prompt QTE sequencer (`evaluateQteSequence`, `pendingQteStep`, `qteProgress`).
- `@jgengine/core/scene/captureCheck` — `captureChance`/`rollCapture`, an hp%+catchPower→probability formula for capture/tame mechanics.
- `@jgengine/core/scene/roster` — `createRoster()`, a persisted per-owner roster (capture/release/list/setEquipped) wired onto the runtime as `ctx.game.roster`, distinct from the ephemeral `game.social.party`.
- `@jgengine/core/stats/rollCheck` — `rollCheck`, a d20-style roll vs. DC with advantage/disadvantage and critical detection.
- Dialogue choices can now gate a branch behind a roll: `DialogueChoice.check` (+ `onSuccess`/`onFailure`) on the `@jgengine/react/components` `DialogueDef`/`DialogueChoice` types, resolved via `resolveDialogueInvoke`.
- `@jgengine/react` gained `SkillCheckBar`, `QteTrack`, and `CaptureOdds` headless minigame UI components, plus a `useRoster(userId?)` hook and extra `DialogueBox` slot classNames (`lineClassName`, `speakerClassName`, `choicesClassName`, `choiceClassName`, `checkClassName`).
- **Navigation & pointer-driven input.** A renderer-free foundation for click-to-move, RTS unit command, and pointer verbs.
  - `@jgengine/core/nav/navGrid` — a walkable grid + A* pathfinding (`createNavGrid`, `findPath`, `smoothPath`); blocked start/goal snap to the nearest walkable cell, paths are string-pulled, and one graph feeds both click-to-move and AI routing.
  - `@jgengine/core/nav/pathFollow` — an authored-polyline mover for tower-defense creeps needing no navmesh (`createPathFollow` + pure `advancePathFollow`); `pathFromNav` lifts an A* route so the same follower drives click-to-move.
  - `@jgengine/core/input/pointer` — the renderer-free `PointerHit` contract (`{ point, normal, entity, object }`) plus `aimToPoint` / `moveTargetFromHit` / `groundOf` so gameplay consumes cursor hits (aim, move-to, routing) without three.js.
  - `@jgengine/core/scene/selection` — pure box-select math (`createSelectionSet`, `screenRect`, `selectWithinRect`, `isMarquee`).
  - `@jgengine/core/interaction/contextMenu` — `contextVerb` / `buildContextMenu` / `contextVerbInput`; catalog entities/objects carry `verbs` for right-click menus.
  - `PlayableGame.pointer` config (`moveCommand`, `select`, `orderCommand`, `contextMenu`, `aim`) — the `@jgengine/shell` `GamePlayerShell` casts the cursor (`pointer.worldHit()`), renders a drag-marquee + right-click verb menu, routes primary-ability aim to the cursor, and remaps orbit to middle-drag so the left button drives verbs.
- **AI over the navmesh.** A renderer-free `ai/` domain for directors, aggro, jobs, and crowds — all ticking on game-time `dt` (the `ctx.time` simClock delta, so pause/fast-forward come free) and routing over the `nav/` navmesh.
  - `@jgengine/core/ai/spawnDirector` — a budget/escalation spawn director for wave shooters and difficulty directors (`createSpawnDirectorState` + pure `advanceSpawnDirector`). Per-`WaveManifest` budgets spent on weighted `SpawnEntry`s (`cost`/`weight`/`minWave`) under a `maxAlive` cap; `duration` auto-advances waves (or `advanceWave` on clear); budget trickles, ramps with sim-time (`escalationPerSecond`), scales per player, and surges on `raiseAlert`. Seeded/deterministic; `pickSpawnPoint` biases placement toward players.
  - `@jgengine/core/ai/threat` — an aggro/threat table (`createThreatTable`): accumulate/`decay` per source, `highest({ current, stickiness })` for sticky MMO target selection feeding `scene/targeting`, `ranked` for a threat meter.
  - `@jgengine/core/scene/behaviors` gained `patrol({ waypoints, speed, loop? })` — a waypoint route as a `BehaviorDescriptor` on top of `wander`, driven by `pathFollow` over `findPath`.
  - `@jgengine/core/ai/jobBoard` — a task/job queue NPCs pull from (`createJobBoard`): `post`/`claim`/`assign`/`release` plus a per-tick `advance(worker, dt, { distanceToStation })` state machine (`travelling → working → done`, `repeat` for production loops) that reports on completion. For colony/companion assignment (Palworld, Schedule I, Sons of the Forest).
  - `@jgengine/core/ai/crowd` — a flow field with congestion + POI routing for management sims (`computeFlowField` Dijkstra steering without per-agent A*, `createCrowdField` per-cell occupancy whose `penalty()` reroutes flow around crowds, `selectPoi` weighting appeal/proximity/capacity with a `findPath`-length distance override).
- **Dropped-item world entities & loot filter.** Loot no longer has to teleport straight to inventory — it can lie on the ground as a rarity-coded, beam-and-labelled `worldItem` you walk up to and grab (Borderlands/Diablo loot beams, ARPG ground loot, Apex/Lethal-Company pick-ups).
  - `@jgengine/core/game/worldItem` — the `worldItem` scene-entity model (position + item ref + rarity): `ctx.scene.worldItem.spawn / get / list / nearestInRadius / pickup` (a third scene bucket alongside object/entity, never merged into inventory). Pure helpers `createWorldItemStore`, `resolveDeathDrops` (splits rolled drops into scattered ground items vs direct grants), `scatterOffset` / `scatterPosition` (the on-death scatter impulse), `selectNearestWorldItem` (pickup-radius nearest selection), and `resolveWorldItemPresentation` (rarity baseline + filter overlay → render binding). Emits `worldItem.dropped` / `worldItem.picked_up`.
  - `onDeath.dropMode: "world"` (+ optional `onDeath.scatter`) routes an entity's death drops through scattered ground items instead of granting straight to the killer; currency still grants directly. Item catalog entries carry `rarity` / `baseType` read by the render binding + filter.
  - `@jgengine/core/game/lootFilter` — a PoE/Last-Epoch-style rule evaluator (`lootFilter`, `evaluateLootFilter`) keyed on rarity + base type + affix-tier that hides/recolors/beams/labels ground items. Rules are **data the game supplies**; first match wins, field-by-field over the rarity baseline. The render binding is engine-owned.
  - `PlayableGame.worldItem` config (`rarityStyle`, `filter`, `pickupRadius`, `beamHeight`) + `pointer.grabWorldItems` — `@jgengine/shell`'s `WorldItems` draws the rarity beam/color/label per drop and `GamePlayerShell` grants + despawns on a click within pickup radius (using `pointer.worldHit()`). `@jgengine/react`'s `useWorldItems()` / `useNearestWorldItem(radius)` drive a HUD pickup prompt.

- **Interactive placement, building & terraform.** Data-only placement becomes the build tooling of Valheim/Enshrouded/The Sims/Fortnite/Dinkum/Palia, all pure `@jgengine/core/world` driven by `pointer.worldHit()`, with shell renderers for the ghost/tint/brush.
  - `@jgengine/core/world/placementController` — `createPlacementController(footprint)` owns the placement ghost: `hover(hit)` → `PlacementPreview` (valid/invalid tint wrapping `validatePlacement`), `rotate`, grid/free/surface `snapMode`, `commit` → `PlacementCommit`.
  - `@jgengine/core/world/connectors` — typed connector sockets with `snapToNearest` snap-to-nearest-compatible (`socketsCompatible`, `worldSockets`).
  - `@jgengine/core/world/support` — `solveSupport` walks the connector graph to ground (`supported`/`unsupported`/hop-`distance` for white→red decay); `toDebrisBodies` sinks collapsed pieces into the `PhysicsWorld`.
  - `@jgengine/core/world/walls` — `createWallDrawTool` (drag walls → auto-enclose → `footprintFromWalls` → `autoRoof` hip/gable/flat) plus `createSurfacePaint` for per-tile floor/wall surfaces.
  - `@jgengine/core/world/placedStructureStore` — `createPlacedStructureStore` save/load/select/move/delete with a `snapshot`↔`load` round-trip that survives reload.
  - `@jgengine/core/world/terraform` — `createEditableTerrain({ bounds, base, cellSize })` makes a `TerrainField` you can **write back to** via `apply(edit)` (raise/lower/flatten/paint), and `createTerraformBrush` is the cursor tool. This height-offset grid is the shared terrain-edit write-back pattern.
  - `@jgengine/core/world/buildPermissions` — `createPlotPermissions` (per-plot/guild `BuildRole` edit authority) + `createContributionPool` (co-op pooled-resource contribution model).
  - `@jgengine/shell` renderers: `structures/PlacementGhost` (tinted ghost), `terrain/EditableGround` (renders an `EditableTerrain` with surface paint), `terrain/TerraformBrushCursor` (brush ring); the `builder-sandbox` demo game wires them to `pointer.worldHit()`.
- **Map, fog of war & contextual ping.** Minimap/world-map/fog/compass + a squad ping verb, built on renderer-free core state, a shell terrain bake, and react HUD components. (Stacked on the pointer foundation above.)
  - `@jgengine/core/world/markers` — a reactive `createMarkerSet()` of `MapMarker`s (objective/entity/loot/ping) with `add`/`remove`/`query`/`prune`/`subscribe`; `DEFAULT_MARKER_KINDS` supplies content-agnostic colors + glyphs.
  - `@jgengine/core/world/fog` — reveal-on-event fog: `createFogField({ bounds, cellSize })` with `reveal` (dig/act) and `revealAlong` (walked trail); revealed cells stay revealed and render from a stable `cells()` snapshot.
  - `@jgengine/core/world/minimap` — pure projection + bearings (`projectToMinimap`, `clampToMinimapEdge`, `compassBearing`, `headingToBearing`, `bearingToCardinal`, `relativeBearing`).
  - `@jgengine/core/game/ping` — `classifyPing(hit, …)` (hostile → enemy, tagged object → its category, ground → location) + `createPingSystem` that classifies, drops a categorized marker, and broadcasts a `PingPayload` over the existing party feed under `PING_FEED_ACTION`. `PlayableGame.pointer.pingCommand` binds the `ping` action → `worldHit()` → your command.
  - `@jgengine/react` — `useMarkers` / `useFog` hooks and `Minimap` / `Compass` / `WorldMap` headless components (bind a core `MarkerSet`/`FogField`, override the `kindStyles` palette).
  - `@jgengine/shell/map` — `bakeTerrainMap` (top-down terrain image for the map background) and `MapMarkerBeacons` world-space beacons; the `extraction-map` demo game wires the whole loop.
- **Physics joints & constraints** — `PhysicsWorld` gains `hingeJoint`/`fixedJoint`/`distanceJoint`/`springJoint(JointOptions)` between two bodies or a body and a fixed world point, plus `removeJoint`, `setJointAnchor` (move a follow point), `setJointRest`, and `readJointSegments`. The sim is translational: `hinge`/`fixed` pin the shared anchor, `distance` holds a separation, `spring` drives toward `restLength` with stiffness/damping. Foundation under vehicle suspension, ragdolls, grapples, and carry. Tune the buffers with `jointCapacity` / `jointCorrection` in `PhysicsWorldConfig`.
- **Physics collision → gameplay-event hook** — `PhysicsWorld.onCollision(listener, minApproachSpeed?)` delivers each impacting contact as a reused `CollisionEvent { a, b, nx, ny, nz, approachSpeed, impulse }` during `step`. The seam crash-damage and destruction read; contacts otherwise stay inside the sim.
- **`@jgengine/core/physics/ragdoll`** — `createRagdoll(world, { bones, links, balance? })` builds a jointed multi-body character on the joint API: floppy by default, or active-ragdoll when a balance motor drives the root toward a target height (`Ragdoll.balance(dt, moveX?, moveZ?)`), with `centerOfMass`, `applyImpulse`, `remove`.
- **`@jgengine/core/physics/carryable`** — `Carryable` grabs a physics body to a moving follow point via a spring constraint (the raycast pick is the caller's), supports shared multi-owner carry (follow point = owner average), drop/throw, and `carrySpeedMultiplier(mass, capacity, owners)` encumbrance.
- **`@jgengine/core/physics/forceVolume`** — `ForceVolume` (impulse/velocity/accelerate trigger region, `once` for boost pads vs continuous fans) and `PlatformCarry` (carry bodies standing on a moving platform by its per-`step` delta).
- **`@jgengine/core/physics/spatialGrid`** — `SpatialGrid`, a broad-phase uniform grid over the x/z plane, separate from the rigid-body sim, for cheap same-tick proximity across hundreds–thousands of simple movers: `rebuild(count, xs, zs)` then `queryCircle` (swarm enemies hitting a player/AoE) or `forEachPair` (mutual separation).
- **`@jgengine/shell/world/InstancedJoints`** — debug LineSegments overlay drawing a `PhysicsWorld`'s active joints from `readJointSegments`.
- **`@jgengine/core/physics/traversal`** — `Grapple` (a fired-anchor rope on the joint API: `fire`/`reel`/`payOut`/`moveAnchor`/`release` for grapple, zipline, and swing traversal) and `Glide` (reduced-gravity, forward-thrust wingsuit/glider via `apply(dt, steerX, steerZ)` before `step`). Grapple/zipline/glide (Sekiro, Deep Rock, Enshrouded) over the shared physics sim.
- **`@jgengine/core/world/carve`** — runtime destructible terrain. `VoxelVolume` is an editable dense voxel grid with `carve`/`deposit` sphere ops honouring per-material `strength` against a tool (Deep Rock dig, Astroneer deposit); `CarvableField` / `carvableTerrain(base)` writes craters and mounds back into any `TerrainField`'s height so ground-snap, collision, and the terrain mesh all see the deformation (Helldivers 2 craters).
- **`@jgengine/core/physics/structure`** — `StructureGraph`, a structural-integrity graph over a building: nodes (pieces), load-bearing edges, anchored foundations. `damage`/`damageEdge`/`severEdge` recompute reachability to an anchor and return one `CollapseEvent` of every newly-disconnected piece; `toDebris(world, event)` sinks the fallen pieces into a `PhysicsWorld` as rigid bodies. Coarse by design — the collapse **event** replicates, not each fragment (The Finals, Rainbow Six).
- **`@jgengine/shell/terrain/CarvedTerrain`** — meshes any `TerrainField` (a `CarvableField` with its craters/mounds) into a vertex-coloured deformed ground; `createFieldGroundGeometry` is the underlying geometry builder.
- **Analog axis input** (`@jgengine/core/input/axisInput`) — `AxisInput { throttle, brake, steer, handbrake }` continuous channel, distinct from the digital action bindings. `AxisChannel` ramps held keys into pedal-like analog values (`sample`) or takes a raw gamepad axis (`setAnalog`); `DRIVE_AXIS_BINDINGS` is a ready WASD/arrow map.
- **Vehicle controller** (`@jgengine/core/physics/vehicleBody`) — `createVehicleBody(world, config)`: chassis body + per-wheel suspension raycast + spring-damper (on `springJoint`) + a `GripCurve` (`sampleGripCurve`) that bleeds lateral velocity for cornering and, under handbrake, drift. Driven by an `AxisInput`; still a colliding body, so contact feeds crash damage. (Rocket League, Trackmania, Wreckfest.)
- **Buoyant boat** (`@jgengine/core/physics/buoyancy`) — `createBuoyantBody(world, { body, water, … })` floats a body on a CPU `waterSurface` (Archimedes per hull point + water drag) and, given an `AxisInput`, drives it as a boat (thrust + yaw + keel).
- **Mount / rideable controller** (`@jgengine/core/scene/mount`) — `createMountController()` transfers camera + input to a driven entity with its own movement kit: `register({ id, kit, seats })`, `mount`/`dismount`, `cameraTarget`/`driveTarget`, `driver`/`occupants`. Control seat drives, passenger seats ride (multi-seat shared vehicles). (Palworld, V Rising, Sea of Thieves.)
- **Crash-damage stages** (`@jgengine/core/physics/damageZones`) — `createDamageModel({ zones, disableAt })` maps accumulated `onCollision` impulse to coarse discrete stages (`absorb`/`routeCollision`), with an optional `detachStage` (part → debris) and a `disabled` threshold. Coarse stages, not soft-body. (Wreckfest.)
- **Race state machine** (`@jgengine/core/game/race`) — `raceTrack({ checkpoints, laps })` + `createRaceState({ track, win })` emit `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished` on game time, keep split times, resolve a pluggable win condition (`firstPastPost`, `topK`, `everyoneFinishes`, `lastStanding`), and `resetToCheckpoint`. (Trackmania, Mario Kart, Fall Guys.)
- **Lag-compensated hit registration** (`@jgengine/core/multiplayer/lagCompensation`) — `createPositionHistory({ historyMs })` retains an N-sample position ring per entity; `rewindTimestamp(now, rtt, interpDelay)` and `resolveHitscan(history, targets, ray, atMs)` (ray-sphere) register a shot where the target *was* at the shooter's perceived time. Coarse server-side rewind, not full rollback. The `@jgengine/node` ws host records accepted presence poses and exposes `server.rewind({ serverId, atMs })` (`positionHistoryMs` option). (Valorant, Apex.)
- **Simultaneous hidden-commit + reveal** (`@jgengine/core/multiplayer/simultaneousCommit`) — `createCommitRound({ participants })`: each player `seal`s a sealed action, nothing is readable until `allSealed()`, then `reveal()` returns commits in deterministic participant order (independent of network arrival) for `resolveCommits`. (Marvel Snap.)
- **Combat-snapshot replay** (`@jgengine/core/multiplayer/combatSnapshot`) — `serializeBoard({ ownerId, units, stats, seed })` deep-freezes a build into a portable `BoardSnapshot`; `replayCombat(a, b, rules)` resolves it deterministically (seeded PRNG) against a live opponent's snapshot — an async-PvP primitive distinct from the live-sync adapters. (The Bazaar.)
- **Shared-vehicle stations** (`@jgengine/core/scene/stationClaim`) — `createStationClaim(controller?)` layers facet stations (`steer`/`sails`/`cannon`) on `scene/mount`: `register({ id, kit, stations })`, `claim`/`release`, `controllerOf(vehicleId, facet)`, `facetOf`, `openFacets`, `crew`. One control station drives the hull; the rest ride but command their facet. (Sea of Thieves.)
- **Shared / group wallet** (`@jgengine/core/economy/sharedWallet`) — `createWalletBook()` holds per-`WalletScope` balances (`userScope`/`groupScope`) beside the per-user `economy/wallet`: `grantTo`/`chargeFrom`/`balanceIn`, with a `contributionOf`/`contributorsOf` ledger tracking who funded a shared pool. (Schedule I company funds, Lethal Company quota.)
- **Session matchmaking** (`@jgengine/core/multiplayer/matchmaking`) — data-driven browse/filter (`browseSessions`, `matchesFilter`, `quickMatch`) hiding private/closed lobbies, plus join-by-code (`findByJoinCode`, `normalizeJoinCode`, `generateJoinCode`). The node host carries generic `SessionAttributes` (`label`/`mode`/`visibility`/`joinCode`/`tags`) on `GameServerRecord`/`ServerListing` and gains `browseServers` + `joinByCode`; the ws backend exposes `browse` / `joinByCode` / `createSession`. (Fortnite island browse, Web Fishing code lobbies.)
- **Camera rig library** (`@jgengine/shell`, config types in `@jgengine/core/game/playableGame`) — the single orbit camera is now one of eight rigs, selected and tuned through `PlayableGame.camera` (never by writing camera positions from `onTick`). Set `camera.rig`:
  - `topDown` — fixed height/pitch/yaw with decoupled follow for ARPG-iso and top-down (Diablo IV/PoE 2/Last Epoch, Hades II); `camera.topDown: { height, pitch, yaw, followSmoothing, zoom }`.
  - `rts` — free-pan / edge-scroll / rotate / zoom independent of any avatar (The Sims 4, Manor Lords, Two Point Museum); `camera.rts: { panSpeed, edgeScroll, rotateSpeed, bounds, start }`.
  - `shoulder` — over-the-shoulder with ADS transition and shoulder-swap, reticle decoupled from camera center (Remnant II, Helldivers 2, The First Descendant); `camera.shoulder: { shoulderOffset, distance, ads, side }`.
  - `lockOn` — yaw bound to the player→target vector with the move axis reinterpreted as strafe (Elden Ring, Sekiro); `camera.lockOn: { targetEntityId?, distance, framingBias, yawSmoothing }`.
  - `chase` — speed-reactive vehicle chase (speed→FOV curve, spring-arm damping, procedural shake) plus fixed `cockpit`/`hood`/`rear` views (Forza Horizon 5, Rocket League, Trackmania); `camera.chase: { distance, springDamping, fov, shakePerSpeed, view }`.
- **Every rig accepts `followEntityId: null`** — avatar-less games (city-builders, card games, auto-battlers) can now mount a camera; the orbit rig no longer bails when there is no follow target.
- **Camera-shake / trauma channel** — every rig reads a shared trauma channel; feed it from any system with `import { cameraShake } from "@jgengine/shell/camera"` (`cameraShake(amplitude, decayPerSecond?)`, amplitude 0..1) or from React via `useCameraShake()`. Tune with `camera.shake: { maxOffset, maxRoll, decayPerSecond, exponent, frequency }`. (Combat hitstop and other systems feed the same channel.)
- **Cinematic camera + mode-swap cross-fade** — `camera.cinematic: { keyframes: [{ position, lookAt, fov?, duration?, ease? }], loop? }` plays a scripted keyframe path over the active rig, and `camera.transitionSeconds` cross-fades the camera when the rig changes so mode swaps no longer hard-cut.
- Pure rig math (shake decay/trauma, spring-arm damping, speed→FOV, lerp/cross-fade, offset/strafe vectors, keyframe sampling) is exported from `@jgengine/shell/camera` as testable functions.
- **Sensors, vision & observer tools** (`@jgengine/core/sensor/*`, renderers in `@jgengine/shell/vision` and `@jgengine/shell/replay`) — a coherent "query hidden/tagged/framed world state and surface it" family:
  - **Reveal vision** (`sensor/revealQuery`) — `createRevealQuery` is an occlusion-ignoring tagged-entity radius query (`inRadius` already never checks occlusion; this scopes it to catalog-declared tags for a vision readout) paired with a toggleable screen-space reveal effect (`shell/vision/RevealVision`: `RevealHighlights` for through-wall 3D highlights, `RevealScreenTint` for the full-screen tint) — Dark Sight / detective-vision / wallhack-style highlight (Hunt: Showdown).
  - **Hidden-state probe** (`sensor/hiddenStateProbe`) — `probeHiddenState`/`probeHiddenStateAll` read a hidden zone/entity state variable in range and surface a distance-weighted reading; `shell/vision/HiddenStateProbeHud`'s `SensorReadoutMeter` renders it as a handheld sensor needle (EMF reader/spirit box/thermometer/geiger, Phasmophobia).
  - **View-frustum sensor** (`sensor/frustumSensor`) — `projectToView`/`framingScore`/`createFrustumSensor` answer "what's in this held camera's view, how well framed, and for how long" (dwell time resets the instant a subject leaves frame); `shell/vision/FrustumSensorHud`'s `FrustumSensorReadout` drives it off the live render camera for a photo-mode HUD (Content Warning).
  - **Session-recording buffer** (`sensor/recordingBuffer`) — `createRecordingBuffer` appends timestamped snapshots on game-time and seeks/scrubs them for replay, photo mode, or kill-cam; `shell/replay/useSessionRecorder` records an entity's pose every frame.
  - **`observer` camera rig** (config in `@jgengine/core/game/playableGame`: `ObserverCameraConfig`) — a detached spectator/photo cam bound to any entity or fixed point that reads no player input at all (van CCTV spectate, Forza-style photo mode free cam, Trackmania ghost/kill-cam).

- **Possession** (`@jgengine/core/scene/possession`, `createPossession`) — a player can own N scene entities and switch which one is under active control, distinct from the social party. `ctx.player.possession.own/disown/owns/listOwned(userId, entityId)` tracks ownership; `possess(userId, entityId)` swaps active control (rejecting entities the user doesn't own), flips the previous/next entity's `EntityRole` between `"player"`/`"npc"` (reusing entity control, not forking it), and emits `possession.swapped`. `active(userId)` defaults to `userId` itself until a swap happens. `@jgengine/shell`'s `GamePlayerShell` rebinds WASD movement, targeting, hotbar `from`, and the camera rig's `followEntityId` to the active possessed entity on every swap — no per-game camera glue required.
- **Form / shapeshift** (`@jgengine/core/scene/form`, `createForms`) — a `form` bundles movement params + an ability-id list + a mesh (reusing the entity's catalog name, so mesh, movement defaults, and receive/role all follow the swap through the existing name-keyed resolution — no parallel mesh system). `ctx.scene.entity.form.register(defs)` in `onInit`; `shapeshift(instanceId, formId, durationSeconds?)` applies the bundle and optionally reverts automatically after `durationSeconds` of **game time** (`ctx.time.after`, so it obeys pause/fast-forward); `revert(instanceId)` reverts early. Emits `form.changed`.
- **Cosmetic loadouts + emote broadcast** (`@jgengine/core/game/cosmetics` `createCosmetics`; `@jgengine/core/game/social` `Social.emotes`) — `ctx.player.cosmetics.register(defs)` + `apply(userId, loadoutId)` / `equip(userId, slot, cosmeticId)` manage a per-player cosmetic slot map (skin/back/aura/…), emitting `cosmetics.changed`. `ctx.game.social.emotes.play(fromUserId, emoteId, radius?)` broadcasts to nearby **player**-role entities (reusing `scene.entity.inRadius`, not a parallel proximity system) and emits `emote.played` — bind it through the existing `ctx.game.feed` primitive (`feed.bind("emote.played")`) for a HUD feed, no new hook needed.
- `entityStore`'s `update()` patch now also accepts `name`, so possession/form (and any future system) can retarget an instance's catalog id without despawn/respawn.

An additive layer over effects/projectiles/death that adds melee/action feel. Every model is a renderer-free `@jgengine/core` factory a game composes per entity; `@jgengine/shell` renders the world/HUD side. No existing API moved.

- **Animation state machine** — `@jgengine/core/combat/animationState`: `createAnimationState({ clips })` over `AnimationClip`s whose `FrameRange`s tag `windup | active | recovery | cancel` windows. The root contract combat/defense subscribe to (`inPhase`, `isActive`, `canCancel`, `activeWindowMs`).
- **Shared accumulator meter** — `@jgengine/core/stats/accumulatorMeter`: `createAccumulatorMeter({ max, mode, decayPerSecond, decayDelayMs, tiers })`, the fill/decay/threshold-fire/tier primitive. `@jgengine/core/combat/breakMeters` builds `createStaggerMeter` (poise/posture break → riposte) and `createBuildupMeter` (bleed/frost/rot proc) on it.
- **Defensive window + attack tags** — `@jgengine/core/combat/defensiveWindow` (`resolveDefense` / `createDefensiveWindow`, parry/block/dodge evaluated against the attacker's active frames) reading `@jgengine/core/combat/attackTags` (`attackMeta`, unblockable/thrust/sweep/grab; `counters` for Mikiri-style reads).
- **Combo strings** — `@jgengine/core/combat/comboString`: `advanceCombo` / `createComboRunner` — ordered attacks with stance-conditioned cancel points over the animation SM.
- **Dash / dodge** — `@jgengine/core/movement/dash`: `createDashState` — directional burst + i-frame window + stamina/cooldown.
- **Hit reaction, telegraphs, typed damage numbers** — `@jgengine/core/combat/hitReaction` + `ctx.scene.entity.hitReaction(...)` (knockback impulse + hitstop + `combat.hitReaction` shake channel); `@jgengine/core/combat/telegraph` + `ctx.scene.entity.telegraph(...)` (windup→activation ground decal bound to an effect, drawn by the shell); `ctx.scene.entity.floatText({ crit, element, hitType, scale })` styled by `@jgengine/shell/world/floatTextStyle`.

Genre systems over the existing effects/projectiles/targeting/loot primitives. Every model is a renderer-free `@jgengine/core` factory the game ticks on game-time `dt`; `@jgengine/react` adds four-state slot binding hooks. No existing API moved. The ult/streak meters build on the `stats/accumulatorMeter` from the combat-feel layer above.

- **Ability kit** — `@jgengine/core/combat/abilityKit`: `createAbilityKit([{ id, cooldownMs, chargesMax?, resourceCost?, castType? }])` models an ability slot **separate from an inventory item**, exposing the four HUD states `ready | cooldown | no-resource | just-cast` plus charges + cooldown fraction. Resource-agnostic — reports `no-resource` against a supplied `resourceAvailable`, the game spends. (MOBA/ARPG action bars, hero-shooter kits.)
- **Event-fed meters** — `@jgengine/core/stats/eventMeter`: `createEventMeter({ max, mode, gains, resets?, tiers? })` on the shared accumulator. Mode `"hold"` = ult/adrenaline charge (`feed` combat tags, `ready()`, `consume()`); mode `"reset"` = kill-streak/combo with tiered thresholds that resets on a break tag. (Overwatch/Marvel Rivals ult, Returnal/DMC streak.)
- **Auto-target policy** — `@jgengine/core/scene/autoTarget`: `selectAutoTarget` / `createAutoTargeter` — zero-input per-tick target selection `nearest | farthest | random | strongest | weakest | first | last` (path-progress aware). (Vampire Survivors auto-fire, Bloons tower priority.)
- **Resistance matrix** — `@jgengine/core/combat/resistance`: `resolveResistance` / `resistanceScale` — damage-category × target-property → `immune | resist | normal | vulnerable` multiplier over the `receive` gate. (Bloons pop-types, elemental RPG weaknesses.)
- **Run draft** — `@jgengine/core/game/runDraft`: `createRunDraft` / `createRunModifierStack` — pause, present N weighted picks (`pickWeighted`), choose, stack the modifiers for the run (aggregated onto `stats/statModifiers`). (Vampire Survivors level-ups, Hades boons.)
- **React** — `@jgengine/react` `useAbilitySlots` / `useAbilitySlot` (four-state snapshots) and `useEventMeter` (ult/streak bar view).

- `@jgengine/core/item/durability` — per-instance item durability + repair. A catalog `DurabilitySpec` (`max`, `wearPerUse`/`wearPerHit`, `disableAtZero`, `repair`); `createDurability`/`wear`/`isDisabled`/`durabilityFraction` for the wear loop, `repairQuote(spec, state, { station?, to? })` for a quote-then-apply repair (material cost scaled by points restored, optional `qualityLossPerRepair` shrinking `max`), and `createDurabilityTracker()` to hold state per instance id. For weapon/tool/armor degradation repaired at stations.
- `@jgengine/core/item/affix` — rarity-weighted procgen roller. `createAffixRoller({ pools, rarities })` turns `base × rarity` into `{ rolled affixes, computed stats, name }`: draws `affixCount` distinct affixes without replacement (weighted via the engine `pickWeighted`), computes stats (base × rarity scale, then `add` then `mul` affixes), and composes a name from rarity + prefix/suffix parts. `seededRng(seed)` gives deterministic drops. For looter-shooter / ARPG generated weapons.
- `@jgengine/core/item/modularItem` — parts-in-typed-slots assembly. `ModularItemDef` with category-constrained `MountSlotDef`s; `install`/`uninstall` validate slot + category + occupancy, `computeEffectiveStats` rolls part `stats` (additive) then `multipliers` over the frame's `baseStats`, `missingRequiredSlots`/`isComplete` gate a buildable whole; `createModularItem(def)` is the stateful wrapper. For piece-by-piece guns and mech loadouts.
- `@jgengine/core/inventory/storageTier` — tiered extraction-economy inventory. A `tier: "carried" | "banked"` on inventory containers (`InventoryDeclaration.tier`); `partitionOnDeath` splits a death snapshot into kept (banked) vs lost (carried) with merged stacks, `createDeliveryQueue()` is the delayed-delivery (insurance) hook (`schedule`/`due`/`claimDue` on the game clock), `insureLost` filters the lost set to insured items with a delayed `deliverAt`, and `resolveConsolation` yields a baseline loadout id (apply via `applyLoadout`) for the post-death consolation grant. The inventory foundation the extraction session/round machines build on.
- `InventoryDeclaration` (`@jgengine/core/game/defineGame`) gained an optional `tier?: StorageTier` flag so containers declare carried-vs-banked storage directly.
- `@jgengine/core/session/contestedChannel` — the interrupt-on-damage progress objective (plant/defuse, cash-out, urn deposit, hold-to-extract). `createContestedChannel({ duration, interruptOnDamage?, resetOnInterrupt?, favorability?, ratePerOccupant?, contested?, decayRate? })`: `start(team)`, `tick(dt, occupants)` (per-team occupancy → `start`/`tick`/`contested`/`paused`/`complete` events), `damage()` interrupts. `favorability`/`ratePerOccupant` scale the fill rate; `contested: "pause" | "decay"` handles a contesting team.
- `@jgengine/core/session/roundState` — the buy→live→end match machine. `createRoundState({ phases, teams, maxRounds?, winReward?, lossBonus? })`: `tick(dt)` runs phase timers and auto-advances rounds, `concludeRound(winner)` settles win + escalating loss-bonus economy (`lossBonusFor`), `onPhaseEnd(hook)` gates commerce/spawns, `match.end` fires at `maxRounds`.
- `@jgengine/core/combat/downed` — the alive→downed→dead revive chain. `createDownedState({ bleedoutSeconds, reviveSeconds?, reviveHealthFraction?, banner? })`: `down`/`tick` (bleedout → `died` + optional banner), `revive(id, dt)` accumulates ally hold time, `finish` executes, `respawnFromBanner` beacons back. Sits in front of engine death resolution.
- `@jgengine/core/session/ring` — the shrinking battle-royale safe zone. `RingConfig` = `{ center, phases }`; `ringSampleAt(config, t)`/`createRing` give the live `{ center, radius, damagePerSecond }` (interpolated on the game clock), `isOutside`/`distanceOutside`/`damageOutside(t, dt, positions)` for out-of-bounds DoT.
- `@jgengine/core/session/extraction` — the raid-scoped extract-to-bank session, composed from the contested channel + `inventory/storageTier`. `createRaidSession({ extracts, insurance?, consolation? })`: `beginExtract`/`tickExtract`/`damage` drive hold-to-leave, `resolveExtraction` banks everything carried, `resolveDeath` partitions/insures/consoles via storage tiers, `claimDeliveries(now)` drains insured returns.
- `@jgengine/core/runtime/persistenceScope` — run-vs-meta persistence split with reset boundaries. `partitionScopes`/`resetRun`/`mergeScopes` over flat records, `clearRunFields`/`applyRunReset` over player rows/profiles, `planScenarioReset(...)` normalizes a season/scenario wipe applied through the new optional `HostPersistence.resetScenario` — implemented by `@jgengine/sql` (deletes a server's chunks + session and run-resets each profile in one transaction, keeping account meta).


## 0.6.0

An additive release: **every 0.5.0 API is unchanged**, so upgrading is only a
version bump. The headline is a whole outdoor-world layer — composable
`environment(...)` descriptors, renderer-free query primitives so gameplay reads
the same world the shell renders, a standalone rigid-body physics sim, and the
`@jgengine/shell` renderers that draw it all.

### Migrate

- Bump every `@jgengine/*` dependency to `^0.6.0` (the eight packages version in lockstep).
- No code change is required — 0.6.0 adds surface, it doesn't move or remove any.
- Optional: describe an outdoor scene once with `environment({ terrain, weather, vegetation, water, structures })` from `@jgengine/core/world/features`. `@jgengine/shell`'s `EnvironmentScene` renders it (terrain, rain/snow, grass, ocean, buildings), and gameplay reads the same world through the renderer-free primitives — `resolveTerrainField(...)` for ground-snap/collision, `windField(...)` for sway/sailing, `waterSurface(...)` for buoyancy, `scatter(...)` for prop/spawn placement, `buildingIndex(...)` for placement avoidance — no three.js needed.

### Added

- `@jgengine/core/world` query primitives — pure, renderer-free world sampling so gameplay and rendering read one source of truth. `world/terrain` (`TerrainField`, `noiseField`, `resolveTerrainField`, `resolveGroundStep` for slope-limited movement), `world/wind` (`windField` — one wind source for weather sway, grass, sailing, fire spread), `world/water` (`waterSurface` / `waterSurfaceFromDescriptor` — CPU Gerstner matching the ocean shader, for buoyancy and shorelines), `world/scatter` (`scatter` — seeded, overlap-aware point distribution for vegetation/props/spawns), `world/buildings` (`generateBuilding`, `generateBuildingDistrict`) and `world/buildingIndex` (`buildingIndex` — `at`/`within`/`nearest`/`isInside`/`blockers` over a district).
- `@jgengine/core/world/features` composable outdoor descriptors — `environment(...)` plus `terrain()`, `rain()`, `snow()`, `grass()`, `ocean()`, `building()`.
- `@jgengine/core/world/regions` (`createRegionField` — blend content-agnostic biomes by nearest selector, adding `tint`/`water`/`fog`/`speedMultiplier`/opaque `data`; extends `TerrainField` so it ground-snaps too) and `@jgengine/core/world/scatterItems` (`scatterItems` — region-driven content scatter: density per region, grounded, above-water/slope-aware, with `pickWeighted` for weighted rolls).
- `@jgengine/core/physics/physicsWorld` — standalone fixed-capacity rigid-body sim (SoA buffers, spatial-hash broadphase, sleeping) for many colliding dynamic bodies (piles, debris, stress scenes). Distinct from the `defineGame` `physics: { gravity }` character-controller config.
- `ctx.time` simulation clock (`@jgengine/core/time/simClock`) — `onTick`'s `dt` is now **game time**, so `rate * dt` decay/regen/AI obeys pause and fast-forward for free without per-system wiring. Configure with `defineGame({ time: { scale?, speeds?, dayLength?, start?, startPaused? } })` (all optional; default is real-time 1:1). `ctx.time` exposes `pause`/`play`/`toggle`/`setSpeed`/`cycleSpeed` + `snapshot()`/`calendar()`; `useGameClock()` binds it in React.
- `@jgengine/shell` environment/terrain/water/weather/structures renderers — `EnvironmentScene` mounts an `environment()` descriptor as R3F renderers; `GrassField`, `ProceduralGround`, `Ocean`, `RainField`, `SnowField`, `LightningStrike`, `GeneratedBuilding`, and `world/InstancedBodies` (renders `PhysicsWorld` bodies).
- `@jgengine/react/liveBind` — `useFrameBind` drives a DOM/SVG element from a per-frame value without re-rendering React (HUDs bound to live engine state never re-render or lag).

## 0.5.0

An additive release: **every 0.4.0 API is unchanged**, so upgrading is only a
version bump. New pure primitives across progression, inventory slots, world
geometry, and React store bindings.

### Migrate

- Bump every `@jgengine/*` dependency to `^0.5.0` (the eight packages version in lockstep).
- No code change is required — 0.5.0 adds surface, it doesn't move or remove any.
- Optional: replace a game's hand-rolled `progression/curves.ts` with the new `leveling(...)` track. `leveling({ xpForLevel: { kind: "power", base, exponent, round: "floor" }, maxLevel })` returns `xpForLevel`, `resolve`, and `grantXp(ctx.scene.entity.stats, userId, amount, onLevelUp?)` — a drop-in for the old `xpRequiredForLevel` / `resolveLevelProgress` / `grantXp` exports. `ctx.scene.entity.stats` satisfies the primitive's `LevelingStatAccess` structurally, so no adapter is needed.

### Added

- `@jgengine/core/game/progression` — genre-agnostic progression primitive. `curve(spec)` / `evalCurve(spec, x)` evaluate declarative scalar curves (`const`, `linear`, `power`, `geometric`, `steps`, `piecewise`, each with optional `round`/`min`/`max`) for speed-by-level, difficulty-by-wave, loot drop-rate ramps, and similar scaling. `leveling(config)` builds the stateful XP→level track (threshold accumulation, multi-level grants, cap handling, `stat.levelUp` emit) on top of an `xpForLevel` curve.
- `@jgengine/core/inventory/slotModel` — pure slot-grid primitives (`createSlots`, `placeAt`, `removeAt`, `moveSlot`).
- `@jgengine/core/world/geometry`, `/world/interiors`, `/world/placement` — pure world primitives: grid snapping, footprint AABBs and overlap, interior/exterior spaces, and placement validation.
- `@jgengine/react/engineStore` — raw-store React bindings (`useEngineState`, `useEngineStore`, `useEngineEvent`).
- Pure/functional tiers for the `trade`, `unlocks`, `quest`, and `feed` verbs in `@jgengine/core/game`.

## 0.4.0

Baseline release: the eight `@jgengine/*` packages (core, ws, sql, react,
convex, node, shell, assets) as the first tracked version. No migration —
this is the floor changelog entries are measured against.
