@jgengine/react 0.25.0 → 0.27.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
@@ -26,6 +26,115 @@ At publish, rename this heading to the new version and mirror the entries into
26
26
  `packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
27
27
  -->
28
28
 
29
+ ## 0.27.0
30
+
31
+ ### Migrate
32
+
33
+ - **Bump lockstep SDK packages to `^0.27.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.24.0`; `@jgengine/github` is `0.14.0`.
34
+
35
+ ### Added
36
+
37
+ - `rollLoot` (`@jgengine/core/loot/lootGen`) rolls a named item from a caller `LootCatalog`: bases with mount slots, parts, affix pools, weighted rarity tiers, manufacturer personalities and a `levelScale` curve. Stats are `base × rarity.statScale × levelScale`, then part stats/multipliers, maker `bias`/`scale`, then additive and multiplicative affixes; additive affix rolls scale with level, multiplicative ones do not.
38
+ - `createLootRoller` wraps a validated catalog with roll counts per rarity, `snapshot()`/`restore()`/`reset()` and an optional devtools probe; `assertLootCatalog` rejects duplicate ids, unresolvable pools, empty item slots and a non-ascending level curve at construction.
39
+ - `validateLootItem` returns coded issues (`unknown-base`, `manufacturer-base`, `missing-slot-part`, `part-category`, `unknown-affix`, `empty-name`, …) and `compareLootItems` returns sorted per-stat deltas plus a weighted score with caller-named `lowerIsBetter` stats.
40
+ - `lootWeaponDefinition` (`@jgengine/core/loot/lootWeapon`) projects a rolled item onto a `WeaponDefinition` through a caller stat map, so a generated gun runs in `createWeaponSystem` after `assertWeaponDefinition`.
41
+ - `defineXpCurve` (`@jgengine/core/gameplay`) resolves one authored growth rule (`flat`, `linear`, `power`, `geometric`, `table`) plus a `maxLevel` cap into per-level thresholds, lifetime totals, a plain-words rule string and a `leveling` track.
42
+ - `createSkillTree` spends and refunds points over authored nodes with `cost`, `requires`, `excludes`, passives, actives and multi-node `synergies`; `resolved()` returns the stat modifiers and actions, and state snapshots/restores as `{ points, ranks }`.
43
+ - `gearScore` / `compareGearSets` score a whole loadout from stat weights, level, rarity and slot coverage.
44
+ - `lootStatContributions` and `scoreLootStats` expose the weight policy `compareLootItems` already used.
45
+ - `createEquipment` (`@jgengine/core/inventory/equipment`) wears gear by slot: slot defs declare accepted kinds and the slots they block, item defs declare the slots an item may occupy, a filled slot swaps and hands back the replaced item, and a two-handed item and the slot it blocks cannot both be filled. `state()` / `restore()` round-trip the worn set; a devtools probe reports filled slots.
46
+ - `characterLoad` and `canCarry` (`@jgengine/core/inventory/carryLoad`) weigh carried inventories plus worn gear against a capacity and hand back the shared encumbrance bands (tier, `moveMultiplier`, sprint/jump gates, `remaining`), so a stash left out of `carried` adds no weight.
47
+ - `fitShapedItem` (`@jgengine/core/inventory/shapedGrid`) places a shaped item at its first fit or names why not: `too-large` (with both sizes), `no-space`, `budget-exceeded`.
48
+ - `InventorySet.snapshot()` / `restore()` / `reset()` (`@jgengine/core/inventory/inventoryModel`) save and reload every declared inventory as detached plain data, so a bag and a stash never share live slots.
49
+ - `splitStack` takes an optional stack `limit`, rejecting `over-stack` instead of merging past the max stack.
50
+ - `createLaneGraph` and `laneGraphFromDocument` (`@jgengine/core/world`) read an authored lane graph out of the scene document: `traffic_lane` paths are one directed span each, `road` paths (the street bake) become a lane per direction split where streets cross, `traffic_intersection` markers carry signal phases and right-of-way in `meta`, and `traffic_parking` markers place kerbside spots.
51
+ - `createTraffic` (`@jgengine/core/world`) runs cars on that graph: IDM car following and braking, one car at a time through each intersection box behind a signal phase or first-come right-of-way, lane changes across authored neighbours, parking that reserves a spot, deterministic injected variety, `snapshot`/`restore`, and a devtools probe.
52
+ - `Traffic.setDemand(count)` is the fleet target; the time-of-day curve stays the caller's policy.
53
+ - `applyVehicleParts` (`@jgengine/core/world`) composes data-driven tuning parts onto a `KinematicVehicleTuning` — stat multipliers/addends applied in install order plus an optional tire grip curve — and returns the composed tuning, the installed ids, and a `VehicleFeel` (top speed, drive, brakes, turn rate, handbrake grip, and the grip curve's peak/slide). `withPart` swaps a part within its slot and `feelOf` reads the numbers out of any tuning.
54
+ - `createVehicleDamage` (`@jgengine/core/world`) is fixed-zone crash damage wired to handling and visuals: impacts accumulate into discrete stages, each stage costs grip/drive/brakes/top speed/drag and pulls the steering (`effect`), and each zone shows one bounded dent (`deformation`) that never exceeds its authored `deform`. Parts can detach at a set stage, a total impulse can disable the vehicle, and `snapshot`/`restore` resume mid-crash. `damagedTuning` and `damagedAxis` fold the effect into a tuning or an input, and `modifiers()` carries it as per-tick overrides.
55
+ - `KinematicVehicleTuning.weightTransfer` (`@jgengine/core/world`) adds load-transfer grip: longitudinal transfer under braking/traction and lateral transfer under cornering cut the tire grip curve (tire load sensitivity), so braking into a corner holds less slide than coasting through it. Each tick reports `weightTransfer` and `loadGrip`; omitting the block keeps the previous flat-grip model exactly.
56
+ - `KinematicVehicleModifiers.gripScale` scales the grip curve for one tick, mirroring `VehicleDynamicsModifiers.gripScale` so a damage layer drives either sim.
57
+ - `decal` scene kind (`@jgengine/core/world/decalKind`) and its renderer (`@jgengine/shell/scene/DecalRenderer`): dirt, cracks, paint and blood decals authored as placed markers, draped on the live terrain or standing on an authored tilt, with a deterministic procedural texture per style+seed.
58
+ - `TerrainMaterialLayer.material` (`@jgengine/core/world/terraform`): a material library name whose scanned color map paints that terrain layer. The runtime samples up to four layer maps at each layer's `tiling` and blends them through the authored mask weights (`createTerrainDetailMaterial`'s `layerMaps`), so painted terrain blends between authored layers instead of tiling one flat map.
59
+ - `grass_field` volumes now paint a ground-cover mat under their blades (`groundCover` rule, `@jgengine/shell/terrain/GrassCover`), so an authored patch reads as turf rather than bare ground with stubble. `GrassEnvironmentConfig.groundCover` does the same for `grass()` patches.
60
+ - `ModelConfig.wind` (`@jgengine/core/game/playableGame`) and `applyModelWind` (`@jgengine/shell/render/modelWind`): per-model foliage sway with height-ramped travel, shared gust clock and per-model strength/speed/direction.
61
+
62
+ ### Fixed
63
+
64
+ - The player shell re-renders only for the ctx slices it reads — the possessed entity and the touch control mode — instead of on every `ctx` write. Subscribing the shell root to the raw ctx version reconciled the canvas, the whole actor tree and the HUD on each per-frame entity pose write, which is what `subscribeMembership` and the markers' imperative `useFrame` pose reads exist to avoid (#625). Scrap Signal: 86.6 ms → 16.7 ms per frame at the same 621 draws; Harbor Heat: 102.8 ms → 46.2 ms at the same ~7.2k draws.
65
+ - The `ocean()` environment feature passes the live ground to its shader, so the sea shades shallow→deep and its shoreline foam band lands where the bed meets the surface.
66
+
67
+ ### Numbers
68
+
69
+ - Test catalog (60/25/10/4/1 rarity weights) over 10,000 drops at level 20, fixed seed: common 59.41% (target 60%), uncommon 25.66% (25%), rare 9.58% (10%), epic 4.20% (4%), legendary 1.15% (1%); every tier inside its 4σ binomial bound. All 10,000 items validated with zero issues and composed names (e.g. `Voss Rare Long Keen Lightframe of Reach`, `Tallow Long Lightframe`, `Voss Snub Lightframe of Steadying`).
70
+ - Level curve `[[1,1],[15,1.6],[30,2.4],[60,4]]`: a level-60 roll's base and additive affix values are 4× the same seed at level 1; maker personalities move the same roll (`Voss` +power/−cadence, `Tallow` the reverse).
71
+ - A rolled epic projects to a hitscan `WeaponDefinition` (rate, damage, integer magazine capacity, spread, range), passes `assertWeaponDefinition`, and its first `tick` fires one shot and one impact through `createDamageRuntime` with the rolled capacity spent.
72
+
73
+ ## 0.26.0
74
+
75
+ ### Migrate
76
+
77
+ - **Bump lockstep SDK packages to `^0.26.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.23.0`; `@jgengine/github` is `0.13.0`.
78
+
79
+ ### Added
80
+
81
+ - `createLogBus` (`@jgengine/core/devtools/logBus`): a fixed-capacity ring of log lines tagged by owning system, with a separate ring for warnings and errors so a crash line is never pushed out by info traffic. `devtools.logs` now writes through it (`write(level, system, message)`, `snapshot()`, `counts()`, `dropped()`); `DevtoolsLogEntry` gained `system`.
82
+ - `devtools.probes.registerSystem(id, { counters, timers, state })`: a system reports its own counters, named timings and one state line in a few lines, with no shell wiring. Readings surface as `systems` in `devtools.snapshot()`, in `debug_snapshot`, and in every perf record.
83
+ - `PerfReport` (`@jgengine/core/devtools/report`): the one perf record shape shared by `bun run perf` and the in-page report.
84
+ - The shell samples frame telemetry in-page in dev builds: `window.__JG.report()` returns that record, and the page pushes it about once a second to the dev server's `GET/POST /__jgengine/report` route. `bun run debug <game>` prints the record of a game already open and exits non-zero on a budget breach in `scripts/perf-budget.json`.
85
+ - `createNeedSet` (`@jgengine/core/gameplay`) drains named needs on game time and reads them by index — decaying hunger, energy, fun with snapshot/restore and per-need rate retuning.
86
+ - `createUtilityBrain` (`@jgengine/core/world`) picks an action from decaying-need appeal: linear, quadratic, logistic, exponential and threshold response curves, candidate cost (travel, queues, price), a switch margin plus minimum hold (hysteresis), declared interrupts, optional seeded jitter, and a devtools probe string.
87
+ - `createNavRuntime` (`@jgengine/core/world`) consumes an editor-baked nav mesh: reads the `nav` bake off a scene document (`navMeshFromBake`), routes with the mesh query, resolves authored doors, ladders and jumps (`AuthoredNavLink`, priced by kind) onto the current surface, and swaps in a new bake after an edit while matching polygon identity by geometry — `applyMesh` returns a `remap` so only routes over changed polygons need re-planning.
88
+ - `createNavCrowd` (`@jgengine/core/world`) moves many agents across that mesh: plans a corridor, follows the funnel-smoothed waypoints, keeps agents apart with reciprocal velocity obstacle avoidance, bounds path searches per tick by count and milliseconds, fans converging agents out with `spread`, and round-trips through `snapshot`/`restore`.
89
+ - `createRvoCrowd` (`@jgengine/core/world`) is the avoidance solver on its own: RVO/ORCA half-plane solve over a uniform-grid neighbour search capped at `maxNeighbors`, deterministic tie-breaking, overlap relief, retunable `timeHorizon`/`neighborDist`/`maxNeighbors`/`sideBias`/`overlapRelief`, and `snapshot`/`restore` for saves and replays.
90
+ - `resolveRoutine` (`@jgengine/core/gameplay`) resolves an authored day into real arrival and departure times against the world clock. Travel time is injected per stop or as a callback, so a stop departs early enough to walk to the next one and an arrival that runs late slips instead of teleporting; `phaseAt(time)` returns the held place, the walking leg and its progress, and `nextChangeAfter(time)` drives a scheduler. A day whose walks cannot fit its cycle throws at construction.
91
+ - `createOffScreenWorld` (`@jgengine/core/gameplay`) keeps a whole population without simulating it: residents beyond the injected radius are cheap state (position, routine phase, needs) that only moves when the clock crosses a routine boundary, and they spawn into the game's own near simulation through a `NearAgents` interface when they come close. The radius, an optional per-agent cost model, a near budget and a per-tick switch budget are all injected; `snapshot`/`restore` round-trip the whole population, and crossing the boundary preserves identity, schedule progress, need values and position.
92
+ - `createNearAgents` (`@jgengine/core/gameplay`) is the default near half of that seam: a nav crowd with a need set and utility brain per agent, goal-tracking as routines move residents between places, and a decision counter for tests and probes.
93
+ - `createPerception` shares squad knowledge: `squadMates(observerId)` names an observer's roster, and a sighting one member makes is written into every mate's memory with `sourceId` (who saw it) and `sourceTick` (the observe pass it was recorded on). A mate's own sighting at the same instant replaces the hand-off. `maxSharesPerTick` (default 64, per observer pass) bounds a large roster and reports elided shares.
94
+ - Perception memory is auditable without guessing: `memoryOf(observerId, targetId)` returns the one remembered target with its confidence right now or `null` once it has faded, and `stats()` counts pair checks, range prunes, cone tests, line-of-sight calls, sightings, heard stimuli, shares and dropped shares. The check order is range → cone → line of sight, so occlusion work only sees the pruned set. `resetStats()` opens a measurement window; `memories` are pruned per observer so a 200-observer pass does not rescan every memory.
95
+ - `devtoolsName` registers a probe showing observers, remembered targets and the work counters, and `dispose()` removes it.
96
+ - `createCoverField` and `coverPointsFromSolids` (`@jgengine/core/combat`) derive stand-able cover from authored solid boxes instead of a coordinate list: one row of points per vertical face, clear of the corners, filtered by an injected `walkable` test, rebuilt when the world is edited, and queried through a bounded `near(position, radius, limit)`. `CoverSource` is the seam, so a game with its own spatial index can supply cover without the box generator.
97
+ - `selectCover` / `scoreCoverPoint` choose cover by threat: `hidden` is the confidence-weighted share of remembered threats whose line of sight the point (and a body-sized footprint at it) blocks, scored against the role's preferred fighting range, flank angle around the threat, distance, a squadmate's claim on the point, and whether the walk to it is a detour. Fading memories stop steering at `minConfidence`, and a point that hides from only one of two shooters is not cover.
98
+ - `createSquadCombat` (`@jgengine/core/combat`) drives a squad over existing seams: role-driven cover selection, fire discipline (hold, lean out, shoot), suppression from `underFire()`, retreat below a role's health threshold, and relocation under sustained fire. Threat comes only from `threatsFromMemory`, i.e. the perception memory the game already keeps; movement goes through `NavCrowd` with a tighter arrival tolerance so a body actually reaches the stand point. Serializable through `snapshot()`/`restore()` (member decisions, crowd, tuning), retunable, and a `devtoolsName` probe reports pins, peeks, shots and the worst exposure.
99
+ - `assignSquadRoles` and `threatsFromMemory` (`@jgengine/core/combat`): deterministic role assignment over a caller-authored `SquadComposition` (role ids, counts, ranges, flank angles, retreat thresholds and cover weights are all content), and a bounded, confidence-ranked threat list from one observer's memory.
100
+ - A 2v4 arena soak proves the behavior: 90 simulated seconds, 2700 ticks, 4 squad members against 2 scripted raiders, worst exposure 2.33 s (bar 3 s), 0 of 38 exposure spells over 3 s, per-tick cost p50 0.05 ms / p95 0.19 ms.
101
+ - `createDamageRuntime` (`@jgengine/core/combat`) composes a damage table, hit zones, the armor curve and carried statuses into one stateful model: `hit()` returns every stage of the number (`base × zone multiplier × type multiplier × (1 − armor mitigation)`), `tick()` runs status effects through the same table and armor, `timeToKill()` quotes a fight, and `table()` reads the whole chart. `snapshot()`/`restore()` carry registered targets and carried statuses; `retune()` swaps table, zones or status definitions without dropping either; a `devtoolsName` probe reports hits, crits, ticks and the last hit.
102
+ - A damage table (`combat/damageTable`) of damage type × resistance class, with `armor / (armor + halfValue)` mitigation — armor equal to the half value halves the hit — a per-type `bypassArmor`, and `readDamageTable` / `formatDamageTable` to read every row at given armor values. An unknown damage type throws instead of silently dealing 1×.
103
+ - Hit zones and crits (`combat/hitZones`): a zone carries a normal multiplier and an optional crit multiplier, `resolveHitZone` rolls once and returns one multiplier, and a crit replaces the zone multiplier rather than compounding with it. `hitZoneMultiplier` quotes the same number without a roll.
104
+ - `createStatusRuntime` (`combat/statusRuntime`): data-defined statuses with the existing `StatusStackPolicy` (`refresh`, stack to `max`, `replace`, `ignore`) as the only thing that changes stacks, per-tick effects of `{ kind: "damage", channel }` or `{ kind: "stat", stat }`, damage routed through an injected `applyDamage` so resistances and armor still apply, bounded ticks per call, and plain-data state out and back.
105
+ - Numbers from the tests: 25 base kinetic against a 150 HP target, 0.15 s between shots — 8 shots / 1.05 s unarmored, 20 shots / 2.85 s at 60 armor, 36 shots / 5.25 s at 200 armor; two stacked burns of 4 per tick deal 8, not 16; a 1.5× head zone with a 2.5× crit multiplier deals 2.5×, not 3.75×.
106
+ - `createWeaponSystem` (`@jgengine/core/combat`) runs one `WeaponDefinition`: single, automatic or burst fire, hitscan or ballistic projectiles, recoil that moves the aim the shot flies down, spread bloom, interrupted/resumed reloads, a readable ammo economy (`ammo()`, `stats()`), `snapshot()`/`restore()` and a devtools probe. Every hit, hitscan or projectile, resolves through `createDamageRuntime`. Pellet work is bounded per tick (`maxShotsPerTick`, default 4) and per weapon (`maxInFlight`, default 32).
107
+ - `WeaponDefinition` (`@jgengine/core/combat/weaponDefinition`) as plain JSON — rate, fire mode, delivery, magazine, handling, damage, falloff — with `assertWeaponDefinition`, `weaponIntervalMs`, `weaponDamageAt` and `weaponMaxInFlight`. A generated gun is the same shape.
108
+ - `Magazine.pauseReload()` / `resumeReload()` / `isPaused()` suspend a reload and keep its progress; a paused reload is not `isReloading()`, does not advance in `tick`, and round-trips through `snapshot()`/`restore()`.
109
+ - `environment.grading` on the scene document authors a per-game colour grade: shadow/midtone/highlight swatches plus tint, saturation and contrast bake into a small display-space LUT (`lutCubeFromGrading`, `colorGradeFromDocument` in `@jgengine/core/editor/environment`), edited in the Lighting workspace. The shell applies it from `editorLayers`, and a graded game needs no LUT asset or hardcoded pass.
110
+ - Clamped auto-exposure: `postProcessing.autoExposure: { min, max, adaptationSeconds } | false` meters the pre-tone-map frame every four frames toward 18% grey and stays inside the authored range. `WHITE_POINT` and `MID_GREY` in `@jgengine/core/render/colorPipeline` are the single documented calibration the pipeline uses.
111
+ - `GradeConfig.lut` also accepts a baked `{ cube: { size, data } }` LUT, sampled trilinearly after the numeric grade.
112
+ - `MATERIAL_LIBRARY` (`@jgengine/assets/catalogs/materialLibrary`) names 56 PBR surfaces — wet asphalt, painted metal, stucco, brick, wood, foliage with cut-out coverage and sheen, glass, water, sand, concrete and more — each mapping a name to an ambientCG source, a world tile size in metres and a physical response. `libraryMaterialAsset("asphalt/wet")` returns the editable `MaterialAsset` those names resolve to, and `filterLibraryMaterials` backs pickers. `assets pull library` pulls every pinned map set.
113
+ - `TriplanarAssetMaterial` / `useTriplanarAssetMaterial` / `applyTriplanarProjection` (`@jgengine/shell/render/triplanarMaterial`) render a material asset from world position, so world geometry without a `uv` attribute still gets colour, normal, roughness and AO maps. Tile size is world metres; instanced meshes project in instanced space.
114
+ - The editor's Materials workspace browses the library by name and writes the chosen entry into the document's material assets.
115
+
116
+ ### Changed
117
+
118
+ - `createPerception` (`@jgengine/core/world`, also `@jgengine/core/sensor/perception`) takes `isSegmentClear` instead of `occluded`: the game supplies collision as a "nothing blocks this segment" test, and it is only called for pairs already inside range and cone. **Migration:** flip the predicate — a `occluded: (from, to) => blocked(from, to)` becomes `isSegmentClear: (from, to) => !blocked(from, to)`.
119
+ - `MagazineSnapshot` gains an optional `paused: true` field, present only while a reload is suspended, so existing snapshots and idle shapes are unchanged.
120
+ - AgX is the pipeline's tone map: `postProcessing.toneMapping` defaults to `"agx"`, and the `cinematic`, `photoreal`, `comic`, `retro` and `STUDIO_STAGE_POST` configs now pick it too. `"toon"` keeps `"linear"` on purpose. Games that authored `"aces"` explicitly are unchanged.
121
+ - The grade pass samples LUT tiles at texel centres and blends the blue axis by hand, so a supplied strip LUT no longer bleeds across slices.
122
+
123
+ ### Fixed
124
+
125
+ - `bun run debug <game>` now finds the server `bun dev` started. The website dev server publishes the same checkout marker the runner does (`scripts/devServerMarker.ts`, written at listen, removed on shutdown, dev builds only), and the marker records the host that answers, since Vite's default `localhost` can be IPv6-only where `127.0.0.1` hangs.
126
+ - The website dev server serves `GET/POST /__jgengine/report` too, so a game opened at `/games/<id>` reports from the `/play` runner it embeds.
127
+ - The in-page sampler installs when the dev report route answers, not on a build-time `PROD` check: the `/play` runner is a production build, so the old check silently disabled the report on `bun dev`.
128
+ - A report taken inside the sampler's warmup is no longer judged on link/framebuffer counts, which only mean something after the warmup baseline exists.
129
+ - `extractMaterialMaps` and `buildMaterialCatalog` now recognise ambientCG's `_Opacity` map, so cut-out foliage and decal scans keep their coverage instead of rendering as solid cards.
130
+ - Capture runs no longer leak a ~150 MB temp Chrome profile each. `shoot`, `drive` and `perf` delete their profile on every exit path — success, throw, watchdog and SIGINT — `bun run shoot` reclaims profiles a killed run left untouched for three hours, the capture guard sweeps the profile of a run it force-kills, and the generated `browser.mjs` harness for game repos removes its own. A capture also refuses to start when the temp drive has less free space than it needs.
131
+ - The post-processing chain, graphics settings and textured terrain material keep a stable identity across renders. Before, every render rebuilt the effect composer and recompiled shaders, which dropped games to single-digit FPS.
132
+
133
+ ### Numbers
134
+
135
+ - Test fixture at 600 rpm, 30-round magazine: 2.9 s to empty; 17.0° pitch and 3.1° yaw climb over the magazine; back to half climb in 0.37 s and within 5% in 0.6 s; bloom 2.0 → 8.0 → 12.0 mrad (cap) by the tenth shot, settling to base 0.63 s after the last shot.
136
+ - A hitscan and a projectile contact at the same distance return the same damage (20 body, 30 on a 1.5x head zone) through one damage model; a 60 m/s projectile crosses 20 m in 0.33 s and drops 2.45 m in 0.5 s at 9.81 m/s².
137
+
29
138
  ## 0.25.0
30
139
 
31
140
  ### Migrate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/react",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "React UI layer for JGengine: GameProvider, hooks, and headless primitives over @jgengine/core.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "styles": "bun scripts/styles.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@jgengine/core": "^0.25.0"
38
+ "@jgengine/core": "^0.27.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^19.0.0",
@@ -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.25.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.27.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
10
10
 
11
11
  ## @jgengine/core/authoring
12
12
 
@@ -82,25 +82,42 @@
82
82
  - `Devtools` (interface): interface Devtools — Top-level devtools facade aggregating frame, profile, render, logs, latency, controls, discover, overrides, and probes.
83
83
  - `DevtoolsControl` (interface): interface DevtoolsControl — Registered tunable control with its resolved schema metadata and read/write/reset accessors.
84
84
  - `DevtoolsControlKind` (type): type DevtoolsControlKind = | "slider" | "toggle" | "color" | "select" | "text" | "vec2" | "vec3" | "vec4" | "interval" | "angle" | "enum" — ⚠ undocumented
85
- - `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, and formatted message.
85
+ - `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, owning system tag, and formatted message.
86
86
  - `DevtoolsLogLevel` (type): type DevtoolsLogLevel = "log" | "info" | "warn" | "error" — Severity level for a captured devtools log entry.
87
+ - `DevtoolsLogs` (interface): interface DevtoolsLogs — Bounded log ring with per-system tags, a warning/error ring, and lifetime counts.
87
88
  - `DevtoolsOverrides` (interface): interface DevtoolsOverrides { version: number; enabled: string[]; values: Record<string, unknown>; schemas?: Record<string, {kind: DevtoolsControlKind; schemaVersion?: number}> } — ⚠ undocumented
88
89
  - `DevtoolsSnapshot` (interface): interface DevtoolsSnapshot — Serializable point-in-time snapshot of devtools state: frame, render, latency, logs, controls, and discovered fields.
89
90
  - `DiscoveredEntry` (interface): interface DiscoveredEntry — A control auto-discovered by scanning an object/table, with its binding metadata and current reader.
90
91
  - `DiscoverySkip` (interface): interface DiscoverySkip { readonly path: string; readonly reason: string } — ⚠ undocumented
91
92
  - `FrameRecordSample` (interface): interface FrameRecordSample — Per-frame timing input recorded by callers: total frame time, sim time, and optional named phase durations.
92
93
  - `FrameStats` (interface): interface FrameStats — Aggregated frame-timing statistics over the recent sampling window, including per-phase breakdown.
93
- - `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — Frame duration in milliseconds above which a frame is recorded as a long-frame spike (~30fps budget).
94
+ - `FrameWindow` (interface): interface FrameWindow — Frame timings over the measured window, as the player sees them.
95
+ - `GlReport` (interface): interface GlReport — GPU-object creation counts. `total` is since the page loaded, the named fields since warmup.
96
+ - `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — The one perf record shape. `bun run perf` (CDP, out of process) and the in-page telemetry report (`window.__JG.report()`, the dev-server route, `bun run debug`) both produce exactly this object, so a budget check and a diff read either one.
94
97
  - `LatencyStats` (interface): interface LatencyStats — Aggregated latency statistics (last/avg/min/max) over recorded latency samples.
98
+ - `LogBusCounts` (interface): interface LogBusCounts — Lifetime totals per level, plus how many lines the ring dropped.
99
+ - `LogBusEntry` (interface): interface LogBusEntry — One captured line: when, how bad, which system said it, and what it said.
100
+ - `LogBusSnapshot` (interface): interface LogBusSnapshot — Read-out of the log bus: newest-window entries, the warning/error ring, totals, and evictions.
95
101
  - `LongFrameEvent` (interface): interface LongFrameEvent — Recorded spike event for a frame that exceeded the long-frame threshold, with culprit attribution.
96
102
  - `NormalizedColor` (interface): interface NormalizedColor { readonly hex: string; readonly rgb: string; readonly alpha: number; readonly hasAlpha: boolean } — ⚠ undocumented
97
103
  - `OVERRIDES_FORMAT_VERSION` (const): const OVERRIDES_FORMAT_VERSION: 1 — ⚠ undocumented
98
104
  - `OverrideApplyDiagnostic` (interface): interface OverrideApplyDiagnostic { readonly id: string; readonly reason: string } — ⚠ undocumented
99
105
  - `OverrideApplyResult` (interface): interface OverrideApplyResult — Outcome of applying a devtools overrides payload: counts of applied and skipped entries plus diagnostics.
106
+ - `PerfQuality` (type): type PerfQuality = "full" | "draft" — Graphics tier the record was measured at.
107
+ - `PerfReport` (interface): interface PerfReport — One measured window of one game: the record `bun run perf`, `window.__JG.report()` and `bun run debug` print.
100
108
  - `PhaseStats` (interface): interface PhaseStats — Timing summary for a single named profiling phase across the sampling window.
109
+ - `ProfileEntry` (interface): interface ProfileEntry — One sampled stack frame reduced to a readable key and its self time.
110
+ - `ProfileReport` (interface): interface ProfileReport — CPU profile summary; only the CDP harness can produce one.
111
+ - `RenderReport` (interface): interface RenderReport — Renderer-side counts for the measured window; `null` where the source could not read them.
101
112
  - `RenderSample` (interface): interface RenderSample — Snapshot of renderer resource counts (draw calls, triangles, geometries, textures) for a frame.
113
+ - `ReportLogLine` (interface): interface ReportLogLine — One log line as it appears in the report.
114
+ - `ReportLogs` (interface): interface ReportLogs — Bounded log read-out carried by the report: warn/error lines survive even a flood of info logs.
115
+ - `ReportTelemetry` (interface): interface ReportTelemetry — Who produced the report, and how much producing it cost.
102
116
  - `ScanFieldMeta` (interface): interface ScanFieldMeta extends TunableOptions { kind?: DevtoolsControlKind } — ⚠ undocumented
103
117
  - `ScanMeta` (type): type ScanMeta = Readonly<Record<string, ScanFieldMeta>> — ⚠ undocumented
118
+ - `SceneNodeSource` (type): type SceneNodeSource = "scene-graph" | "heap-object3d" | "unavailable" — Where the scene-node count came from: the live scene graph, a heap walk, or nowhere.
119
+ - `SystemProbe` (interface): interface SystemProbe — What one system reports about itself: totals it keeps anyway, named timings, and one state line.
120
+ - `SystemProbeReading` (interface): interface SystemProbeReading — One system's probe read out on demand; missing sections stay empty rather than absent.
104
121
  - `Tunable` (interface): interface Tunable<T> — Typed handle to a registered control, exposing its live value and subscribe/set/reset operations.
105
122
  - `TunableAccessor` (interface): interface TunableAccessor — Get/set accessor pair plus initial value used to bind a discovered field to a control.
106
123
  - `TunableChoice` (interface): interface TunableChoice<T = unknown> { readonly value: T; readonly label?: string } — ⚠ undocumented
@@ -139,9 +156,20 @@
139
156
  ## @jgengine/core/devtools/frame
140
157
 
141
158
  - `FrameModule` (interface): interface FrameModule — Frame subsystem exposing the frame-timing, profiling, and render-sample facades.
142
- - `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — Frame duration in milliseconds above which a frame is recorded as a long-frame spike (~30fps budget).
143
159
  - `createFrameModule` (const): const createFrameModule: (deps: { readProbes: () => Record<string, unknown>; }) => FrameModule — Create the frame subsystem that records frame/sim/phase timings, computes stats, and captures long-frame events.
144
160
 
161
+ ## @jgengine/core/devtools/logBus
162
+
163
+ - `LOG_BUS_CAPACITY` (const): const LOG_BUS_CAPACITY: 200 — Default measured window for the log ring; older lines are evicted, never grown.
164
+ - `LOG_BUS_MESSAGE_MAX` (const): const LOG_BUS_MESSAGE_MAX: 400 — Longest kept message; a longer one is cut once, at write time.
165
+ - `LOG_BUS_SEVERE_CAPACITY` (const): const LOG_BUS_SEVERE_CAPACITY: 64 — Warnings and errors also land in their own ring, so an error is never pushed out by info traffic.
166
+ - `LogBus` (interface): interface LogBus — A fixed-capacity ring of log lines tagged by owning system. `write` reuses a preallocated slot — no array growth, no splice, no per-write object — so a hot path can call it and only the message string (already built by the caller) costs an allocation.
167
+ - `LogBusCounts` (interface): interface LogBusCounts — Lifetime totals per level, plus how many lines the ring dropped.
168
+ - `LogBusEntry` (interface): interface LogBusEntry — One captured line: when, how bad, which system said it, and what it said.
169
+ - `LogBusOptions` (interface): interface LogBusOptions — Tunables for the log ring: capacity, the severity ring's capacity, and the clock.
170
+ - `LogBusSnapshot` (interface): interface LogBusSnapshot — Read-out of the log bus: newest-window entries, the warning/error ring, totals, and evictions.
171
+ - `createLogBus` (function): function createLogBus(options: LogBusOptions = {}): LogBus — Create the bounded log bus the devtools log facade writes through.
172
+
145
173
  ## @jgengine/core/devtools/logs
146
174
 
147
175
  - `LogsModule` (interface): interface LogsModule — Logs subsystem exposing the log-capture and latency-tracking facades.
@@ -153,6 +181,27 @@
153
181
  - `OverridesModule` (interface): interface OverridesModule — Overrides subsystem exposing the export/apply facade for persisting and restoring control values.
154
182
  - `createOverridesModule` (const): const createOverridesModule: (deps: { signal: ChangeSignal; controlRecords: Map<string, ControlRecord>; writeControl: (record: ControlRecord, raw: unknown) => boolean; discoveredRecords: Map<string, DiscoveredRecord>; enableDiscovered: (id: string) => void; }) => OverridesModule — Create the overrides subsystem that serializes changed control values and re-applies a saved overrides payload.
155
183
 
184
+ ## @jgengine/core/devtools/report
185
+
186
+ - `FrameWindow` (interface): interface FrameWindow — Frame timings over the measured window, as the player sees them.
187
+ - `GlReport` (interface): interface GlReport — GPU-object creation counts. `total` is since the page loaded, the named fields since warmup.
188
+ - `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — The one perf record shape. `bun run perf` (CDP, out of process) and the in-page telemetry report (`window.__JG.report()`, the dev-server route, `bun run debug`) both produce exactly this object, so a budget check and a diff read either one.
189
+ - `PerfQuality` (type): type PerfQuality = "full" | "draft" — Graphics tier the record was measured at.
190
+ - `PerfReport` (interface): interface PerfReport — One measured window of one game: the record `bun run perf`, `window.__JG.report()` and `bun run debug` print.
191
+ - `ProfileEntry` (interface): interface ProfileEntry — One sampled stack frame reduced to a readable key and its self time.
192
+ - `ProfileReport` (interface): interface ProfileReport — CPU profile summary; only the CDP harness can produce one.
193
+ - `RenderReport` (interface): interface RenderReport — Renderer-side counts for the measured window; `null` where the source could not read them.
194
+ - `ReportLogLine` (interface): interface ReportLogLine — One log line as it appears in the report.
195
+ - `ReportLogs` (interface): interface ReportLogs — Bounded log read-out carried by the report: warn/error lines survive even a flood of info logs.
196
+ - `ReportTelemetry` (interface): interface ReportTelemetry — Who produced the report, and how much producing it cost.
197
+ - `SceneNodeSource` (type): type SceneNodeSource = "scene-graph" | "heap-object3d" | "unavailable" — Where the scene-node count came from: the live scene graph, a heap walk, or nowhere.
198
+
199
+ ## @jgengine/core/devtools/reportEndpoint
200
+
201
+ - `REPORT_ENDPOINT_PATH` (const): const REPORT_ENDPOINT_PATH: "/__jgengine/report" — The dev-only route a running game pushes its in-page report to, and that `bun run debug <game>` reads back. One process: the page gathers, the dev server holds the newest record per game, the CLI fetches it. Nothing here ships in a production build — the Vite plugin mounts it under `apply: "serve"`.
202
+ - `ReportPushBody` (interface): interface ReportPushBody — Body the page POSTs: the game it measured and the record itself.
203
+ - `ReportRouteError` (interface): interface ReportRouteError — Body sent when there is nothing to return yet.
204
+
156
205
  ## @jgengine/core/devtools/rewriteTunables
157
206
 
158
207
  - `TunableDelta` (interface): interface TunableDelta — One changed tunable to write back into game source: which table, key path, and new value.
@@ -203,8 +252,10 @@
203
252
 
204
253
  - `Devtools` (interface): interface Devtools — Top-level devtools facade aggregating frame, profile, render, logs, latency, controls, discover, overrides, and probes.
205
254
  - `DevtoolsControl` (interface): interface DevtoolsControl — Registered tunable control with its resolved schema metadata and read/write/reset accessors.
206
- - `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, and formatted message.
255
+ - `DevtoolsLatency` (interface): interface DevtoolsLatency — Latency samples recorded by instrumented transport methods.
256
+ - `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, owning system tag, and formatted message.
207
257
  - `DevtoolsLogLevel` (type): type DevtoolsLogLevel = "log" | "info" | "warn" | "error" — Severity level for a captured devtools log entry.
258
+ - `DevtoolsLogs` (interface): interface DevtoolsLogs — Bounded log ring with per-system tags, a warning/error ring, and lifetime counts.
208
259
  - `DevtoolsSnapshot` (interface): interface DevtoolsSnapshot — Serializable point-in-time snapshot of devtools state: frame, render, latency, logs, controls, and discovered fields.
209
260
  - `DiscoveredEntry` (interface): interface DiscoveredEntry — A control auto-discovered by scanning an object/table, with its binding metadata and current reader.
210
261
  - `FrameRecordSample` (interface): interface FrameRecordSample — Per-frame timing input recorded by callers: total frame time, sim time, and optional named phase durations.
@@ -214,6 +265,8 @@
214
265
  - `OverrideApplyResult` (interface): interface OverrideApplyResult — Outcome of applying a devtools overrides payload: counts of applied and skipped entries plus diagnostics.
215
266
  - `PhaseStats` (interface): interface PhaseStats — Timing summary for a single named profiling phase across the sampling window.
216
267
  - `RenderSample` (interface): interface RenderSample — Snapshot of renderer resource counts (draw calls, triangles, geometries, textures) for a frame.
268
+ - `SystemProbe` (interface): interface SystemProbe — What one system reports about itself: totals it keeps anyway, named timings, and one state line.
269
+ - `SystemProbeReading` (interface): interface SystemProbeReading — One system's probe read out on demand; missing sections stay empty rather than absent.
217
270
  - `Tunable` (interface): interface Tunable<T> — Typed handle to a registered control, exposing its live value and subscribe/set/reset operations.
218
271
  - `TunableAccessor` (interface): interface TunableAccessor — Get/set accessor pair plus initial value used to bind a discovered field to a control.
219
272
 
@@ -232,7 +285,7 @@
232
285
 
233
286
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
234
287
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
235
- - `VERSION` (const): const VERSION: "0.25.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
288
+ - `VERSION` (const): const VERSION: "0.27.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
236
289
 
237
290
  ## @jgengine/core/runtime/adapter
238
291
 
@@ -48,6 +48,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
48
48
 
49
49
  - `defineSystem` (function) · `import { defineSystem } from "@jgengine/shell/gameKit"`
50
50
 
51
+ ## devtools-log-bus — a bounded, per-system-tagged log ring a system writes to and a report reads
52
+
53
+ - `LogBus` (interface) · `import { LogBus } from "@jgengine/core/devtools/logBus"`
54
+
51
55
  ## editor-catalogs — Export typed gameplay catalogs for in-editor tuning.
52
56
 
53
57
  - `EditorCatalogDefinition` (interface) · `import { EditorCatalogDefinition } from "@jgengine/shell/gameKit"`
@@ -33,10 +33,30 @@
33
33
  - `createI18n` (function): function createI18n(options: I18nOptions): I18n — Create a translator over a message catalog: active-locale lookup with a fallback-locale chain, `{param}` interpolation, and `Intl.PluralRules`-based pluralization. Observable (`subscribe`) so React re-renders on `setLocale`; the catalog is caller-owned static data, the locale is the only state.
34
34
  - `interpolate` (function): function interpolate(template: string, params?: TParams): string — Replace `{name}` placeholders in `template` with `params[name]`. Unknown placeholders are left intact. Pure and locale-independent.
35
35
 
36
+ ## @jgengine/core/render/colorPipeline
37
+
38
+ - `AutoExposureConfig` (interface): interface AutoExposureConfig — Bounds and adaptation policy for clamped auto-exposure.
39
+ - `DEFAULT_AUTO_EXPOSURE` (const): const DEFAULT_AUTO_EXPOSURE: Readonly<Required<AutoExposureConfig>> — Default clamped auto-exposure. The range is deliberately asymmetric: a bright street is already readable after AgX's shoulder, while a lamp-lit interior is the case that crushes, so the meter may lift 1.3 stops but only darken 0.7 — an artist's exposure survives.
40
+ - `METERING_HIGHLIGHT_CEILING` (const): const METERING_HIGHLIGHT_CEILING: number — Pixels count toward metering up to this multiple of {@link WHITE_POINT}. A window or a sun disc sits far above it and would otherwise pull the whole frame down to a silhouette.
41
+ - `MID_GREY` (const): const MID_GREY: 0.18 — Linear scene luminance of an 18% grey card — the auto-exposure metering target.
42
+ - `WHITE_POINT` (const): const WHITE_POINT: 1 — Calibration of the output pipeline, in linear scene luminance (1.0 = a white-looking surface at exposure 1).
43
+ - `adaptExposure` (function): function adaptExposure(current: number, target: number, deltaSeconds: number, adaptationSeconds = DEFAULT_AUTO_EXPOSURE.adaptationSeconds): number — Frame-rate independent blend of `current` toward `target`, exponential in time so a hitch does not overshoot and a steady view settles. `deltaSeconds` may be 0..∞; a zero-length step keeps `current`.
44
+ - `exposureForKey` (function): function exposureForKey(keyLuminance: number, config: AutoExposureConfig = {}): number — Exposure that places a frame whose key luminance is `keyLuminance` on {@link MID_GREY}, bounded by the authored range. A dark interior asks for more than `max` and gets `max`; a blown-out exterior asks for less than `min` and gets `min`, so the frame never runs away at either end.
45
+ - `keyLuminanceFromSamples` (function): function keyLuminanceFromSamples(samples: ArrayLike<number>): number — Key luminance of a metered frame: the median of its samples, each clamped to {@link METERING_HIGHLIGHT_CEILING}. A median rather than a mean because a bright sky, a window or a lamp is a large minority of the frame: averaging those in drags an interior down to a silhouette, where the median still reports the room.
46
+
36
47
  ## @jgengine/core/render/environment
37
48
 
38
49
  - `EnvironmentSource` (type): type EnvironmentSource = | { kind: "gradient"; sky: string; ground: string; sun?: string; intensity?: number } /** Equirectangular HDR or EXR environment map loaded from `url`. */ | { kind: "hdri"; url: string; rotation?: number; intensity?: number } /** Six faces of a cube environment map, in three… — A declarative source for image-based scene lighting.
39
50
 
51
+ ## @jgengine/core/render/lightProbes
52
+
53
+ - `IrradianceBakeInput` (interface): interface IrradianceBakeInput — Everything a bake needs for one volume: where the grid sits, how much sky reaches it, and what fills the dark.
54
+ - `IrradianceBakeSettings` (interface): interface IrradianceBakeSettings — Authored bake inputs stored beside the samples so a volume can be re-baked without the editor.
55
+ - `IrradianceVolume` (interface): interface IrradianceVolume — One irradiance volume authored on a scene document.
56
+ - `PROBE_STRIDE` (const): const PROBE_STRIDE: number — Floats stored per probe: {@link SH_COEFFICIENTS} × RGB.
57
+ - `SH_COEFFICIENTS` (const): const SH_COEFFICIENTS: 9 — Baked irradiance volume: a grid of second-order (9-coefficient, RGB) spherical-harmonic probes over a box region, sampled at runtime for ambient lighting. It is the datum an interior has that a hemisphere light cannot: a room's fill is its own, not the sky's, and it varies across the space.
58
+ - `bakeIrradianceVolume` (function): function bakeIrradianceVolume(input: IrradianceBakeInput): IrradianceVolume — Bakes an irradiance volume from authored inputs: sky radiance where the region is open, an authored ceiling/floor fill where it is enclosed, plus inverse-square lamp radiance. Each probe projects its directional radiance into L2 SH, so ambient varies across the grid and keeps a lit side instead of flattening to one number.
59
+
40
60
  ## @jgengine/core/render/lightingBalance
41
61
 
42
62
  - `DEFAULT_IBL_AMBIENT_SCALE` (const): const DEFAULT_IBL_AMBIENT_SCALE: 0.5 — Ambient multiplier applied under image-based light when `iblAmbientScale` is unset.
@@ -59,6 +79,16 @@
59
79
  - `mergePostProcessing` (function): function mergePostProcessing(preset: PostProcessingConfig, authored: PostProcessingConfig | undefined): PostProcessingConfig — Deep-merge a game's partial post chain over a preset's. Every field the game set wins, including `false` to drop a stage; fields it left unset come from the preset, and stage objects merge field by field (tuples such as `grade.lift` replace whole).
60
80
  - `resolveGameLook` (function): function resolveGameLook(input: GameLookInput): ResolvedGameLook — Expand a game's `look` into concrete lighting/backdrop/post. The default is `"neutral"`; `"flat"` passes the explicit knobs through untouched. Anything the game authored wins — the preset only fills unset knobs, and it never adds a sky when the world already owns one (so the sky's tuned sun/hemisphere serve as the lighting rig). A styled preset's post stages are deep-merged under a partial game `postProcessing` ({@link mergePostProcessing}); under `"neutral"` a game's own chain is used as authored.
61
81
 
82
+ ## @jgengine/core/render/physicalSky
83
+
84
+ - `MOONLIGHT_COLOR` (const): const MOONLIGHT_COLOR: "#a9c3e8" — Moonlight's colour: scattered sunlight, blue-shifted because the eye's scotopic response peaks there.
85
+ - `MOONLIGHT_PEAK` (const): const MOONLIGHT_PEAK: 0.12 — Peak moonlight as a fraction of the authored sunlight, at the moon's highest point. Real moonlight is ~2.5e-6 of daylight; every game exaggerates it to stay readable, and this is the exaggeration, not a measurement. Games override it through `SkyEnvironmentConfig.moon`.
86
+ - `NIGHT_FOG_COLOR` (const): const NIGHT_FOG_COLOR: "#0a0e16" — Fog color once the sun is gone: the night sky's own haze, lifted enough to keep silhouettes readable.
87
+ - `elevationOf` (function): function elevationOf(direction: readonly [number, number, number]): number — Elevation of a world-space direction in degrees; a zero vector reads as overhead.
88
+ - `moonIllumination` (function): function moonIllumination(moonElevationDeg: number, peak: number = MOONLIGHT_PEAK): number — Moonlight at `sunElevationDeg`'s opposite point: full at the top of the moon's arc, zero while the moon is below the horizon, scaled by `peak` (a fraction of the authored sun intensity).
89
+ - `starOpacity` (function): function starOpacity(sunElevationDeg: number): number — How visible the stars are: none in daylight, half at the first hint of a set sun, full once it is 6° under.
90
+ - `sunlightColor` (function): function sunlightColor(elevationDeg: number): string — The sun's colour at `elevationDeg`, normalised so its brightest channel is full: white overhead, orange at the horizon, moon-blue once it has set. Intensity stays the authored cycle's job.
91
+
62
92
  ## @jgengine/core/render/postProcessing
63
93
 
64
94
  - `AoConfig` (interface): interface AoConfig — Ground-truth ambient occlusion stage — darkens contact creases and cavities for depth.
@@ -66,12 +96,20 @@
66
96
  - `DEFAULT_AO` (const): const DEFAULT_AO: Readonly<AoConfig> — Contact shading used when a post chain leaves `ao` unset: soft creases, no dark halos.
67
97
  - `DofConfig` (interface): interface DofConfig — Depth-of-field (bokeh) stage — throws the fore/background out of focus around a focus distance.
68
98
  - `GradeConfig` (interface): interface GradeConfig — Final colour-grade stage: lift/gain/gamma, saturation, vignette, film grain — applied in display space after tone mapping.
99
+ - `GradeLut` (type): type GradeLut = { url: string; size?: number } | { cube: LutCube } — A grade's LUT: a strip PNG (one `size`-wide tile per blue slice) or a baked cube.
100
+ - `LutCube` (interface): interface LutCube — A baked display-space LUT cube: `size³` RGB entries with red varying fastest, three bytes per entry in 0..255. Kept as plain data so a scene document can author one and a test can assert on it.
69
101
  - `OutlineConfig` (interface): interface OutlineConfig — Ink outlines traced from depth and surface-normal edges — silhouettes plus interior creases.
70
102
  - `PostAaMode` (type): type PostAaMode = "smaa" | "msaa" | false — Edge antialiasing for the post chain. SMAA runs in linear-sRGB before the output tone-map and cleans alpha-tested foliage/particles that MSAA samples alone leave crawling. `"msaa"` keeps only the render-target multisample resolve; `false` disables AA stages.
71
103
  - `PostProcessingConfig` (interface): interface PostProcessingConfig — Declarative post-processing chain (RenderPass → AO → Bloom → SMAA → tone-map output → Grade). Present on a game means the shell mounts an `EffectComposer` and owns the render; absent means the renderer draws directly (unchanged). Each stage is a config object, `false` to skip, or omitted for its default. Pure data — no three.js types leak into core.
72
104
  - `STUDIO_STAGE_POST` (const): const STUDIO_STAGE_POST: PostProcessingConfig — A cinematic "product shot" post preset — the full chain on (contact-AO, soft bloom, a warm film grade with vignette + a touch of grain + chromatic aberration). Meant for a `StudioStage` where a single parametric asset is framed on a backdrop, so every studio reads shipped, not intern-tier. DoF is left off by default (it needs a per-scene focus distance); set `dof` to enable it.
73
105
  - `StylizeConfig` (interface): interface StylizeConfig — Art-style stage that turns the same models and lighting into a different look without touching materials: ink outlines (comic / Borderlands), cel bands (cartoon), pixelation (retro). Runs after tone mapping, before the grade.
74
- - `ToneMappingMode` (type): type ToneMappingMode = "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none" — Renderer tone-mapping curve applied by the post chain's output stage.
106
+ - `ToneMappingMode` (type): type ToneMappingMode = "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none" — Renderer tone-mapping curve applied by the post chain's output stage. `"agx"` is the pipeline default: it keeps hue in saturated highlights and rolls a sun-lit surface off gradually, where `"aces"` skews bright colours toward white.
107
+
108
+ ## @jgengine/core/render/skyOwnedLighting
109
+
110
+ - `SkyOwnedLighting` (interface): interface SkyOwnedLighting — Which half of a game's authored `lighting` a time-of-day sky takes over.
111
+ - `skyNoonKeyframes` (function): function skyNoonKeyframes<T extends SkyEnvironmentConfig>(sky: T, lighting: LightingConfig | undefined, fogColor?: string): T — The authored `lighting` as the day cycle's **noon keyframes**, filling only the sky fields the game left unset. A game's midday sun, fill and fog therefore become the peak the cycle scales and cross-fades from, so an authored world gets a sunset and a night without re-authoring the same numbers in the sky. Authored sky fields always win over the lighting config.
112
+ - `splitSkyOwnedLighting` (function): function splitSkyOwnedLighting(lighting: LightingConfig | undefined): SkyOwnedLighting — Splits authored lighting into the sun/ambient terms a time-of-day sky drives and the rest. Returns an empty `sky` when the game authored no lighting, so callers can keep engine defaults.
75
113
 
76
114
  ## @jgengine/core/render/sprite2d
77
115
 
@@ -256,7 +294,7 @@
256
294
  - `TEXT_SCALE_MAX` (const): const TEXT_SCALE_MAX: 2 — Maximum UI text scale.
257
295
  - `TEXT_SCALE_MIN` (const): const TEXT_SCALE_MIN: 0.75 — Minimum UI text scale.
258
296
  - `TParams` (type): type TParams = Readonly<Record<string, string | number>> — Interpolation values for a message template.
259
- - `ToneMappingMode` (type): type ToneMappingMode = "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none" — Renderer tone-mapping curve applied by the post chain's output stage.
297
+ - `ToneMappingMode` (type): type ToneMappingMode = "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none" — Renderer tone-mapping curve applied by the post chain's output stage. `"agx"` is the pipeline default: it keeps hue in saturated highlights and rolls a sun-lit surface off gradually, where `"aces"` skews bright colours toward white.
260
298
  - `TooltipContent` (interface): interface TooltipContent — Structured tooltip content — a renderer lays these fields out; the model owns what is shown.
261
299
  - `UI_SCALE_MAX` (const): const UI_SCALE_MAX: 1.5 — ⚠ undocumented
262
300
  - `UI_SCALE_MIN` (const): const UI_SCALE_MIN: 0.5 — ⚠ undocumented
@@ -2208,6 +2246,16 @@
2208
2246
 
2209
2247
  - `DebugSummary` (interface): interface DebugSummary — Compact `debug_summary` payload: the numbers an agent needs to judge a run, about 1 KB.
2210
2248
 
2249
+ ## @jgengine/shell/devtools/frameSampler
2250
+
2251
+ - `FrameReportInputs` (interface): interface FrameReportInputs — Everything the report needs, gathered by whichever producer is asking.
2252
+ - `FrameTelemetry` (interface): interface FrameTelemetry — The installed sampler: ask it for a report, reset its window, or tear it down.
2253
+ - `FrameTelemetrySources` (interface): interface FrameTelemetrySources — What the shell hands the sampler: live getters, called only when a report is built.
2254
+ - `GlCounterTotals` (interface): interface GlCounterTotals — GPU-object creation counts, the fields the budget gate compares.
2255
+ - `REPORT_PUSH_MS` (const): const REPORT_PUSH_MS: 1000 — Cadence of the push to the dev server, in milliseconds.
2256
+ - `TELEMETRY_FLAG` (const): const TELEMETRY_FLAG: "telemetry" — In-page frame telemetry: the game reports on itself instead of needing a second process to attach a profiler. Frame timings come from the devtools frame ring, renderer counts from the live renderer, GPU-object creation from wrapped GL calls, and per-system numbers from `devtools.probes` — all read on demand, so nothing here runs on the hot path.
2257
+ - `TELEMETRY_WARMUP_MS` (const): const TELEMETRY_WARMUP_MS: 5000 — How long after the sampler starts the GL counters get their post-warmup baseline.
2258
+
2211
2259
  ## @jgengine/shell/devtools/panelAtoms
2212
2260
 
2213
2261
  - `SectionLabel` (function): function SectionLabel({ children }: { children: string }): React.JSX.Element — ⚠ undocumented
@@ -2244,34 +2292,38 @@
2244
2292
 
2245
2293
  - `DayNightSky` (function): function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, lights = true, keyLightIntensity = 1.1, ambientIntensity = 0.6, }: DayNightSkyProps): React.JSX.Element — Binds a {@link DayNightCycle} to the engine's existing `SkyDome` shader and a pair of lights: each frame it reads `cycle.sample()` and writes the blended tint into the dome's top/horizon/sun uniforms while the sun's arc comes from the shared `daylightStateAt` geometry. This is the turnkey presentation seam — a game mounts this one component in its scene and gets a moving, color-graded day-night sky from the serializable model, with no hand-rolled per-frame lerp and no new renderer. The cycle's `phase`/color values are free-form and never interpreted here.
2246
2294
  - `DayNightSkyProps` (interface): interface DayNightSkyProps — Props for {@link DayNightSky}: the cycle to bind plus optional dome shape and light-strength knobs.
2247
- - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig } — ⚠ undocumented
2248
- - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; ambient?: {skyColor?: string; groundColor?: string; intensi… — ⚠ undocumented
2249
- - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; ambientIntensity: number; skyTop: string; skyBottom: string; background: string } — ⚠ undocumented
2295
+ - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig; sunColor?: string; fogColor?: string; moonIntensity?: number; moonColor?: string } — ⚠ undocumented
2296
+ - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; moon?: {position?: readonly [number, number, number]; inten… — ⚠ undocumented
2297
+ - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; sunColor: string; ambientIntensity: number; skyTop: string; skyBottom: string; background: string; fogColor: string; moonPosition: [number, number, number]; moonIntensity: numbe… — ⚠ undocumented
2250
2298
  - `EnvironmentScene` (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element — ⚠ undocumented
2251
2299
  - `EnvironmentSceneProps` (interface): interface EnvironmentSceneProps { feature: EnvironmentWorldFeature } — ⚠ undocumented
2252
2300
  - `SKY_PRESET_DAY_FRACTION` (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number> — ⚠ undocumented
2253
- - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2254
- - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; hazeStrength?: number; cloudiness?: number; sunGlow… — ⚠ undocumented
2301
+ - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2302
+ - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; moonDirection?: readonly [number, number, number]; … — ⚠ undocumented
2255
2303
  - `SkyLightOwnership` (type): type SkyLightOwnership = "authored" | "sky-default" — Policy for composing sky backdrops with `PlayableGame.lighting`: - authored lighting present → sky renders dome + fog only; lights stay game-owned - no authored lighting → sky may emit its default sun/hemisphere with the dome Time-of-day never rewrites configured lights; it only drives sky colors/fog (and sky-owned lights when the game did not author lighting).
2256
- - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2304
+ - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2257
2305
  - `VolumetricCloudsProps` (interface): interface VolumetricCloudsProps — Props for {@link VolumetricClouds} — fully-resolved cloud rules plus the shared sun direction.
2258
2306
 
2259
2307
  ## @jgengine/shell/environment
2260
2308
 
2261
2309
  - `DayNightSky` (function): function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, lights = true, keyLightIntensity = 1.1, ambientIntensity = 0.6, }: DayNightSkyProps): React.JSX.Element — Binds a {@link DayNightCycle} to the engine's existing `SkyDome` shader and a pair of lights: each frame it reads `cycle.sample()` and writes the blended tint into the dome's top/horizon/sun uniforms while the sun's arc comes from the shared `daylightStateAt` geometry. This is the turnkey presentation seam — a game mounts this one component in its scene and gets a moving, color-graded day-night sky from the serializable model, with no hand-rolled per-frame lerp and no new renderer. The cycle's `phase`/color values are free-form and never interpreted here.
2262
2310
  - `DayNightSkyProps` (interface): interface DayNightSkyProps — Props for {@link DayNightSky}: the cycle to bind plus optional dome shape and light-strength knobs.
2263
- - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig } — ⚠ undocumented
2264
- - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; ambient?: {skyColor?: string; groundColor?: string; intensi… — ⚠ undocumented
2265
- - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; ambientIntensity: number; skyTop: string; skyBottom: string; background: string } — ⚠ undocumented
2311
+ - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig; sunColor?: string; fogColor?: string; moonIntensity?: number; moonColor?: string } — ⚠ undocumented
2312
+ - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; moon?: {position?: readonly [number, number, number]; inten… — ⚠ undocumented
2313
+ - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; sunColor: string; ambientIntensity: number; skyTop: string; skyBottom: string; background: string; fogColor: string; moonPosition: [number, number, number]; moonIntensity: numbe… — ⚠ undocumented
2266
2314
  - `EnvironmentScene` (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element — ⚠ undocumented
2267
2315
  - `EnvironmentSceneProps` (interface): interface EnvironmentSceneProps { feature: EnvironmentWorldFeature } — ⚠ undocumented
2268
2316
  - `SKY_PRESET_DAY_FRACTION` (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number> — ⚠ undocumented
2269
- - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2270
- - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; hazeStrength?: number; cloudiness?: number; sunGlow… — ⚠ undocumented
2317
+ - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2318
+ - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; moonDirection?: readonly [number, number, number]; … — ⚠ undocumented
2271
2319
  - `SkyLightOwnership` (type): type SkyLightOwnership = "authored" | "sky-default" — Policy for composing sky backdrops with `PlayableGame.lighting`: - authored lighting present → sky renders dome + fog only; lights stay game-owned - no authored lighting → sky may emit its default sun/hemisphere with the dome Time-of-day never rewrites configured lights; it only drives sky colors/fog (and sky-owned lights when the game did not author lighting).
2272
- - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2320
+ - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2273
2321
  - `VolumetricCloudsProps` (interface): interface VolumetricCloudsProps — Props for {@link VolumetricClouds} — fully-resolved cloud rules plus the shared sun direction.
2274
2322
 
2323
+ ## @jgengine/shell/environment/AuthoredDaylight
2324
+
2325
+ - `AuthoredDaylightProps` (interface): interface AuthoredDaylightProps — Props for {@link AuthoredDaylight}: the authored sky whose values are the noon keyframe, the world clock, and the holder configured lights read.
2326
+
2275
2327
  ## @jgengine/shell/environment/DayNightSky
2276
2328
 
2277
2329
  - `DayNightSky` (function): function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, lights = true, keyLightIntensity = 1.1, ambientIntensity = 0.6, }: DayNightSkyProps): React.JSX.Element — Binds a {@link DayNightCycle} to the engine's existing `SkyDome` shader and a pair of lights: each frame it reads `cycle.sample()` and writes the blended tint into the dome's top/horizon/sun uniforms while the sun's arc comes from the shared `daylightStateAt` geometry. This is the turnkey presentation seam — a game mounts this one component in its scene and gets a moving, color-graded day-night sky from the serializable model, with no hand-rolled per-frame lerp and no new renderer. The cycle's `phase`/color values are free-form and never interpreted here.
@@ -2279,10 +2331,10 @@
2279
2331
 
2280
2332
  ## @jgengine/shell/environment/Daylight
2281
2333
 
2282
- - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; ambient?: {skyColor?: string; groundColor?: string; intensi… — ⚠ undocumented
2283
- - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2284
- - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; hazeStrength?: number; cloudiness?: number; sunGlow… — ⚠ undocumented
2285
- - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2334
+ - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; moon?: {position?: readonly [number, number, number]; inten… — ⚠ undocumented
2335
+ - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2336
+ - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; moonDirection?: readonly [number, number, number]; … — ⚠ undocumented
2337
+ - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2286
2338
 
2287
2339
  ## @jgengine/shell/environment/EnvironmentScene
2288
2340
 
@@ -2310,9 +2362,10 @@
2310
2362
  - `DEFAULT_DAY_SKY_BOTTOM` (const): const DEFAULT_DAY_SKY_BOTTOM: "#e3f4ff" — ⚠ undocumented
2311
2363
  - `DEFAULT_DAY_SKY_TOP` (const): const DEFAULT_DAY_SKY_TOP: "#3fa4f2" — ⚠ undocumented
2312
2364
  - `DEFAULT_DAY_SUN_INTENSITY` (const): const DEFAULT_DAY_SUN_INTENSITY: 1 — ⚠ undocumented
2365
+ - `DEFAULT_SUN_COLOR` (const): const DEFAULT_SUN_COLOR: "#fff1c9" — Warm-white noon sun used when the game authored no sun tint (the shell's `SUN_COLOR`).
2313
2366
  - `DEFAULT_SUN_ELEVATION_DEG` (const): const DEFAULT_SUN_ELEVATION_DEG: number — The engine arc's noon height: `atan(1 / SUN_DEPTH_RATIO)`, ~68 degrees.
2314
- - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig } — ⚠ undocumented
2315
- - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; ambientIntensity: number; skyTop: string; skyBottom: string; background: string } — ⚠ undocumented
2367
+ - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig; sunColor?: string; fogColor?: string; moonIntensity?: number; moonColor?: string } — ⚠ undocumented
2368
+ - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; sunColor: string; ambientIntensity: number; skyTop: string; skyBottom: string; background: string; fogColor: string; moonPosition: [number, number, number]; moonIntensity: numbe… — ⚠ undocumented
2316
2369
  - `SKY_PRESET_DAY_FRACTION` (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number> — ⚠ undocumented
2317
2370
 
2318
2371
  ## @jgengine/shell/environment/groundPadMath
@@ -2425,7 +2478,7 @@
2425
2478
 
2426
2479
  ## @jgengine/shell/postfx/PostProcessing
2427
2480
 
2428
- - `PostProcessing` (function): function PostProcessing({ config, quality = "high", stages }: { config: PostProcessingConfig; quality?: GraphicsQuality; stages?: { ao: boolean; bloom: boolean; dof: boolean; smaa: boolean } }): null — Mounts an `EffectComposer` inside the shell Canvas and takes over rendering (priority-1 `useFrame`, which disables R3F auto-render) to run the configured post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Stylize → Grade. Rendered only when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
2481
+ - `PostProcessing` (function): function PostProcessing({ config, quality = "high", stages }: { config: PostProcessingConfig; quality?: GraphicsQuality; stages?: { ao: boolean; bloom: boolean; dof: boolean; smaa: boolean } }): null — Mounts an `EffectComposer` inside the shell Canvas and takes over rendering (priority-1 `useFrame`, which disables R3F auto-render) to run the configured post chain: RenderPass → GTAO → UnrealBloom → SMAA → exposure meter → OutputPass → Stylize → Grade. Rendered only when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
2429
2482
 
2430
2483
  ## @jgengine/shell/postfx/ScreenEffectsOverlay
2431
2484
 
@@ -2476,7 +2529,7 @@
2476
2529
  ## @jgengine/shell/render/SceneLighting
2477
2530
 
2478
2531
  - `BackdropFog` (function): function BackdropFog({ fog }: { fog: BackdropConfig["fog"] }): React.JSX.Element | null — ⚠ undocumented
2479
- - `ConfiguredLighting` (function): function ConfiguredLighting({ lighting, profile }: { lighting: LightingConfig; profile?: GraphicsProfile }): React.JSX.Element — ⚠ undocumented
2532
+ - `ConfiguredLighting` (function): function ConfiguredLighting({ lighting, profile, modulation, }: { lighting: LightingConfig; profile?: GraphicsProfile; /** Day-cycle scaling for the first directional (the sun) and the ambient fill. */ modulation?: DaylightModulation; }): React.JSX.Element — ⚠ undocumented
2480
2533
  - `POINT_LIGHT_BUDGET` (const): const POINT_LIGHT_BUDGET: 8 — Default cap for combined dynamic point and spot lights.
2481
2534
  - `syncShadowProjection` (function): function syncShadowProjection(light: DirectionalLight): void — Refresh a directional light's shadow projection after its frustum bounds change. R3F pierced `shadow-camera-*` props write the bounds but never call `updateProjectionMatrix()`, so depth renders with the stale default ~10×10 box and `shadowCameraSize` is silently inert.
2482
2535
 
@@ -2512,6 +2565,15 @@
2512
2565
  - `SurfaceShape` (type): type SurfaceShape = "box" | "cylinder" — Primitive UV layout whose physical dimensions drive `repeatMetres`.
2513
2566
  - `useAuthoredSurfaceMaterial` (function): function useAuthoredSurfaceMaterial(config: AuthoredSurfaceConfig, shape: SurfaceShape = "box"): THREE.MeshStandardMaterial — Load declared surface-map roles and own the material/map clones (cached textures remain untouched). Colour, emissive, sheen-colour and specular-colour maps are sRGB; numeric maps are linear. Cylinder side metres use the RMS ellipse circumference approximation. Static batches and custom meshes share this lifecycle.
2514
2567
 
2568
+ ## @jgengine/shell/render/decalGeometry
2569
+
2570
+ - `DecalDrapeGeometry` (interface): interface DecalDrapeGeometry — Baked vertex/index buffers for one draped ground decal.
2571
+ - `DecalDrapeOptions` (interface): interface DecalDrapeOptions — Inputs for {@link createDecalDrapeGeometry}.
2572
+
2573
+ ## @jgengine/shell/render/decalTexture
2574
+
2575
+ - `DecalTextureData` (interface): interface DecalTextureData — Row-major RGBA bytes plus their edge length, ready for a `THREE.DataTexture`.
2576
+
2515
2577
  ## @jgengine/shell/render/materialAppearanceSignals
2516
2578
 
2517
2579
  - `RenderMaterialAppearanceBaseline` (interface): interface RenderMaterialAppearanceBaseline extends MaterialAppearanceBaseline — Immutable resolved baseline for one caller-owned assigned material, including its original card exposure.
@@ -2571,6 +2633,14 @@
2571
2633
  - `cloneModelScene` (function): function cloneModelScene(source: THREE.Object3D, options?: { cloneMaterials?: boolean; shadows?: ModelShadowMode; surfaceDetail?: boolean }): THREE.Object3D — Clone a model with independent pose and materials, retaining shared geometry and textures.
2572
2634
  - `disposeModelScene` (function): function disposeModelScene(root: THREE.Object3D): void — Release materials and bone textures owned by `cloneModelScene`; shared assets and attached models remain owned by their callers.
2573
2635
 
2636
+ ## @jgengine/shell/render/modelWind
2637
+
2638
+ - `ResolvedModelWind` (interface): interface ResolvedModelWind — Resolved sway settings baked into one material.
2639
+ - `WindClock` (function): function WindClock(): null — Advances the shared gust clock once per frame; mount one per 3D world.
2640
+ - `applyModelWind` (function): function applyModelWind(material: THREE.MeshStandardMaterial, wind: ResolvedModelWind): THREE.MeshStandardMaterial — Compiles height-ramped wind sway into a standard material's vertex shader. Idempotent per material; the clock and direction are shared module uniforms, so the whole scene advances with one write per frame and no per-material bookkeeping.
2641
+ - `windClock` (const): const windClock: { value: number; } — Shared gust clock every foliage material reads; advanced once per frame by `WindClock`.
2642
+ - `windDirection` (const): const windDirection: Vector2 — Shared gust direction (XZ); a model may override it with its own authored direction.
2643
+
2574
2644
  ## @jgengine/shell/render/projectedOverdraw
2575
2645
 
2576
2646
  - `ProjectedOverdrawOptions` (interface): interface ProjectedOverdrawOptions — Sampling resolution; lower resolutions change thin geometry and alpha-mask raster coverage.
@@ -2604,6 +2674,13 @@
2604
2674
 
2605
2675
  - `drawProceduralTileLayer` (function): function drawProceduralTileLayer(canvas: CanvasFixture): TilemapWorldConfig — Draw the deterministic tile atlas used by textured tile-layer captures and tests.
2606
2676
 
2677
+ ## @jgengine/shell/render/triplanarMaterial
2678
+
2679
+ - `TriplanarAssetMaterial` (function): function TriplanarAssetMaterial({ asset, ...options }: { asset: MaterialAsset } & { tileMetres?: number; sharpness?: number; normalScale?: number }): React.JSX.Element — Mounts {@link useTriplanarAssetMaterial} on a mesh; the R3F renderer owns disposal.
2680
+ - `TriplanarProjectionOptions` (interface): interface TriplanarProjectionOptions — Inputs for {@link applyTriplanarProjection}.
2681
+ - `applyTriplanarProjection` (function): function applyTriplanarProjection(material: THREE.MeshStandardMaterial, options: TriplanarProjectionOptions): THREE.MeshStandardMaterial — Projects a standard material's maps from world position instead of mesh UVs, so world geometry without a `uv` attribute still renders a textured PBR surface. Instanced meshes project in their instanced world space. Skinned meshes keep the bind-pose normal for the blend weights, which is close enough for the static world surfaces this path exists for.
2682
+ - `useTriplanarAssetMaterial` (function): function useTriplanarAssetMaterial(asset: MaterialAsset, options: { tileMetres?: number; sharpness?: number; normalScale?: number } = {}): THREE.MeshStandardMaterial — Render a serializable material asset — one built by `libraryMaterialAsset`, or authored in the editor — with world-space triplanar projection, so world geometry needs no UVs. Tile size defaults to the asset's own `physicalSize`, which the shared library sets per surface.
2683
+
2607
2684
  ## @jgengine/shell/render/useDisposable
2608
2685
 
2609
2686
  - `Disposable` (interface): interface Disposable — Anything exposing dispose() — three.js geometries, materials, textures, render targets.
@@ -2635,7 +2712,7 @@
2635
2712
  ## @jgengine/shell/render/useModelInstance
2636
2713
 
2637
2714
  - `ModelInstance` (interface): interface ModelInstance — Render `scene` with `position` and uniform `scale`; animate or deform `content` inside that placement frame.
2638
- - `ModelInstanceConfig` (type): type ModelInstanceConfig = Pick<ModelConfig, "url" | "scale" | "targetHeight" | "y" | "anchor" | "dims" | "shadows" | "animation" | "hiddenNodes" | "surfaceDetail" > — Model loading, placement, shadows and animation for a custom renderer.
2715
+ - `ModelInstanceConfig` (type): type ModelInstanceConfig = Pick<ModelConfig, "url" | "scale" | "targetHeight" | "y" | "anchor" | "dims" | "shadows" | "animation" | "hiddenNodes" | "surfaceDetail" | "wind" > — Model loading, placement, shadows and animation for a custom renderer.
2639
2716
  - `ModelInstanceOptions` (interface): interface ModelInstanceOptions — Caller-owned styling runs on the isolated clone, never the loader cache.
2640
2717
  - `useModelInstance` (function): function useModelInstance(model: ModelInstanceConfig, options: ModelInstanceOptions = {}): ModelInstance — Load and own an isolated model instance, preserving imported transforms and measuring placement at bind pose. Apply hiddenNodes to the configured clone before measurement; equivalent name lists retain the instance. Mount `scene` with the returned position/scale. Custom materials, deformation and surrounding geometry stay caller-owned. Must run inside an R3F canvas and Suspense boundary; a game provider is optional.
2641
2718
 
@@ -2969,6 +3046,11 @@
2969
3046
  - `EditableGround` (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element — ⚠ undocumented
2970
3047
  - `EditableGroundProps` (interface): interface EditableGroundProps { terrain: Pick<EditableTerrain, "sampleHeight" | "surfaceAt">; bounds: Aabb; segments?: number; version?: number; baseColor?: string; surfaceColors?: Record<string, string> } — ⚠ undocumented
2971
3048
 
3049
+ ## @jgengine/shell/terrain/GrassCover
3050
+
3051
+ - `GrassCover` (function): function GrassCover({ area, seed, colorBase, colorTip, strength, edgeFeather, heightAt, coverageAt, segments, position, }: GrassCoverProps): React.JSX.Element | null — The turf surface under one authored `grass_field` patch: a ground-draped, edge-feathered mat that turns a bare pavement footprint into grass the blade field stands in. Bounded to one draw and a {@link GRASS_COVER_MAX_SEGMENTS}² grid, so a large park still costs a single mesh.
3052
+ - `GrassCoverProps` (interface): interface GrassCoverProps — Props for {@link GrassCover}: one authored patch's footprint, colours and drape samplers.
3053
+
2972
3054
  ## @jgengine/shell/terrain/GrassField
2973
3055
 
2974
3056
  - `DEFAULT_GRASS_COUNT` (const): const DEFAULT_GRASS_COUNT: 1500 — ⚠ undocumented
@@ -2992,6 +3074,13 @@
2992
3074
  - `DEFAULT_GRASS_COUNT` (const): const DEFAULT_GRASS_COUNT: 1500 — ⚠ undocumented
2993
3075
  - `DEFAULT_GRASS_DENSITY` (const): const DEFAULT_GRASS_DENSITY: 4 — Blades per square meter — matches `@jgengine/core/world/vegetation`'s `VEGETATION_DEFAULTS.density`, so an editor-placed grass volume and a bare `<GrassField>` agree on what "4" means.
2994
3076
 
3077
+ ## @jgengine/shell/terrain/grassCoverGeometry
3078
+
3079
+ - `GRASS_COVER_MAX_SEGMENTS` (const): const GRASS_COVER_MAX_SEGMENTS: 96 — Largest grid a cover mat bakes to, per axis — a 66 m park stays one draw at ~1 m cells.
3080
+ - `GRASS_COVER_TILE_METRES` (const): const GRASS_COVER_TILE_METRES: 3 — World metres one cover-texture tile spans; the mat's own grain is sampled in world space.
3081
+ - `GrassCoverGeometry` (interface): interface GrassCoverGeometry — Bake result for {@link createGrassCoverGeometry}; empty arrays when the mat is switched off.
3082
+ - `GrassCoverGeometryOptions` (interface): interface GrassCoverGeometryOptions — Inputs for {@link createGrassCoverGeometry}.
3083
+
2995
3084
  ## @jgengine/shell/terrain/grassGeometry
2996
3085
 
2997
3086
  - `GRASS_TUFT_BLADES` (const): const GRASS_TUFT_BLADES: 5 — Blades baked into each tuft instance — one instance reads as a clump, not a lone blade.
@@ -3048,9 +3137,11 @@
3048
3137
  ## @jgengine/shell/terrain/terrainDetailMaterial
3049
3138
 
3050
3139
  - `ALL_TERRAIN_SURFACE_CLASSES` (const): const ALL_TERRAIN_SURFACE_CLASSES: 511 — Compile every surface response, for an editor viewport whose paint can change to any surface.
3140
+ - `MAX_TERRAIN_LAYER_MAPS` (const): const MAX_TERRAIN_LAYER_MAPS: 4 — At most this many painted layers sample their own scanned map; extra layers keep procedural shading.
3051
3141
  - `TerrainDetailMaterialHandle` (interface): interface TerrainDetailMaterialHandle — The built procedural detail terrain material, ready to mount on the ground mesh.
3052
3142
  - `TerrainDetailMaterialOptions` (interface): interface TerrainDetailMaterialOptions — Surface-pass inputs beyond the resolved detail config.
3053
3143
  - `TerrainDetailMaterialTextures` (interface): interface TerrainDetailMaterialTextures — Loaded PBR textures matching a resolved `ResolvedTerrainDetailMaterial.maps`' roles. The caller (a React component, via `useTexture`/`useLoader`) owns loading and disposal; `core` never touches `THREE.Texture` and this shell function never fetches a URL itself. Omitted AO/roughness skip sampling.
3144
+ - `TerrainLayerMap` (interface): interface TerrainLayerMap — One painted layer's scanned color map plus the world metres one tile covers.
3054
3145
 
3055
3146
  ## @jgengine/shell/terrain/terrainMath
3056
3147
 
@@ -177,6 +177,8 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
177
177
  ## day-night-sky — drop-in R3F presenter that drives the engine's existing sky dome and lights from a serializable day-night cycle for a moving, color-graded day-night look
178
178
 
179
179
  - `DayNightSky` (function) · `import { DayNightSky } from "@jgengine/shell/environment"`
180
+ - `skyNoonKeyframes` (function) · `import { skyNoonKeyframes } from "@jgengine/core/render/skyOwnedLighting"`
181
+ - `splitSkyOwnedLighting` (function) · `import { splitSkyOwnedLighting } from "@jgengine/core/render/skyOwnedLighting"`
180
182
 
181
183
  ## debounced-commit — bind a slider/number/color control to an expensive commit path (scene-document patch, settings persist) — instant local mirror, one trailing commit per pause, flush on release/blur/unmount; no raw onChange→patch wiring
182
184
 
@@ -401,6 +403,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
401
403
 
402
404
  - `KillFeed` (function) · `import { KillFeed } from "@jgengine/react"`
403
405
 
406
+ ## light-probes — bake an irradiance volume from authored sky, fill and lamp inputs
407
+
408
+ - `bakeIrradianceVolume` (function) · `import { bakeIrradianceVolume } from "@jgengine/core/render/lightProbes"`
409
+
404
410
  ## live-markers — a self-ticking MarkerSet kept in sync from the live scene each HUD tick — pass a rebuild(markers, ctx) that clears+repopulates, no hand-rolled setInterval
405
411
 
406
412
  - `useLiveMarkers` (function) · `import { useLiveMarkers } from "@jgengine/react"`
@@ -440,6 +446,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
440
446
 
441
447
  - `configureMaterialTexture` (function) · `import { configureMaterialTexture } from "@jgengine/shell/render/materialAsset"`
442
448
 
449
+ ## material-triplanar — render a material asset on world geometry without UVs
450
+
451
+ - `useTriplanarAssetMaterial` (function) · `import { useTriplanarAssetMaterial } from "@jgengine/shell/render/triplanarMaterial"`
452
+
443
453
  ## menu-router — screen stack for a game front-end — push/back/replace/reset with optional Escape-to-back
444
454
 
445
455
  - `useMenuRouter` (function) · `import { useMenuRouter } from "@jgengine/react/menuRouter"`
@@ -498,6 +508,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
498
508
 
499
509
  - `modelLoadStarts` (function) · `import { modelLoadStarts } from "@jgengine/shell/render/modelLoad"`
500
510
 
511
+ ## model-wind — sway a foliage model in the shared gust field
512
+
513
+ - `applyModelWind` (function) · `import { applyModelWind } from "@jgengine/shell/render/modelWind"`
514
+
501
515
  ## multiplayer-p2p — bootstrap a playable authoritative world for browser peer co-op
502
516
 
503
517
  - `resolvePeerShellMultiplayer` (function) · `import { resolvePeerShellMultiplayer } from "@jgengine/shell/multiplayer"`
@@ -84,9 +84,11 @@ Do not create a primitive whose only value is wrapping a `div` with border radiu
84
84
 
85
85
  The post chain itself is never hand-wired render passes. Set `defineGame({ postProcessing })` (`PostProcessingConfig` from `@jgengine/core/render/postProcessing`) and the shell mounts an `EffectComposer` and owns the render: RenderPass → AO → Bloom → tone-map output → Stylize → Grade. Each stage is a config object, `false` to skip, or omitted for its tuned default:
86
86
 
87
- - `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `aces`) + `exposure`.
87
+ - `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `agx`, which keeps hue in saturated highlights; `toon` picks `linear` on purpose) + `exposure` (default 1).
88
+ - `autoExposure: { min, max, adaptationSeconds } | false` — clamped auto-exposure. Every four frames the shell meters the pre-tone-map frame to a 16×16 log-luminance grid and drives `exposure` toward 18% grey (`MID_GREY`), bounded by `min`/`max` (defaults 0.5×..2.5×, 0.6 s adaptation). `exposure` stays the baseline it corrects around; `false` pins it. One white point calibrates the whole pipeline: `WHITE_POINT = 1` (`@jgengine/core/render/colorPipeline`) is the linear luminance AgX prints as white, and pixels up to 8× it count toward metering so a window cannot set the key.
88
89
  - `bloom: { strength, radius, threshold }` — HDR glow around bright pixels (defaults 0.32 / 0.55 / 0.85).
89
- - `grade: { lift, gain, gamma, saturation, vignette, grain }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain.
90
+ - `grade: { lift, gain, gamma, saturation, vignette, grain, lut }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain. `lut` is either a strip PNG (`{ url, size }`) or a baked cube (`{ cube: { size, data } }`), trilinearly sampled after the numeric grade.
91
+ - **Authored per game.** A scene document's `environment.grading` (Lighting workspace panel) bakes a small display LUT from shadow/midtone/highlight swatches plus saturation and contrast, and carries the clamped exposure range. The shell reads it from `editorLayers` and folds it over the game's static post config, so the authored LUT and range win where they exist and an ungraded game mounts exactly the chain it did before. Author it in `editor.scene.json`; do not hardcode a grade in the game config.
90
92
  - `ao: { radius, intensity, distanceFalloff, blend }` — ground-truth ambient occlusion. Omitted runs `DEFAULT_AO` on tiers whose `postStages.ao` is on (medium and up; low skips it); `ao: false` turns it off everywhere. It adds a normal/depth prepass (one extra draw per opaque mesh) and is fill-heavy: 8 samples on medium, 12 on high, 16 on ultra.
91
93
  - `stylize: { outline, bands, pixelSize }` — art style without touching materials. `outline: { color, thickness, opacity, depthThreshold, normalThreshold, fadeDistance }` traces ink lines from depth and normal edges (one extra normal/depth scene render, shadow maps reused); `bands` quantizes brightness into cel bands keeping hue; `pixelSize` snaps the frame to retro blocks. Mix freely: outline alone is a clean comic ink, `bands: 3` plus outline is cartoon, `pixelSize` plus `bands` is retro. Mark custom overlay effects with `POSTFX_OVERLAY_USERDATA` so they are not outlined; transparent, alpha-cut, points, lines and sprites are skipped automatically.
92
94