@jgengine/core 0.15.0-next.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -28,6 +28,17 @@ At publish, rename this heading to the new version and mirror the entries into
28
28
 
29
29
  ### Migrate
30
30
 
31
+ ### Changed
32
+
33
+ ### Added
34
+
35
+ ### Fixed
36
+
37
+ ## 0.15.0
38
+
39
+ ### Migrate
40
+
41
+ - **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`.
31
42
  - **`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`.
32
43
  - **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.
33
44
  - **Every game must now declare its run-phase story — silence is a gate error (#1337).** The run-phase
@@ -73,10 +84,16 @@ At publish, rename this heading to the new version and mirror the entries into
73
84
 
74
85
  ### Added
75
86
 
87
+ - **`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.
88
+ - **`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.
89
+ - **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.
90
+
76
91
  - **`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).
77
92
 
78
93
  ### Fixed
79
94
 
95
+ - **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.
96
+
80
97
  - **`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.
81
98
  - **`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.
82
99
  - **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.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Hysteretic music-state selector: turns a continuous intensity signal (nearby threat,
3
+ * alert count, chase proximity) into a stable theme id. The engine can already crossfade
4
+ * a soundtrack; this decides *when*, without the flapping a bare threshold produces when
5
+ * one enemy walks in and out of range.
6
+ */
7
+ export interface MusicTier {
8
+ id: string;
9
+ /** Theme id passed to `ctx.game.audio.music`; null silences the soundtrack in this tier. */
10
+ theme: string | null;
11
+ /** Intensity at or above which this tier may be entered. */
12
+ enter: number;
13
+ /** Minimum time in this tier before it may drop to a lower one. Default 0. */
14
+ holdMs?: number;
15
+ }
16
+ /** The tier ladder plus the release margin that keeps a hovering intensity from flapping between tiers. */
17
+ export interface MusicStateConfig {
18
+ /** Ordered or unordered; the highest `enter` at or below the intensity wins. */
19
+ tiers: readonly MusicTier[];
20
+ /** Intensity must fall this far below a tier's `enter` before leaving it. Default 0.2. */
21
+ release?: number;
22
+ }
23
+ /** Which tier is playing and when it was entered, on the caller's clock. Serialisable. */
24
+ export interface MusicStateSnapshot {
25
+ tierId: string | null;
26
+ /** Timestamp the current tier was entered, on the caller's clock. */
27
+ sinceMs: number;
28
+ }
29
+ /** The current intensity signal and clock reading for one resolve. */
30
+ export interface MusicStateInput {
31
+ intensity: number;
32
+ nowMs: number;
33
+ }
34
+ /** Next state, the theme that should be playing, and whether this advance is the one that changed it. */
35
+ export interface MusicStateResult {
36
+ state: MusicStateSnapshot;
37
+ theme: string | null;
38
+ /** True only on the advance the tier changed — the caller's cue to crossfade. */
39
+ changed: boolean;
40
+ }
41
+ /** Fresh state with no tier entered, so the first resolve always settles and reports `changed`. */
42
+ export declare function createMusicState(): MusicStateSnapshot;
43
+ /**
44
+ * Pick the theme for the current intensity. Escalation is immediate; de-escalation waits for the
45
+ * tier's `holdMs` and needs the intensity to fall a `release` margin below the tier it is leaving.
46
+ */
47
+ export declare function resolveMusicState(state: MusicStateSnapshot, input: MusicStateInput, config: MusicStateConfig): MusicStateResult;
@@ -0,0 +1,38 @@
1
+ /** Fresh state with no tier entered, so the first resolve always settles and reports `changed`. */
2
+ export function createMusicState() {
3
+ return { tierId: null, sinceMs: 0 };
4
+ }
5
+ function tierById(config, id) {
6
+ return id === null ? undefined : config.tiers.find((tier) => tier.id === id);
7
+ }
8
+ function highestTierAt(config, intensity) {
9
+ let best;
10
+ for (const tier of config.tiers) {
11
+ if (intensity < tier.enter)
12
+ continue;
13
+ if (best === undefined || tier.enter > best.enter)
14
+ best = tier;
15
+ }
16
+ return best;
17
+ }
18
+ /**
19
+ * Pick the theme for the current intensity. Escalation is immediate; de-escalation waits for the
20
+ * tier's `holdMs` and needs the intensity to fall a `release` margin below the tier it is leaving.
21
+ */
22
+ export function resolveMusicState(state, input, config) {
23
+ const current = tierById(config, state.tierId);
24
+ const candidate = highestTierAt(config, input.intensity) ?? config.tiers.find((tier) => tier.enter <= 0);
25
+ if (current !== undefined && candidate !== undefined && candidate.enter < current.enter) {
26
+ const release = config.release ?? 0.2;
27
+ const heldLongEnough = input.nowMs - state.sinceMs >= (current.holdMs ?? 0);
28
+ if (!heldLongEnough || input.intensity > current.enter - release) {
29
+ return { state, theme: current.theme, changed: false };
30
+ }
31
+ }
32
+ const next = candidate ?? current;
33
+ if (next === undefined)
34
+ return { state, theme: null, changed: false };
35
+ if (next.id === state.tierId)
36
+ return { state, theme: next.theme, changed: false };
37
+ return { state: { tierId: next.id, sinceMs: input.nowMs }, theme: next.theme, changed: true };
38
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Distance-driven footstep cadence. Pure state-in/state-out: feed it how far a mover
3
+ * travelled this frame and it tells you how many steps landed, so foley stays locked to
4
+ * ground distance instead of a wall-clock timer that desyncs when speed changes.
5
+ */
6
+ export interface StepCadenceConfig {
7
+ /** World units travelled per step at `strideScale` 1. */
8
+ strideDistance: number;
9
+ /**
10
+ * Fraction of a stride the first step after standing still lands at, so walking off
11
+ * from rest is heard immediately rather than half a stride later. Default 0.5.
12
+ */
13
+ firstStepFraction?: number;
14
+ }
15
+ /** Cadence bookkeeping between advances — banked distance, last moving state, and the step count whose parity is the foot. */
16
+ export interface StepCadenceState {
17
+ /** Distance banked toward the next step. */
18
+ travelled: number;
19
+ /** Whether the mover was moving last advance — drives the from-rest first step. */
20
+ moving: boolean;
21
+ /** Monotonic step counter; its parity is the left/right foot. */
22
+ steps: number;
23
+ }
24
+ /** What the mover did this advance: ground distance covered, whether feet are planting, and the stride multiplier. */
25
+ export interface StepCadenceInput {
26
+ /** Ground distance covered since the last advance, in world units. */
27
+ distance: number;
28
+ /** False when the mover is airborne, downed, or otherwise not planting feet. */
29
+ moving: boolean;
30
+ /** Multiplies `strideDistance` — crouch shortens it, sprint lengthens it. Default 1. */
31
+ strideScale?: number;
32
+ }
33
+ /** Outcome of one advance: the next state plus how many steps landed and which foot led. */
34
+ export interface StepCadenceResult {
35
+ state: StepCadenceState;
36
+ /** Steps that landed this advance — 0 or 1 normally, more only on a very long frame. */
37
+ steps: number;
38
+ /** Parity of the first step landed this advance: 0 = left, 1 = right. */
39
+ foot: 0 | 1;
40
+ }
41
+ /** Fresh cadence state for a mover that is standing still. */
42
+ export declare function createStepCadenceState(): StepCadenceState;
43
+ /**
44
+ * Bank this advance's distance and report the steps it completed. Pure — same state and input
45
+ * always give the same result, so footstep timing is testable headless and replays identically.
46
+ */
47
+ export declare function advanceStepCadence(state: StepCadenceState, input: StepCadenceInput, config: StepCadenceConfig): StepCadenceResult;
@@ -0,0 +1,29 @@
1
+ /** Fresh cadence state for a mover that is standing still. */
2
+ export function createStepCadenceState() {
3
+ return { travelled: 0, moving: false, steps: 0 };
4
+ }
5
+ /**
6
+ * Bank this advance's distance and report the steps it completed. Pure — same state and input
7
+ * always give the same result, so footstep timing is testable headless and replays identically.
8
+ */
9
+ export function advanceStepCadence(state, input, config) {
10
+ const stride = Math.max(config.strideDistance * (input.strideScale ?? 1), Number.EPSILON);
11
+ if (!input.moving) {
12
+ return { state: { travelled: 0, moving: false, steps: state.steps }, steps: 0, foot: (state.steps % 2) };
13
+ }
14
+ const fromRest = !state.moving;
15
+ const threshold = fromRest ? stride * (config.firstStepFraction ?? 0.5) : stride;
16
+ let travelled = state.travelled + Math.max(0, input.distance);
17
+ let steps = 0;
18
+ let remaining = threshold;
19
+ while (travelled >= remaining) {
20
+ travelled -= remaining;
21
+ steps += 1;
22
+ remaining = stride;
23
+ }
24
+ return {
25
+ state: { travelled, moving: true, steps: state.steps + steps },
26
+ steps,
27
+ foot: (state.steps % 2),
28
+ };
29
+ }
@@ -23,6 +23,13 @@ export interface ToneVoice {
23
23
  slideTo?: number;
24
24
  /** Attack ramp in seconds. Default 0.012. */
25
25
  attack?: number;
26
+ /**
27
+ * Seconds held at full gain after the attack, before the decay to silence at `duration`.
28
+ * Default 0 — a plain percussive decay. Set it to `duration` for a flat, sustained bed:
29
+ * a looped buffer only reads as continuous if its amplitude does not fall across the loop,
30
+ * so ambient hums and wind beds need this or they pulse once per loop.
31
+ */
32
+ sustain?: number;
26
33
  }
27
34
  /**
28
35
  * A filtered white-noise burst — impacts, whooshes, breath, crackle. Realised
@@ -43,6 +50,11 @@ export interface NoiseVoice {
43
50
  decay?: number;
44
51
  /** Start offset in seconds from the cue trigger. Default 0. */
45
52
  delay?: number;
53
+ /**
54
+ * Seconds held at full gain before the decay begins. Default 0. Set it to `duration` for a
55
+ * flat noise bed (wind, rain, machine roar) that loops without pulsing.
56
+ */
57
+ sustain?: number;
46
58
  }
47
59
  /** One layered voice of a synth cue. */
48
60
  export type SynthVoice = ToneVoice | NoiseVoice;
@@ -1,5 +1,5 @@
1
1
  /** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
2
- export declare const VERSION = "0.15.0-next.0";
2
+ export declare const VERSION = "0.15.0";
3
3
  /** One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md). */
4
4
  export interface ChangelogEntry {
5
5
  migrate: readonly string[];
@@ -1,7 +1,151 @@
1
1
  /** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
2
- export const VERSION = "0.15.0-next.0";
2
+ export const VERSION = "0.15.0";
3
3
  /** Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`). */
4
4
  export const CHANGELOG = {
5
+ "0.15.0": {
6
+ migrate: [
7
+ "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.",
8
+ "`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`) \u2014 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`.",
9
+ "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.",
10
+ "Every game must now declare its run-phase story \u2014 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\u2192playing\u2192ended transitions. No runtime behavior changes for a game that already drove phases.",
11
+ "`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 \u2014 `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.",
12
+ "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).",
13
+ "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.",
14
+ "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\u2192impact line for a game that never asked for one \u2014 `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 \u2014 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.",
15
+ ],
16
+ added: [
17
+ "`advanceStepCadence` (`@jgengine/core/audio/stepCadence`) \u2014 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.",
18
+ "`resolveMusicState` (`@jgengine/core/audio/musicState`) \u2014 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 \u2192 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.",
19
+ "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.",
20
+ "`ModelMaterialOverride.rim` \u2014 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).",
21
+ "An `rng` that falls back to `Math.random` now says so (#1582) \u2014 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 \u2014 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.",
22
+ "The two parts a game front-end could not reach for (#1577) \u2014 `useMenuRouter(root)` (`@jgengine/react/menuRouter`) is the screen stack behind title \u2192 save select \u2192 settings \u2192 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.",
23
+ "`ResourcePool` and `ComboPoints` can hand their state back (#1575) \u2014 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.",
24
+ "`HudTheme` now covers the whole `--jg-*` vocabulary (#1573) \u2014 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.",
25
+ "Runtime-mutable arcade car tuning + hops (#1550) \u2014 `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.",
26
+ "`assets pull starter` (#1550) \u2014 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.",
27
+ "`CastConfig.moveDeadzone` (#1550) \u2014 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.",
28
+ "`LootDrop` + `lootDropsOf(data)` (#1550) \u2014 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 \u2014 three games plus `ToastStack`'s own default renderer \u2014 were each writing by hand.",
29
+ "`createRegenShield({ pool })` \u2014 stat-backed shields (#1550). A `RegenShieldPool` port (`current`/`max`/`set`) lets a regen shield read and write storage the game already owns \u2014 an entity stat, a replicated store \u2014 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.",
30
+ "`ctx.rng` + `createGameContext({ seed, rng })` (#1545) \u2014 deterministic per-world `[0,1)` stream for sim paths; same seed \u2192 same sequence across worlds/hosts.",
31
+ "Post `aa: \"smaa\" | \"msaa\" | false` (#1545) \u2014 SMAA default on post chains (before OutputPass) for alpha-tested edge crawl.",
32
+ "Procedural sky IBL + directional `cascades` CSM (#1545) \u2014 `EnvironmentLighting` bakes sky/ground/sun into PMREM; `cascades > 1` on directional lighting mounts cascaded shadows.",
33
+ "`grounded(ctx, x, z)` (#1543) \u2014 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`.",
34
+ "`DEFAULT_WALK_CODES` (#1543) \u2014 stock WASD + Space map for the shell walk controller (`@jgengine/shell/shellMovement`, re-exported from `gameKit`).",
35
+ "`SETTINGS_TRIGGER_CLASSNAME` + default `SettingsTrigger` skin (#1543) \u2014 HudTheme-token 8\u00d78 chrome button; omit `className` for the stock look.",
36
+ "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.",
37
+ "`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 \u2014 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).",
38
+ "`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` \u2014 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.",
39
+ "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 \u2014 no runtime or signature change.",
40
+ "Conflict-aware key-rebinding session. `@jgengine/core/input/rebindSession` adds `createRebindSession({ actions | input, overrides?, now? })` \u2014 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 \u2014 one row per action with key glyph, conflict badges, per-row and Reset-all, HudTheme-skinnable. Demo: `key-rebinding`.",
41
+ "Vendor / shop stock + grid. `@jgengine/core/economy/shopStock` adds a serializable, observable `createShopStock({ entries })` \u2014 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 \u2014 `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 \"\u221e\", afford-aware Buy, optional Sell) with a wallet balance readout.",
42
+ "Save-slot / profile select menu. `@jgengine/core/game/saveSlots` adds `createSaveSlots(config)` \u2014 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, \u2026) 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`.",
43
+ "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.",
44
+ "Interaction prompt registry (\"Press E to \u2026\"). `@jgengine/core/world` adds `createPromptRegistry()` \u2014 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 \u2014 key cap + label, gauge hold bar, or plain label, theme- and per-prompt-accent skinnable) and `useInteractionPrompt(registry, playerPosition)`. Demo: `interaction-prompt`.",
45
+ "Seeded trauma-based camera shake. `@jgengine/core/vfx/cameraShake` adds `createCameraShake(config?)` \u2014 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` \u00d7 per-axis maxima \u00d7 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`.",
46
+ "Observable wave/spawn runner + drop-in HUD. `@jgengine/core/ai/waveRunner` adds `createWaveRunner(config)` \u2014 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` \u2014 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.",
47
+ "Count-based combo / multiplier meter. `@jgengine/core/combat/comboMeter` adds `createComboMeter({ windowMs, tiers?, dropStep?, multiplierPerTier? })` \u2014 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 \u2014 per-tier colored from a caller map) and a `useComboMeter` hook.",
48
+ "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` \u2014 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`.",
49
+ "Scoreboard / leaderboard ranking. `@jgengine/core/game/leaderboardRank` adds `rankLeaderboard(rows, options)` \u2014 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` \u2192 1,2,2,4; `dense` \u2192 1,2,2,3), `isTie`/`isLocal` flags via `highlightUserId`, and top-N `limit` \u2014 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.",
50
+ "Talent/upgrade tree from any unlock rule. `@jgengine/core/game/talentTreeView` adds `talentTreeViewFrom(nodes, status, totals?)` \u2014 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 \u2014 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.",
51
+ "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 \u2014 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.",
52
+ "Codex / bestiary. `@jgengine/core/game/codex`' `createCodex` tracks discovery over a fixed set of defined entries \u2014 `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.",
53
+ "Generated street elevation (`@jgengine/core/world/streetGenerator`) \u2014 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).",
54
+ "Per-corner radius classes on circuits (`@jgengine/core/world/streetGenerator`) \u2014 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.",
55
+ "`compactness` circuit dial \u2014 space-filling kart-style tracks (`@jgengine/core/world/streetGenerator`) \u2014 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 \u2014 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 \u22653-pitch main straight \u2014 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.",
56
+ "`blockFill` density dial (`@jgengine/core/world/cityGenerator` / `buildingLots`) \u2014 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 \u2014 Manhattan-dense cities at 1, sparse suburbs near 0. Block interiors are never filled with street-less buildings; every plot fronts a street.",
57
+ "Plot-first city generator (`@jgengine/core/world/cityGenerator`) \u2014 `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` \u2014 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 \u2014 `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.",
58
+ "`outline` street dial (`@jgengine/core/world/streetGenerator`) \u2014 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 \u2014 lobes, bays, shaved corners \u2014 reduced to its largest connected component. `generateCity` defaults it to 0.4.",
59
+ "Residential branch streets and cul-de-sacs (`@jgengine/core/world/streetGenerator` / `cityGenerator`) \u2014 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 \u2014 winding 1\u20133-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.",
60
+ "`TrimmedIntersections.junctionIndices` (`@jgengine/core/world/roads`) \u2014 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.",
61
+ "`trimBandAtJunctions` (`@jgengine/core/world/roads`) \u2014 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 \u2014 so sidewalks end at crossings instead of sailing through them. The playground consumes it for `Street.sidewalks`.",
62
+ "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.",
63
+ "Terrain surface shading + layered grass wind (`@jgengine/shell/terrain`, #1373) \u2014 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 \u2014 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\u201395 m to 55\u2013150 m so meadows read expansive at no extra instance cost. `createProceduralGroundGeometry` now delegates to the shared ground builder (behavior-identical geometry, minus the duplication).",
64
+ "Debounced control commits: `useDebouncedCommit` (`@jgengine/react/useDebouncedCommit`, #1372) \u2014 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.",
65
+ "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 \u2014 one banner at a time \u2014 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`.",
66
+ "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.",
67
+ "Countdown / timer HUD. `@jgengine/core/time/timerSet`' `createTimerSet` is a serializable set of named countdown/countup timers on an injected clock \u2014 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 \u2014 no hand-rolled interval math. See the `countdown-timer` dev demo.",
68
+ "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 \u2014 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 \u2014 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.",
69
+ "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.",
70
+ "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` \u2192 `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 \u2014 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.",
71
+ "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 \u2014 the game supplies icon/color/label; icons come from the existing game-icons.net icon system. Demo: `status-effects`.",
72
+ "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 \u2014 even across a large `seek` \u2014 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.",
73
+ "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 \u2014 the game owns final layout, terminology, and skin. Demo: `pause-menu`.",
74
+ "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) \u2014 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).",
75
+ "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.",
76
+ "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 \u2014 `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.",
77
+ "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).",
78
+ "Notification center. `@jgengine/core/game/notifications`' `createNotificationCenter` is a persistent, read-tracked notification log (newest-first, capped, serializable, observable) \u2014 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.",
79
+ "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.",
80
+ "Welded street intersections (`@jgengine/core/world/roads`, #1363) \u2014 `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.",
81
+ "`GROUND_DECAL_LAYERS` (`@jgengine/core/world/roads`, #1366) \u2014 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.",
82
+ "Real curves and sidewalks in the street generator (`@jgengine/core/world/streetGenerator`, #1364, #1368) \u2014 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).",
83
+ "Race-track synthesis in circuit mode (`@jgengine/core/world/streetGenerator`, #1365) \u2014 circuits are grown from seeded scatter \u2192 convex hull \u2192 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 \u2014 an actual track layout instead of a wobbled ellipse.",
84
+ "City lot content resolution (`@jgengine/core/world/cityGenerator`, #1367) \u2014 `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.",
85
+ "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 \u2014 `radial` \u00b7 `arc` \u00b7 `list` \u00b7 `grid` \u2014 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).",
86
+ "Per-context touch control modes (`@jgengine/core/input/touchScheme` `TouchControlsConfig.modes` + `@jgengine/core/input/touchControlsMode` `setTouchControlsMode`/`activeTouchControlsMode`, re-exported from `gameplay`, #1370) \u2014 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.",
87
+ "Analog action values through the input snapshot (`@jgengine/core/runtime/inputSnapshot` `publishAnalog`/`value`/`analog`, `InputFrame.analog`, #1370) \u2014 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 \u2014 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.",
88
+ "Floating joystick variant + Settings selector (`@jgengine/shell` touch dock, `@jgengine/core/input/touchScheme` `TouchJoystickVariant`, #1370) \u2014 two shipped stick behaviors, player-selectable under Settings \u2192 Controls \u2192 \"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.",
89
+ "Radial / quick menu (weapon/emote wheel). `@jgengine/core/ui/radialMenu` ships the pure geometry \u2014 `radialSlices`, `radialIndexFromAngle`, `radialIndexFromVector` (pointer/stick vector \u2192 slice with a neutral dead zone), `radialSlicePosition` \u2014 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.",
90
+ "Accessibility options. `@jgengine/core/ui/accessibility`'s `createAccessibilityStore` is a serializable, observable store of genre-agnostic accessibility preferences \u2014 reduced motion, high contrast, `textScale` (clamped), colorblind mode, and captions \u2014 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.",
91
+ "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 \u2014 `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 \u2014 no runtime coupling to `QuestJournal`. First adopter: the apps/dev `quest-tracker` demo.",
92
+ "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=\u2026 />` component \u2014 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 / \u65e5\u672c\u8a9e switcher).",
93
+ "Click-to-place custom marker kinds (`@jgengine/core/scene` `definePlaceableMarkerKind` + `@jgengine/editor`) \u2014 a game declares its own logical marker kind (a stash, checkpoint, bounty\u2026) 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.",
94
+ "`add_marker` editor verb (`@jgengine/editor`, #1360) \u2014 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.",
95
+ "World-anchored entity frames (overhead nameplates / health bars) (`@jgengine/react/entityFrames`, `@jgengine/shell/world/WorldEntityFrames`, #1266) \u2014 a data-first, caller-owned seam for floating frames over entities. `EntityFrames` takes a plain `{ id, worldPosition, ... }[]` plus a `project` (world\u2192screen) and a `renderFrame` that composes the shipped `HealthBar` + a name (reskinned via `barTokens`/`HudTheme`); it owns only the reusable behavior \u2014 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 \u2014 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`.",
96
+ "Per-entity render hide: `SceneEntity.hidden` (`@jgengine/core/scene/entityStore`, `@jgengine/shell`, #1299) \u2014 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.",
97
+ "`ChaseCameraTuning` covers the full driving-feel surface (`@jgengine/core/runtime/cameraDirector`, #1299) \u2014 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\u2192FOV, lead, bank, speed shake, drift-lag) only while a vehicle is piloted. Existing tuning patches are unchanged.",
98
+ "Draw on the map: annotation layer + FullscreenMap draw tool. `@jgengine/core/world/mapAnnotations`' `createAnnotationLayer` is a serializable player-drawing layer \u2014 freehand `strokes`, area `shapes`, and pinned `notes`, all world-XZ \u2014 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).",
99
+ "`jgengine shoot` / `jgengine drive` CLI verbs (`jgengine` CLI, #1262) \u2014 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 \u2014 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.",
100
+ "Engine preview-fixture capture route (`@jgengine/react` `PREVIEW_FIXTURES`, #1272) \u2014 a registry of deterministic, engine-level preview fixtures (the real exported components `HudThemePreview`, `BarsPreview`, `IconsPreview`, \u2026) 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 \u2014 genre-agnostic, additive.",
101
+ "Per-capture player-spawn override (`@jgengine/core/world/spawnOverride`, #1255) \u2014 `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.",
102
+ "Aerial editor camera in one call (`@jgengine/editor`, #1256) \u2014 `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.",
103
+ "Achievements / trophies \u2014 `@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 \u2014 no defaults or content shipped.",
104
+ "World-space pings (`@jgengine/shell/world/WorldPings`, `@jgengine/shell/world/pingPulse`) \u2014 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 \u2014 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.",
105
+ "Difficulty-aware AI driver (`@jgengine/core/ai/driver`, #1311) \u2014 `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 \u00d7 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\u2605 sloppy rookies \u2192 5\u2605 sharp interceptors) instead of `steer = error*2, throttle = 1`.",
106
+ "Effect primitives: named hooks for the recurring `useEffect` shapes (`@jgengine/react/hooks`, `@jgengine/shell/render/useDisposable`, #1298) \u2014 `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 \u2014 a lint restriction on direct `useEffect` outside the primitives layer follows in a later phase.",
107
+ "Three-tier AI decision quality (`@jgengine/core/ai/difficulty`, #1301) \u2014 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 \u2014 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.",
108
+ "Authored per-placement rig animation applies in game (`@jgengine/core/world/authoredObjects`, `@jgengine/core/scene/objectStore`, #1276) \u2014 a placement's editor-authored `marker.meta.animation` (role\u2192clip states, `\"auto\"`/`\"none\"`, walk/run/fade, one-shot bindings; the stable contract from #1274) now flows through `resolveAuthoredObjects` \u2192 `placeAuthoredObjects` \u2192 `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 \u2014 animation still comes from catalog resolution.",
109
+ "`EffectResult` carries the slain entity's identity (`@jgengine/core/combat/effects`, #1263) \u2014 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 \u2014 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.",
110
+ "Interactive fullscreen map + player waypoints. `@jgengine/core/world/waypoints`' `createWaypointStore` is a serializable player-waypoint layer (place/track/clear, snapshot\u2194restore) 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`.",
111
+ "Editor RPC/CLI verb `add_path` (`@jgengine/editor`): author a new path/route into the scene document from an ordered list of \u22652 `{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.",
112
+ ],
113
+ changed: [
114
+ "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`.",
115
+ "SDK remediation Phase 3a \u2014 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.",
116
+ "SDK remediation Phase 1.2 + 2.3 \u2014 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\u00d78 className re-authoring.",
117
+ "SDK remediation Phase 0 \u2014 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`, \u2026) and reskin via HudTheme \u2014 games own layout/terminology/art direction, not re-derivation. Studio Showcase and Tower Guard import kit-covered symbols from `gameKit`.",
118
+ "Kinematic vehicle drive feel (#1515) \u2014 `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.",
119
+ "`check-game-shape`/doctor accept editor-import-graph modules by convention (`packages/jgengine` `gameShape`, #1377) \u2014 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.",
120
+ "Road ribbons no longer self-intersect at bends (`@jgengine/core/world/roads`) \u2014 `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).",
121
+ "Junction surfaces stopped emitting sliver triangles (`@jgengine/core/world/roads`) \u2014 `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.",
122
+ "Street corner arcs sample at \u2264~9\u00b0 per vertex (`@jgengine/core/world/streetGenerator`) \u2014 corner fillets previously stepped up to `maxTurnAngle` (~28\u00b0) 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.",
123
+ "Circuit centerlines are curve-first splines (`@jgengine/core/world/streetGenerator`) \u2014 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 \u2014 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.",
124
+ "Chase camera yaw is smoothed by default (`camera.chase.yawResponse`, `@jgengine/shell` chase rig, #1370) \u2014 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.",
125
+ "Editor content-browser thumbnails no longer crash the editor when a GLB's textures fail (`@jgengine/editor`, #1270) \u2014 `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.",
126
+ "Offline autosave no longer starves in a living world (`@jgengine/core/runtime/runtimeSave`) \u2014 `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 \u2014 at most one write per `autosaveMs` and always within one interval, even while the world never idles. No API change; behavior only.",
127
+ "Effect consolidation phase 2: subscriptions on `useSyncExternalStore`, dispose pairs on `useDisposable` (`@jgengine/react`, `@jgengine/shell`, `@jgengine/editor`, #1304) \u2014 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 \u2014 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.",
128
+ "Blocking colliders are now walkable surfaces, and collision is on by default (`@jgengine/core` movement, #1402) \u2014 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.",
129
+ "Movement and vehicles no longer tunnel through solids (`@jgengine/core`, #1402) \u2014 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.",
130
+ "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 \u2014 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.",
131
+ "`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\"`.",
132
+ "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 \u2014 invisible on short impact bursts, fatal for anything sustained. The source now wraps; the voice still ends on time.",
133
+ "`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 \u2014 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.",
134
+ "`sky({ gradientExponent, cloudiness })` are now authorable \u2014 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 \u2014 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.",
135
+ "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.",
136
+ "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 \u2014 the base mesh and healthy siblings still render.",
137
+ "`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.",
138
+ "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 \u2014 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.",
139
+ "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).",
140
+ "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`.",
141
+ "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.",
142
+ "`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 \u2192 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 (\u22658 streets yielding fewer than street-count/4 faces), pointing callers at `extractGraphBlocks` \u2014 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.",
143
+ "`assets pull` / `assets add` default output dir now lands where the dev server serves models (`@jgengine/assets` CLI, #1339) \u2014 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.",
144
+ "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.",
145
+ "A missing/mis-served GLB no longer white-screens the whole game \u2014 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 \u2014 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.",
146
+ ],
147
+ removed: [],
148
+ },
5
149
  "0.14.0": {
6
150
  migrate: [
7
151
  "Bump lockstep SDK packages to ^0.14.0: @jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}. CLI jgengine is 0.11.0; @jgengine/github is unchanged at 0.1.0.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/core",
3
- "version": "0.15.0-next.0",
3
+ "version": "0.15.0",
4
4
  "description": "jgengine core — pure-TypeScript game framework SDK (npm @jgengine/core). Entity stores, commands, combat, inventory, multiplayer seams. Zero dependencies. Not an ECS. Agent skills ship in the tarball. https://jgengine.com",
5
5
  "keywords": [
6
6
  "jgengine",
@@ -6,7 +6,7 @@
6
6
 
7
7
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
8
8
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
9
- - `VERSION` (const): const VERSION: "0.15.0-next.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.15.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
10
10
 
11
11
  ## @jgengine/core/authoring
12
12
 
@@ -228,7 +228,7 @@
228
228
 
229
229
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
230
230
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
231
- - `VERSION` (const): const VERSION: "0.15.0-next.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
231
+ - `VERSION` (const): const VERSION: "0.15.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
232
232
 
233
233
  ## @jgengine/core/runtime/adapter
234
234
 
@@ -223,6 +223,25 @@
223
223
  - `notesInWindow` (function): function notesInWindow(theme: MusicTheme, anchorSec: number, fromSec: number, toSec: number): ScheduledNote[] — Pure lookahead scheduler: every note occurrence of `theme` whose onset falls in the half-open window `(fromSec, toSec]`, given the theme's loop-zero at `anchorSec`. Handles any number of loop wraps, so a director calls it once per tick with a non-overlapping window and never double-schedules a note.
224
224
  - `themeLoopSeconds` (function): function themeLoopSeconds(theme: MusicTheme): number — Loop length of a theme in seconds.
225
225
 
226
+ ## @jgengine/core/audio/musicState
227
+
228
+ - `MusicStateConfig` (interface): interface MusicStateConfig — The tier ladder plus the release margin that keeps a hovering intensity from flapping between tiers.
229
+ - `MusicStateInput` (interface): interface MusicStateInput — The current intensity signal and clock reading for one resolve.
230
+ - `MusicStateResult` (interface): interface MusicStateResult — Next state, the theme that should be playing, and whether this advance is the one that changed it.
231
+ - `MusicStateSnapshot` (interface): interface MusicStateSnapshot — Which tier is playing and when it was entered, on the caller's clock. Serialisable.
232
+ - `MusicTier` (interface): interface MusicTier — Hysteretic music-state selector: turns a continuous intensity signal (nearby threat, alert count, chase proximity) into a stable theme id. The engine can already crossfade a soundtrack; this decides *when*, without the flapping a bare threshold produces when one enemy walks in and out of range.
233
+ - `createMusicState` (function): function createMusicState(): MusicStateSnapshot — Fresh state with no tier entered, so the first resolve always settles and reports `changed`.
234
+ - `resolveMusicState` (function): function resolveMusicState(state: MusicStateSnapshot, input: MusicStateInput, config: MusicStateConfig): MusicStateResult — Pick the theme for the current intensity. Escalation is immediate; de-escalation waits for the tier's `holdMs` and needs the intensity to fall a `release` margin below the tier it is leaving.
235
+
236
+ ## @jgengine/core/audio/stepCadence
237
+
238
+ - `StepCadenceConfig` (interface): interface StepCadenceConfig — Distance-driven footstep cadence. Pure state-in/state-out: feed it how far a mover travelled this frame and it tells you how many steps landed, so foley stays locked to ground distance instead of a wall-clock timer that desyncs when speed changes.
239
+ - `StepCadenceInput` (interface): interface StepCadenceInput — What the mover did this advance: ground distance covered, whether feet are planting, and the stride multiplier.
240
+ - `StepCadenceResult` (interface): interface StepCadenceResult — Outcome of one advance: the next state plus how many steps landed and which foot led.
241
+ - `StepCadenceState` (interface): interface StepCadenceState — Cadence bookkeeping between advances — banked distance, last moving state, and the step count whose parity is the foot.
242
+ - `advanceStepCadence` (function): function advanceStepCadence(state: StepCadenceState, input: StepCadenceInput, config: StepCadenceConfig): StepCadenceResult — Bank this advance's distance and report the steps it completed. Pure — same state and input always give the same result, so footstep timing is testable headless and replays identically.
243
+ - `createStepCadenceState` (function): function createStepCadenceState(): StepCadenceState — Fresh cadence state for a mover that is standing still.
244
+
226
245
  ## @jgengine/core/audio/synth
227
246
 
228
247
  - `NoiseVoice` (interface): interface NoiseVoice — A filtered white-noise burst — impacts, whooshes, breath, crackle. Realised from a shared 1s noise buffer at a randomised playback rate and start offset, decaying exponentially to silence at `duration * decay`.