@jgengine/convex 0.53.0 → 0.54.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,275 @@ 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.54.0
30
+
31
+ ### Migrate
32
+
33
+ - **Bump lockstep SDK packages to `^0.54.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.51.0`; `@jgengine/github` is `0.41.0`.
34
+ - the grass meta group species (second species) is now mix; GrassCover's stripeAt prop is now shadeAt.
35
+
36
+ ### Added
37
+
38
+ - `createDoseTable` (`@jgengine/core/stats/doseTable`): What each source does to each kind of receiver, as {@link VitalDose}s: a raptor's foods and how much taming, food and ineffectiveness each gives, a sim's meals and the needs they fill, a shooter's potions and the shield each pours in up to its cap, a dart's torpor by the body it lands in. Also `DoseTable`, `DoseTableOptions`, `DoseTableRows`, `hasRoomFor`.
39
+ - `createVitals` (`@jgengine/core/stats/vitals`): Survival vitals for one body — health, stamina, food, water, oxygen, torpor or anything the game names. Also `VitalDef`, `VitalDose`, `VitalFlag`, `VitalFlagChange`, `VitalLink`, `VitalRateModifier`, `Vitals`, `VitalsState` and 1 more.
40
+ - `importedPackNames` (`@jgengine/assets`): The original export names imported from `generated/packs`, including imports renamed locally.
41
+ - `importedPackNames` (`@jgengine/assets/manifest`): The original export names imported from `generated/packs`, including imports renamed locally.
42
+ - `LINGER_MS` (`@jgengine/core/run/lingering`): How long a resource whose last user left waits before it is freed. Also `Lingering`.
43
+ - `nextFrame` (`@jgengine/core/run/nextFrame.draw`): Resolves on the next animation frame, or a zero-delay timer in tests and SSR.
44
+ - `DECAL_CELL` (`@jgengine/core/scene/DecalRenderer.draw`): Edge in metres of the world cell one merged decal draw covers, so an unseen cell still culls whole.
45
+ - bun run perf <game> --profile also prints what allocates per second of play, the source of garbage collection pauses
46
+ - `SCENE_PROBE_REFRESH_MS` (`@jgengine/core/devtools/discover`): How often a probe that walks the whole scene walks it again; in between it answers its last reading.
47
+ - generator-grown scatter species bake the generator's coarser levels and switch to them by size on screen through visibility.meshLods (Fernwild 28M to 12.5M triangles a frame)
48
+ - `RoadPiece` (`@jgengine/core/roads/RoadRibbons.draw`): One road's share of a merged draw: its trimmed centreline sub-paths and the base height they drape at.
49
+ - Grass reference boards: `bun run shoot --board` shoots six credited photos beside our matching views, our games' stylised art beside our grass, the 18 grass game views and every species, with `--diff` to compare two boards and `--resume`/`--timeout` to finish a long board
50
+ - `--look-from` takes a fourth number, the ground clearance, so a capture can sit centimetres above the turf
51
+ - 128 named grass species (GRASS_SPECIES in @jgengine/core/grass/grassSpecies) across eleven habitats, from putting greens and prairie to reed beds, cereal crops, ornamentals, moss cushions and stylised fantasy grass. A grass_field or grass() names one with species and any knob it also sets wins.
52
+ - grass knobs for blade twist, tip droop, V-fold, tuft splay, moss cushion domes, variegated margins and cross bands, spike seed heads (headShape) and a resting wind lean.
53
+ - a param schema can name a presetKey; the meta id there backs every unset field (presetBackedMeta), and the editor inspector groups presets under headings and shows the current one.
54
+ - `presetBackedMeta` (`@jgengine/core/generator/paramSchema`): A `meta` bag with its {@link ParamSchema.presetKey} preset filled in under it: the preset's values, then the meta's own keys on top.
55
+ - `GrassSpeciesKnobs` (`@jgengine/core/grass/grassKind`): A knob set a species writes: any knobs, never the seed.
56
+ - `GRASS_SPECIES` (`@jgengine/core/grass/grassSpecies`): Every shipped grass species, grouped by habitat. Also `GrassSpecies`, `GrassSpeciesId`.
57
+ - Grass style: a species says what grows, a style says how it is drawn. `realism` (0 cartoon, 0.5 animated, 1 photoreal) moves every style slider along its own curve; the first slider is `normalLift`. A world sets one style (`environment.grassStyle`, the Lighting panel, or `environment({ grassStyle })`) and a patch may override it (`style` in a `grass_field` meta or a `grass()` config)
58
+ - The inspector shows a scene kind's nested settings with the values in effect and a Clear button; grass and ground cover use it for the patch style
59
+ - Grass look fingerprints: a test pins every look the repo draws (128 species, every game volume and band, demo and reference looks) to the values from before the split
60
+ - `MENU_PANE_STORE_KEY` (`@jgengine/core/game/gameFlow`): ⚠ undocumented Also `MenuPane`, `menuPane`, `showMenuPane`.
61
+ - swimming can dive: movement.swim.dive makes crouch swim down to the bed and jump swim up, with telemetry diveDepth
62
+ - Fernwild breath: oxygen drains while the head is under water, refills at the surface, and drowning takes health (survival.oxygen-swim)
63
+ - `SwimDiveConfig` (`@jgengine/core/movement/playerMovement`): Metres a second a diver swims down, swims up, and drifts up with no input.
64
+ - bun run perf fps-gap measures every game at the frame gate's conditions and prints each against 360 fps (2.78 ms) with its costs grouped into ranked engine work items; a report, not a gate
65
+ - a desktop build runs without the display's vsync with <Game>.exe --uncapped on Windows (WebView2); without the flag it keeps vsync
66
+ - bun run perf desktop --launch <app.exe> [--uncapped] measures a production desktop build in the game's scene at the gate's conditions
67
+ - the capture_run agent-bridge verb runs a game's capture.play or a capture state in a shipped build, skipping dev-only helpers on request
68
+ - `CAPTURE_COMMAND_INPUT` (`@jgengine/core/devtools/agentBridge.draw`): The input a capture command named by a bare string runs with: a neutral look, so a command that reads an aim or heading gets one.
69
+ - `bun run perf crowd` times one crowd tick (neighbours within 1.5 m, separation, speed clamp, move, bounds; 0.5 agents/m²) written against createEntityStore + createSpatialIndex the way game code is, beside the same step over Float32Array columns and a counting-sort grid. Engine and flat ticks alternate so machine load hits both alike.
70
+ - useGameStoreLive re-renders a HUD only when what it draws changes and writes per-frame values through refs
71
+ - `MinimapChromeHandle` (`@jgengine/core/map/map.view`): Moves an already-drawn `MinimapChrome`'s markers in place.
72
+ - `useGameStoreLive` (`@jgengine/react`): Select from the game store at two speeds. Also `MinimapChromeHandle`.
73
+ - `useGameStoreLive` (`@jgengine/react/hooks`): Select from the game store at two speeds.
74
+ - bun run perf server, a repeatable load benchmark for the multiplayer host: N players in one room on the real createGameServerHost and createHostRouter, each subscribed to presence and posing every 100 ms of a fake clock that also drives the router's timers; one JSON line per room size (wall and CPU ms per game second, MB and messages out per game second, average message bytes), and a stderr note when a size cannot keep up in real time
75
+ - a run measures up to 100 steps and stops early after 5 s of measuring once it has 5, sizes stop at 256 players, and --max-seconds (default 60, warmup included) is the hard cap on one run; documented in README, the verify drive reference and the multiplayer skill; not a CI gate
76
+ - `store.columns`, the entity store's fast path for engine systems: resolve an id to an integer handle once (`columns.handleOf`), read pose from Float64Array position, velocity and rotation columns, and write by handle (`columns.setPosition`, `columns.setVelocity`) with the same constraint, derived velocity and notifications as `setPose`/`setVelocity`. Every write, by id or by handle, keeps the columns equal to the entity objects; despawned handles are reused with a bumped generation. Games keep using ids and never need it.
77
+ - `createPointGrid` (`@jgengine/core/spatial/pointGrid`), a per-tick counting-sort XZ neighbour grid over a typed-array position column that allocates nothing once warm and returns candidates in a fixed z-row, x, input order.
78
+ - `EntityColumns` (`@jgengine/core/scene/entityStore`): The store's fast path for engine systems that touch many entities per tick (crowds, flocks, pose interpolation, a server kernel): resolve an id to an integer handle once, then read pose straight from typed-array columns and write it by handle.
79
+ - `createPointGrid` (`@jgengine/core/spatial/pointGrid`): Build a {@link PointGrid}. Also `PointGrid`, `PointGridOptions`.
80
+ - `bindingOverrides` (`@jgengine/core/game/controlGate`): The player's rebinds in this game context: action to the codes that now drive it. Also `rebindAction`, `reboundActionCodes`, `setBindingOverrides`.
81
+ - `saveBindingOverrides` (`@jgengine/core/gameplay`): Store a game's whole rebind map (an empty one clears it), as the shell does whenever the game's rebinds change.
82
+ - `saveBindingOverrides` (`@jgengine/core/input/bindingOverrides`): Store a game's whole rebind map (an empty one clears it), as the shell does whenever the game's rebinds change.
83
+ - `solidEnclosure` (`@jgengine/core/world/overhead`): How shut in a point is: the share of the ways round it that meet a wall within reach, and 0 under open sky. Also `EnclosureOptions`.
84
+ - jgengine desktop --steam lays out a SteamPipe build beside the app (steam/content, steam/scripts with app_build_<appid>.vdf and depot_build_<depotid>.vdf, steam/output); the app id, depot id and live branch come from package.json "jgengine": { "steam": { ... } } or --steam-app, --steam-depot and --steam-branch, and a game without an app id still builds its content and is told the id is missing
85
+ - jgengine desktop --steamworks links the Steamworks API and a transparent surface Steam's overlay can draw into, ships steam_api64.dll in the depot, and forwards Shift+Tab from the page to the overlay
86
+ - jgengine desktop --no-installer builds only the game executable
87
+ - a desktop build's Frame rate limit setting offers Match display (V-Sync) and Uncapped; the choice is saved per player and applies on the next launch, and --uncapped still forces uncapped
88
+ - a desktop build's graphics settings gain Display mode (Windowed or Fullscreen): native borderless fullscreen that survives alt-tab, applied at once and remembered for the next launch
89
+ - `readDesktopShell`, `invokeDesktop`, `saveDesktopDisplayMode`, `saveDesktopFrameRate`, `desktopPresentMode`, `syncDesktopSettings`, `DISPLAY_MODE_OPTIONS` (`@jgengine/core/settings/desktopShell`): a desktop build's launch state and its commands; the settings store follows what the shell launched with
90
+ - `FRAME_RATE_LIMIT_UNCAPPED`, `frameRateLimitOptions` (`@jgengine/core/settings/frameRateLimit`): the frame-rate choices, with the desktop-only Uncapped option
91
+ - `SETTING_IDS.graphicsDisplayMode` (`@jgengine/core/settings/settingsModel`)
92
+ - bun run perf desktop --all [--uncapped] builds every game's production desktop app, measures each in turn and prints the 360 fps table; a report, not a gate
93
+ - every tame has a food meter; it eats what its diet takes from its own inventory and starves to death without (creatures.feed-starve)
94
+ - eatWhileRoom and DoseSupply in stats/doseTable, the shared 'eat from what you hold while there is room' step
95
+ - `eatWhileRoom` (`@jgengine/core/stats/doseTable`): Eat from `supply` while there is room: each time, the receiver's most preferred source it holds whose gain to `vital` fits under the max is taken and its doses go into `body`. Also `DoseSupply`.
96
+ - `CompanionStance` (`@jgengine/core/scene/companion`): What a tamed companion fights, wherever its {@link CompanionCommand} keeps it: - `passive` — never engages, even when struck (a pacifist escort).
97
+ - the creature panel says when a downed creature will wake
98
+ - `SaveStore.switchSlot(slot, { flush: false })` opens a slot without writing the current state first.
99
+ - Beacon's HUD says when a defense is not being saved, and its pause menu offers Withdraw without saving for it.
100
+ - The `reload-mid-run` Beacon flow raises a keep from the title, builds, starts wave 1, reloads the page and continues into the same defense.
101
+ - `HeadlessKeys` (`@jgengine/core/play/headlessKeys.draw`): Keys held against a headless {@link PlaySession}, mapped through the game's own bindings.
102
+ - `ContextMap` (`@jgengine/core/runtime/contextIdentity`): A `WeakMap` keyed by context identity that treats a wrapper of a context as the context itself ({@link aliasContext}). Also `contextOf`.
103
+ - `auditSuiteContent` (`@jgengine/core/suite/contentAudit`): The generic content check behind the suite's data entries: every item has a source and a use, every source is usable, every placeable item places, every firing weapon has obtainable ammo, and unlocks run steadily to the progression cap. Also `SuiteContent`, `SuiteContentItem`, `SuiteContentUse`, `SuiteUnlock`, `placedFact`, `recipeContent`.
104
+ - `expandSuiteGenres` (`@jgengine/core/suite/gameSuite`): The genres a declaration reaches, its includes followed. Also `GameSuiteConfig`, `GameSuiteScore`, `SuiteDataResult`, `SuiteProofResult`, `SuiteReport`, `SuiteScoreRow`, `SuiteStatus`, `parseSuiteReport` and 9 more.
105
+ - `runGameSuite` (`@jgengine/core/suite/runSuiteProof.draw`): Run a game's whole suite: every proof, then the content check. Also `LoadedSuiteProof`, `SuiteProofRun`, `SuiteProofRunOptions`, `runSuiteProof`.
106
+ - `SetupClosedError` (`@jgengine/core/suite/setupWindow`): Thrown when code touches a {@link SetupWindow} view after the window closed. Also `SetupWindow`, `openSetupWindow`.
107
+ - `SUITE_ENTRIES` (`@jgengine/core/suite/suiteCatalog`): Every entry, universal first, then by genre. Also `SUITE_GENRES`, `SUITE_UNIVERSAL`, `SuiteDepth`, `SuiteEntry`, `SuiteGenre`, `SuiteProofBy`.
108
+ - `CHEAT_COMMAND_NAME` (`@jgengine/core/suite/suiteProof`): Command names a proof treats as cheats even when the game forgot `cheat: true`. Also `ProofPlay`, `ProofSetup`, `SuiteProof`, `SuiteProofSpec`, `defineProof`.
109
+ - `STAND_ENVELOPE` (`@jgengine/core/plants/species/speciesReference`): How far a stand-grown tree may sit from its tables and still pass, at 20, 50 and 100 years in {@link REFERENCE_STAND}: height within 15% of its Carmean or FIA curve, DBH within 25% of its FIA curve, crown width inside its Tallo band.
110
+ - `FRAXINUS_NIGRA` (`@jgengine/core/plants/species/data/fraxinusNigra`): Black ash.
111
+ - `PRUNUS_SEROTINA` (`@jgengine/core/plants/species/data/prunusSerotina`): Black cherry.
112
+ - `ROBINIA_PSEUDOACACIA` (`@jgengine/core/plants/species/data/robiniaPseudoacacia`): Black locust.
113
+
114
+ ### Changed
115
+
116
+ - A ridden flyer lifts a smaller creature in its talons
117
+ - The creature-survival suite asks for depth the research says matters: favourite and crafted taming food, waking loses the tame, gather roles at twice the best tool, lighter loads, airlift, feeding and starving, behavior modes, flight over terrain, encumbrance, farming, refining, fuel, preserving, tool wear, boss unlocks
118
+ - Fernwild: the slingshot throws stones and a new bow fires stone and tranquilizer arrows, so a charging raptor is knocked out from 25 m on two arrows; Fernwild scores 20/67
119
+ - Fernwild proof: the beach and the snow fill with their own animals
120
+ - The grass board notices every code edit
121
+ - Boat out of starts no longer runs the Linux suite on this PC: a start window that frees within the hour is waited for, one that frees later stops verify:fast with exit 75 and the time it frees, and lane:close lands nothing and says to close again then
122
+ - Boat runs hold a shared fairshare lease so every repo on the PC keeps to two boat machines; machines are named to fairshare so ones left by killed runs still count. Root cause: check-then-start let three machines run at once on 2026-11-07, since a machine is missing from boat list while it is created.
123
+ - Bolas, and every weapon hit is a dose list
124
+ - Summon the Broodmother, beat her and make brood greaves round her trophy
125
+ - A bred creature's screen shows which parent each stat came from and which a mutation raised
126
+ - Fernwild proof: the Broodmother calls her brood when wounded past two thirds
127
+ - Browser parity tests wait for their fixture before calling it
128
+ - Fernwild proof: fists dent a thatch wall but not a wooden pillar
129
+ - Cave air: swamp caves choke and sicken, lava caves burn hot, snow caves freeze
130
+ - Hostile caves: swamp gas burns breath and health, the lava cave is searing, the snow cave freezing
131
+ - Taming is built from general parts now: a body's meters (vitals, moved to @jgengine/core/stats/vitals) take doses that land now or over seconds up to a cap, and a dose table says what each source gives each kind of receiver, in preference order. The same two pieces describe a dino's foods, a sim's meals and shield potions that fill only to 50
132
+ - Fernwild's game-local subdual engine is deleted: torpor, belly, affinity and ineffectiveness are vitals, the diet is a dose table, knockout and the tame are flags. New proofs show waking loses the tame and a clean tame gives more levels; Fernwild scores 24/67
133
+ - Migration: import createVitals and its types from @jgengine/core/stats/vitals (was survival/vitals); VitalsState gains doses, and tick() also reports flags a change raised between ticks
134
+ - Fernwild proof: beach animals roll low levels and the far snow's roll high
135
+ - Fernwild proof: carrying slows the survivor and past the limit stops them
136
+ - Validation: the complete nine-game 1600x900 high moving-player gate measured zero shader links and framebuffers during play. Eight games failed the unchanged 16.7 ms frame limit while Wardogs used 26-31% GPU and whole-machine CPU reached 97%. The empty exception file and handling remain pending a clean all-game pass. An earlier quiet matched pair measured Harbor Heat 19.2 to 98.2 fps (102.5 to 12.5 ms p95) and Drift Foundry 62.8 to 113.4 fps (21.7 to 11.1 ms p95). Baseline Ember Command failed readiness twice and is not a matched comparison.
137
+ - ![Drift Foundry](https://raw.githubusercontent.com/Noisemaker111/jgengine/pr-shots/pr-shots/agent/every-game-60fps-13/sol-drift-after.png)
138
+ - ![Beacon Bastion](https://raw.githubusercontent.com/Noisemaker111/jgengine/pr-shots/pr-shots/agent/every-game-60fps-13/beacon-bastion-sol-beacon-loaded.png)
139
+ - ![Fernwild](https://raw.githubusercontent.com/Noisemaker111/jgengine/pr-shots/pr-shots/agent/every-game-60fps-13/fernwild-sol-fern-final.png)
140
+ - [Drift corridor movement proof](https://raw.githubusercontent.com/Noisemaker111/jgengine/pr-shots/pr-shots/agent/every-game-60fps-13/drift-foundry-sol-corridor.mp4): all arrival and movement checks passed; representative clip frames were inspected.
141
+ - Fernwild: a Small Crop Plot grows mejoberries from a seed, with water and fertilizer
142
+ - Following the Survivor's path through the beach uses every core feature first
143
+ - Fernwild proof: a tame follows when whistled and stays when told
144
+ - `bun run shoot --board --views` shoots six game views, one per grass game, the way a player sees them: the strategy games from their start camera, the others with the player placed in a grass patch (`--mode play --spawn`). They replace 18 close-up views from a detached camera 1 to 4 m off the ground, an angle the top-down games never show, three of which showed no grass
145
+ - beacon-bastion, brightway-park, ember-command, fernwild, harbor-heat and scrap-signal grass fields now name a species picked for their setting, and the grass-looks demo lays out every species.
146
+ - the engine bundle budget rises to 1880 KiB for the grass species table every grass field reads.
147
+ - A scene that sets no style draws exactly as before: every curve passes through today's look at realism 0.5
148
+ - Fernwild: a tame hauls what the survivor cannot, and an overloaded survivor can turn to it
149
+ - Fernwild: care raises a baby imprint, and the aim picks a baby standing in its parent
150
+ - A lane's landing wait runs in the harness background: lane:close, the Stop hook, the Monitor ban and AGENTS.md stop teaching a foreground background await --timeout 540 in slices, and a background await marks itself as the task's awaiter so the Stop hook lets a correctly waiting turn end
151
+ - Late gear from level 62 to the cap gives every Fernwild item a use
152
+ - The title and pause menus show their Controls, Settings and Credits panes through the menu.pane command
153
+ - Native Rust experiment for 360 fps: three.js frame export, wgpu replay with a pixel diff against three, Rune HUD, Deepward's tick in embedded V8 and in Rust, a wasm/WebGPU build, and bun run perf native for the four-way table. Verdict in native/README.md and #3041: stay on TS/three.js; the speedup Harbor Heat shows comes from WebGPU, in the browser too.
154
+ - Fernwild proof: a picked-clean berry bush grows back
155
+ - Diving and breath: crouch swims down, breath drains under water, drowning hurts
156
+ - Passive taming: an Ovis, Equus or Castoroides tames awake, hand-fed while hungry
157
+ - Cells live in an open-addressing table with their coordinates inline and their members in one pooled Int32Array; a query skips ids it already returned with a typed-array stamp instead of a Map. Cells keep creation order and each cell keeps its ids' order, so snapshot/restore still replay query results exactly. Public API unchanged.
158
+ - Measured in one process, old and new interleaved tick by tick (Bun 1.4.4, shared 24-core PC). Index alone, one querySphere + one update per id: 1k 9.8 → 1.4 ms (7.1x), 10k 207 → 24 ms (8.6x), 100k 4,199 → 881 ms (4.8x). Whole crowd tick through today's store: 1k 13.5 → 4.7 ms (2.9x), 10k 403 → 186 ms (2.2x), 100k 8,477 → 4,442 ms (1.9x); flat-array reference 0.19 / 2.0 / 38 ms.
159
+ - harbor-heat is unchanged, and was expected to be: the spatial index was ~1% of its frame and rendering dominates. Paired runs in both orders: old index 20.3 / 25.3 ms average frame, new index 26.6 ms (and one 53.7 ms run under machine load whose engine JS was not slower); the index's own profile share fell from 0.8-0.85% to 0.17-0.5%. This layout is the foundation for large crowds, big multiplayer worlds and the Rust multi-core kernel in #2999; the remaining crowd gap is string-keyed store lookups, addressed by the store fast path in a follow-up PR.
160
+ - Verified on a boat.dev Linux machine from a clean clone of head f602128fd: preflight (install, gen, type-checks, gates) and verify:fast (1001 test files, 31 type-checks, 6 gates, harbor-heat boot) passed.
161
+ - Regenerated the jgengine-world api.md for the spatial index's updated docs
162
+ - 128 players in one room, every player posing every 100 ms: 15 ms of host time per game second instead of 12 to 30 s on JSON (8.6 ms instead of 30 s on binary), and 25 MB/s out instead of 3.2 GB/s. The wire format is unchanged; #2990 covers binary delta rosters and interest-limited rows
163
+ - Measured: `bun run perf crowd` gains a by-handle row (store.columns plus the point grid), the three sides interleaved on the shared 24-core PC, median ms per tick: 1k by id 1.37, by handle 0.18, flat 0.12 (1.5x of flat); 10k by id 33.4, by handle 2.40, flat 1.41 (1.7x); 100k by id 1,629, by handle 41.3, flat 22.9 (1.8x). Under heavier machine load earlier the 100k ratio was 2.2x. The 3x target is met at both sizes on the handle path; the string-id path stays bounded by string-keyed lookups. This column layout is the one the Rust multi-core kernel in #2999 would read.
164
+ - harbor-heat is unchanged; this PR is the foundation for big crowds and multiplayer worlds, not a frame-time change for a game with tens of entities. Six alternating runs (branch, base, branch, base, then profiled) swung 17-28 ms average frame on both sides with other heavy jobs on the PC, and every system moved together, including GPU time and render passes this PR does not touch. Inside harbor-heat's own CPU profiles the changed files took 92 ms on the branch against 117 and 141 ms on base: pose history 14 ms against 28-34, the keyed store 3 against 9-10, scene spatial 11 against 21-25.
165
+ - Fernwild: a Preserving Bin keeps food four times as long
166
+ - Tests that read this PC's models pass in a fresh worktree
167
+ - A key moved in Settings takes effect in play: the game itself keeps the player's rebinds
168
+ - A refining forge smelts metal ore into ingots, and metal tools break stone
169
+ - Fernwild proof: a cleared beach herd comes back after the refill
170
+ - Flying: ride a Pteranodon or Argentavis, climb, cross and land; stamina out drops it
171
+ - A ridden Sarco swims through the sea at its own speed
172
+ - A save made in play survives quitting: Continue on the title brings it back
173
+ - Rebase conflicts inside a game's suite score resolve themselves
174
+ - Stop the served-game port test failing once in 483 runs
175
+ - A closed hut holds off the cold, and a survivor can walk up into one
176
+ - Fernwild: a hide sleeping bag places, and a dead survivor wakes on it once
177
+ - Fuelled stations: a campfire or pot burns wood, thatch or sparkpowder and cooks and warms only while lit
178
+ - Fernwild proof: sparkpowder crafts only beside a placed mortar
179
+ - Fernwild: the Storage Box places and keeps fifteen stacks
180
+ - a baby's hunger is the same meter at a faster pace; the breeding card no longer carries food
181
+ - Tames take a stance apart from follow or stay: passive, neutral or aggressive
182
+ - Tames at work: specialists gather their resource with a ridden bite and carry it lighter
183
+ - Tames gather what they're good at and carry it lighter
184
+ - Fernwild: kibble. Wild egg-layers drop eggs sized to their bodies, a cooking pot cooks five tiers of kibble from them, and each species tames about five times faster and cleaner on its preferred tier; proven side by side against raw meat and prime meat, Fernwild scores 22/67
185
+ - Fernwild proof: the snow at night takes health, and a lit campfire slows it
186
+ - Fernwild: tools wear with each landed swing or shot, break at zero, and repair with materials
187
+ - lane:close and verify:fast run every Linux-capable step on one boat.dev machine; game boots, model-reading and Windows-only tests stay on this PC
188
+ - The suite score generator writes into hand-edited READMEs and plans, so it names no outputs and reads only each game's suite files
189
+ - runStages test knows game-suite writes into hand-edited READMEs and PLANs
190
+ - jgengine suite scores the run it just made, so a regression reads red instead of the committed green report
191
+ - Game READMEs and plans lead with the suite score
192
+ - Brightway Park: headless suite proofs and the content check; Credits is not simulatable (#2967)
193
+ - Per-context state resolves a wrapped context to the real one, so a proof's setup seeds the game's own model
194
+ - perContext and the new ContextMap key by contextOf(ctx); the suite runner aliases its setup view to the
195
+ - session's context. A game helper handed the setup view used to build a second model for the wrapper.
196
+ - Scrap Signal: headless suite proofs; dev capture commands are marked cheat
197
+ - Fernwild: 18 headless suite proofs and the content check
198
+ - Games declare their suite genres; jgengine suite prints the score and gen writes it to README and PLAN
199
+ - Suite proofs: game code may keep the setup context, a command setup defines is a cheat, and the score renders by area
200
+ - The setup window lets the game's own ticks, commands and probe through after it closes, so a store
201
+ - that registered through the setup context keeps working; proof code that kept it is still refused.
202
+ - Game suite: a catalog of what players must be able to do, headless proofs that refuse cheats, and a content check
203
+ - Landing always runs beta's current landing code: lane:close fetches beta, unpacks its scripts/ once per version into .git/jg-landing/, and runs lane-close, land, the queue and the batch from there against the lane, so a landing fix reaches lanes made before it
204
+ - Breaking: the exports FRAXINUS_NIGRA, PRUNUS_SEROTINA and ROBINIA_PSEUDOACACIA and their plants/species/data subpaths are removed, because those species cannot yet grow a decurrent crown inside the forestry envelope; use another species from PLANT_LIBRARY
205
+
206
+ ### Fixed
207
+
208
+ - `bun run shoot --board` no longer runs for hours under one 90-minute guard that killed full boards part way (292 captures at 40 s to 2 min each). One run shoots for `--budget` seconds (default 6 min), records each shot in the board folder's `board.json` as it lands and exits 3 while shots remain; the same command carries on, and the run that takes the last shot lays out the sheets. A killed run loses only the capture in flight, a failed capture is named and keeps the board unfinished (no sheets with gaps) until it lands, and shots taken with other code are retaken (`--resume` keeps them)
209
+ - A board run killed by its guard names the shot that hung instead of "hung before it reported a step"
210
+ - a lane's game suite reports come back from the boat machine and their scores are written before the commit, so new proofs count in each game's score
211
+ - grass and terrain clearing math stops calling Math.hypot per zone and per vertex, so streaming grass in ember-command allocates about a third less
212
+ - Scattered plants and rocks draw through the shared static batches, so every chunk, variant and detail level with the same material is one draw per area instead of one draw each (Fernwild: 738 draws down to about 370, frame p95 24.8 ms to 15.6 ms).
213
+ - Unique near plants and rocks (scatter heroes) join the same shared batches instead of drawing every part of every hero on its own in the main pass and each shadow cascade.
214
+ - World-projected (triplanar) materials now follow each placement's own rotation, so instanced and batched rocks blend their texture by their real facing, and they can share batched draws.
215
+ - A shared batch picks up textures its source material swaps in after the batch was made, such as generated rock surfaces whose bake lands after the first rock is placed.
216
+ - Clicking a building piece or other batched scenery that names no object no longer reports an empty object id.
217
+ - Harbor Heat no longer relinks its car shaders every few seconds or stalls when traffic comes and goes; its p95 frame went from 22.6 ms to 18.2 ms.
218
+ - the landing queue retries a failed GitHub read instead of giving up, and says what GitHub answered.
219
+ - landing retries a GitHub server error on a read or a push instead of giving up, and says what GitHub answered.
220
+ - landing retries a GitHub server error on a read, a push or a merge instead of giving up, and says what GitHub answered.
221
+ - flocks of birds draw as one instanced mesh per species instead of one skinned draw per bird
222
+ - placed props cost nothing per frame while nothing moves and the view shows the same set
223
+ - Harbor Heat holds 60 fps on the reference GPU (p95 16.2 ms, was 22.6 ms) and leaves the known-failing frame list
224
+ - shared scenery draws and impostor bakes survive temporary membership changes without rebuilding shaders in play.
225
+ - static props, buildings, character uploads, particles and the Drift Foundry HUD avoid repeated work while keeping their authored materials and detail.
226
+ - shadow and particle programs warm before play, and scatter loading holds readiness until the complete models mount.
227
+ - provisioning includes named asset-pack imports, and every game is measured moving at high quality with automatic quality changes disabled.
228
+ - the shader-ready check after load rides on the renderer's own draws instead of walking the whole visible scene before every render, so its cost follows what the camera draws, not the size of the world (Fernwild: 285 ms to 54 ms per 6 s)
229
+ - dev probes that walk the scene (triangles, shadow casters, static batch draws) walk it at most every 2 s; every slow frame and every telemetry report used to walk it again, which made slow games slower in dev and in perf runs
230
+ - the HUD overflow check measures panels at most twice a second instead of on every style change, which forced a page layout each frame in a HUD that moves bars or markers
231
+ - perf and capture wait for a slow load to finish instead of giving up at 90 s while the shader warmup still had 30 s left (Ember Command on a GPU short of memory); a perf batch's deadline grows with its games
232
+ - ambient fish, insects and motes reuse the ground height they last read until they move half a metre or near the edge of their height band, instead of every agent reading the sculpted ground every frame; they land exactly where they did on ground up to about 65°, and read it 5 to 15 times less often (Fernwild spent about 2.5 ms a frame there)
233
+ - small scatter species (shrubs, ferns, rocks) pick a detail level per plant by its own distance instead of per 200 m chunk, and stop drawing past their draw distance one plant at a time
234
+ - a scatter field keeps a mesh in the scene only while it has plants to draw; Fernwild kept about 11,900 empty meshes that three walked on every render (scene 13,315 to 4,817 nodes)
235
+ - the small-shadow cull stops watching a node once it leaves the scene, so meshes added and dropped as the camera moves are not held for the whole session
236
+ - verify:fast type-checks every package, app and game whose program imports a changed engine file, as CI's types job does; it checked only the games, so an engine change the shell's stricter settings reject passed locally and failed in CI
237
+ - the frame gate (bun run perf gate), CI's perf matrix and perf network --all measure every game under games/ with a web entry, read from disk, instead of a hand-kept list of 8 that left Fernwild unjudged; a new game is measured the day it lands
238
+ - the capture test that checks --reuse-storage refuses without a warm browser allows 30 s per start instead of 5; the script loads much of the engine first, which ran past 5 s on a cold CI runner and failed an unrelated PR
239
+ - props with a see-through part (street-lamp glass, foliage cards) now batch like other scenery instead of drawing one by one; each batch draws its copies farthest first, so blending looks as before. Harbor Heat drew its 28 street lamps and 28 bushes one by one; its scene draws drop from 440 to 336 a frame.
240
+ - Roads that look alike draw as one draw per layer (asphalt, gutter, lane paint, sidewalk, kerb), and every road junction of one colour is one draw: a street grid no longer costs five draws per street.
241
+ - Crowd characters that differ only in paint colour (traffic resprayed four ways) share one instanced draw per material, each drawing its own colour.
242
+ - Shared static-prop batches are culled whole by their cell instead of re-culling, re-sorting and re-uploading every instance in every render pass and shadow cascade.
243
+ - Foliage sway ramps over the model's own measured height, so one plant placed at several sizes sways alike and shares one batched draw.
244
+ - Auto exposure reads its light meter back at the start of a frame instead of after the scene, so it no longer stalls the frame for 7-118 ms while the browser finishes drawing.
245
+ - When the camera pans, at most one far shadow cascade redraws per frame (the longest waiting first) instead of all of them on the same frame.
246
+ - City ground layers with an edge fade draw in one pass instead of re-flagging their material twice every frame.
247
+ - Ember Command's HUD no longer re-renders every frame for a mission clock that changes once a second, and its minimap fog is built once per fog change as one rectangle per run of cells.
248
+ - The perf breakdown's own per-draw bookkeeping no longer allocates, so the garbage it reports is the game's.
249
+ - Characters out of view are no longer animated: they draw nothing and cast no shadow, so their skeletons are not posed until they come back into view (Scrap Signal: 3,508 bone writes a frame down to 32).
250
+ - Building kit pieces join the shared static batches, so walls and roofs of every building that share a material draw together per area instead of one draw per building chunk.
251
+ - Props with texture-map overrides (panel and scrap-metal crates, rocks) now batch like any other scenery instead of drawing one at a time; Scrap Signal's frame went from 25.7 ms to 16.5 ms at p95.
252
+ - grass no longer reads as a spotted patch. Tuft roots are laid on an even low-discrepancy pattern instead of random scatter, which left piles and bare gaps, and they stay even when distance thins them.
253
+ - each tuft's blades now spread far enough to meet the next tuft (at least three quarters of the tuft spacing), so a field is one sward instead of fountain islands on the turf mat.
254
+ - grass blades seen from behind are no longer near black. three.js flipped the whole normal on back faces and pointed the blade's sky lift into the ground. Both sides of a blade now light from above, and the lower blade sits in an even shade so a dense field still reads deep.
255
+ - clumping only pulls tufts into clumps when a look asks for strong clumping (or cushions), so ordinary lawns and meadows stop forming bald patches.
256
+ - the stray-browser sweep test no longer opens a Chrome window that takes the foreground; its planted browser now runs headless
257
+ - the home and /why pages' 2D canvas demos (car, keys, saves and the rest) stop drawing while offscreen or in a background tab, like the three.js demos already did
258
+ - traced code panels stop re-reading their lit lines every 90 ms while offscreen or in a background tab
259
+ - a production build's frame breakdown names its post passes instead of their minified class names
260
+ - perf play, perf gate and jgengine desktop link the game's asset packs first, so a fresh checkout no longer measures or ships placeholder boxes
261
+ - jgengine desktop --diagnostics passes --sourcemap through a game's own build script, and names the app after defineGame's own name
262
+ - createSpatialIndex keys cells by number and ids by integer handle, so moving ids and querying allocate nothing.
263
+ - Harbor Heat's chart stops re-rendering every frame; it pans, turns and places its markers through MinimapChrome's new place handle
264
+ - static prop batches with the same material share one material copy across cells, so the main pass stops re-uploading the same uniforms at each cell's batch
265
+ - a ws room sends at most one presence roster per pose interval (poseRules.minIntervalMs, 100 ms by default) instead of one per accepted pose; the first move in a quiet room, a player's first pose and a leave still go out at once, and later moves and appearance changes ride the roster that closes the window
266
+ - presence, chat and voice broadcasts encode each message once per codec and send the same frame to every subscriber
267
+ - `stepFlock` finds flockmates through the point grid instead of a per-call Map of arrays. Results are bit-identical over 120 steps at 50 and 500 agents; a step at 1k agents went 1.13 to 1.00 ms and at 10k 18.5 to 13.9 ms (8 agents: 5.8 µs either way).
268
+ - pose history captures each step from the store's columns into one flat array by handle. `beginStep` at 10k entities went 1.10 to 0.11 ms and at 100k 27.2 to 0.58 ms; sampling every entity at 100k went 19.5 to 14.6 ms. A handle reused by a new entity, or a body respawned under the same id, now renders live until the next step instead of gliding from the old body's pose.
269
+ - pose writes by id are cheaper, with no API change. setPose, setVelocity, update and resetToSpawn find the entity through the handle table (one id lookup where store.get then store.set took three) and notify through a new `store.touch`; the behaviour-change check skips its lookup when nothing is pending; a move that stays inside its scene-spatial cell skips the index bookkeeping. setPose plus setVelocity on a bare store, columns mirrored, beta and branch interleaved in one process: 10k 106 to 80 ns, 100k 210 to 195 ns. Through the game context's wiring (store onChange to the scene spatial index, one batch per frame), setPose with dt, median of three runs: 10k 271 to 259 ns, 100k 667 to 574 ns. A position given as {x, y, z} and a dt-derived velocity are still new tuples, each with the reason in code. Entity objects carry no handle: a hidden property made spreading them 8x slower in Bun.
270
+ - the land queue rebases a PR the base moved into conflict instead of giving up
271
+ - the demo nudge ring and arrow pause while their demo is off screen or the tab is hidden, so a page scrolled past a demo stops restyling it every frame
272
+ - a knocked-out creature now provably stays out until its torpor, plus any narcotic still landing, wears off; the wake proof fails if it wakes any sooner
273
+ - editor capture waits for its viewport and finished loading
274
+ - editor and asset provisioning help reject invalid options before changing files
275
+ - Keep weathering finite under deep cover
276
+ - Sort river sediment using its concentration near the bed
277
+ - Include hillslope creep in outlet volume
278
+ - Attribute test failures to the right file
279
+ - grass streaming bounds its work per frame and resumes it on the next, growing the nearest leaf first so the first frame still shows grass
280
+ - grass rebuilds when the terrain under it changes
281
+ - every grass patch on a canvas shares one 3 ms frame budget, the 8 ms fill budget ends once the first view has filled, and the first fill counts as loading so play never starts on half-grown grass
282
+ - captures wait out slow model loads for the whole settle budget instead of failing after one 15 s wait
283
+ - bun run shoot --load runs headless by default, so no jgengine capture opens a Chrome window unless --headed is passed; headless Chrome on Windows still renders on the real GPU.
284
+ - A Beacon defense saves itself once a second while playing, so a page reload mid-run continues into the same towers, purse, wave and raiders (about 0.2 ms and 12 KB per write on a busy defense).
285
+ - Opening or raising another Beacon keep never overwrites the keep left behind, and a keep changed in another window is left alone instead of overwritten.
286
+ - A HUD gated by `showDuring` shows its panels when play starts from a title menu.
287
+ - A failed capture is retried from its evidence instead of stopping after two attempts.
288
+ - black ash, black cherry and black locust are back in PLANT_LIBRARY, and FRAXINUS_NIGRA, PRUNUS_SEROTINA and ROBINIA_PSEUDOACACIA with their plants/species/data subpaths are exported again (#2959 removed them); each meets the tree envelope: stand height within 15% and DBH within 25% of its tables and inside the Tallo band at 20, 50 and 100 years, and a decurrent crown on two of three open-grown seeds. Their refits had stalled in the basins they started in; plants fit now starts a new tree search from the shipped tree of the same crown form that misses least of the tree checks (or one named with --from), and a missed check costs the fit 100 instead of 10
289
+
290
+ ### Removed
291
+
292
+ - `@jgengine/core/survival/vitals` no longer exports `VitalDef`, `VitalFlag`, `VitalFlagChange`, `VitalLink`, `VitalRateModifier`, `Vitals`, `VitalsState`, `VitalsTickOptions` and 1 more.
293
+ - PERF_GATE_GAMES and perf-network's ALL_GAMES; FRAME_GAMES in scripts/ci-scope.ts is the one list
294
+ - `@jgengine/core/plants/species/data/fraxinusNigra` no longer exports `FRAXINUS_NIGRA`.
295
+ - `@jgengine/core/plants/species/data/prunusSerotina` no longer exports `PRUNUS_SEROTINA`.
296
+ - `@jgengine/core/plants/species/data/robiniaPseudoacacia` no longer exports `ROBINIA_PSEUDOACACIA`.
297
+
29
298
  ## 0.53.0
30
299
 
31
300
  ### Migrate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/convex",
3
- "version": "0.53.0",
3
+ "version": "0.54.0",
4
4
  "description": "Convex adapters for JGengine: game transport, presence transport, and backend wiring over @jgengine/core.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "check-types": "tsgo --noEmit -p tsconfig.json"
31
31
  },
32
32
  "dependencies": {
33
- "@jgengine/core": "^0.53.0"
33
+ "@jgengine/core": "^0.54.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "convex": "^1.31.2",
@@ -61,4 +61,4 @@ If nothing fits, name the shared owner first (AGENTS.md, "Reusable capability go
61
61
 
62
62
  ## Finish
63
63
 
64
- Keep scene coordinates in the editor document, state serializable, randomness injected. A game is not done while its entities and props are box markers: run the asset pass in `jgengine-assets`. Prove it with `jgengine-verify`.
64
+ Declare the game's genres in `jgengine.json` (`suite`) and prove each required player verb with a headless proof; `jgengine suite <game>` is the score ([games/AGENTS.md](../../../games/AGENTS.md#game-suite)). Keep scene coordinates in the editor document, state serializable, randomness injected. A game is not done while its entities and props are box markers: run the asset pass in `jgengine-assets`. Prove it with `jgengine-verify`.
@@ -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.53.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.54.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
10
10
 
11
11
  ## @jgengine/core/authoring
12
12
 
@@ -83,8 +83,8 @@
83
83
 
84
84
  - `CommandDecodeResult` (type): type CommandDecodeResult<TInput> = | { ok: true; value: TInput } | { ok: false; reason: string } — ⚠ undocumented
85
85
  - `CommandDecoder` (type): type CommandDecoder<TInput> = (input: unknown) => CommandDecodeResult<TInput> — Parses raw `unknown` transport input into `TInput`, rejecting anything that doesn't match the command's declared shape. Runs before `validate`/`apply`, so a malformed payload never reaches game logic.
86
- - `CommandDefinition` (interface): interface CommandDefinition<TState, TInput> { decode?: CommandDecoder<TInput>; validate?(state: TState, input: TInput): CommandRejection | null; apply(state: TState, input: TInput): TState | void } — ⚠ undocumented
87
- - `CommandRegistry` (interface): interface CommandRegistry<TState> { define<TInput>(name: string, definition: CommandDefinition<TState, TInput>): void; has(name: string): boolean; names(): string[]; run(state: TState, name: string, input: unknown): CommandResult<TState> } — ⚠ undocumented
86
+ - `CommandDefinition` (interface): interface CommandDefinition<TState, TInput> { decode?: CommandDecoder<TInput>; validate?(state: TState, input: TInput): CommandRejection | null; apply(state: TState, input: TInput): TState | void; cheat?: boolean } — ⚠ undocumented
87
+ - `CommandRegistry` (interface): interface CommandRegistry<TState> { define<TInput>(name: string, definition: CommandDefinition<TState, TInput>): void; has(name: string): boolean; names(): string[]; isCheat(name: string): boolean; run(state: TState, name: string, input: unknown): CommandResult<TState> } — ⚠ undocumented
88
88
  - `CommandRejection` (interface): interface CommandRejection — Why a command refused: `reason` is a stable machine code; `message`, when set, is shown to the player.
89
89
  - `CommandResult` (type): type CommandResult<TState> = | { status: "applied"; state: TState } | { status: "rejected"; reason: string; message?: string } | { status: "unknown-command" } — ⚠ undocumented
90
90
  - `createCommandRegistry` (function): function createCommandRegistry<TState>(): CommandRegistry<TState> — ⚠ undocumented
@@ -142,6 +142,7 @@
142
142
 
143
143
  - `AgentBridgeRequest` (type): type AgentBridgeRequest = { method: string } & Record<string, unknown> — One RPC call into the agent bridge: a verb name plus its verb-specific fields.
144
144
  - `AgentBridgeResponse` (type): type AgentBridgeResponse = | { ok: true; [key: string]: unknown } | { ok: false; error: string } — Result of an agent bridge call: `ok: true` with verb payload, or `ok: false` with a reason.
145
+ - `CAPTURE_COMMAND_INPUT` (const): const CAPTURE_COMMAND_INPUT: { readonly yaw: 0; readonly pitch: 0; readonly aim: { readonly yaw: 0; readonly pitch: 0; }; } — The input a capture command named by a bare string runs with: a neutral look, so a command that reads an aim or heading gets one. Shared by the dev runner's capture mode and `capture_run`.
145
146
 
146
147
  ## @jgengine/core/devtools/collisionDebug
147
148
 
@@ -256,6 +257,7 @@
256
257
 
257
258
  - `DiscoverModule` (interface): interface DiscoverModule — Discovery subsystem: the public discover and probes facades plus internal record map and helpers.
258
259
  - `DiscoveredRecord` (interface): interface DiscoveredRecord — Internal record for an auto-discovered field: identity, kind, initial value, source object, and get/set accessors.
260
+ - `SCENE_PROBE_REFRESH_MS` (const): const SCENE_PROBE_REFRESH_MS: 2000 — How often a probe that walks the whole scene walks it again; in between it answers its last reading.
259
261
  - `createDiscoverModule` (const): const createDiscoverModule: (deps: { signal: ChangeSignal; register: <T>(name: string, initial: T, options?: TunableOptions<T> | undefined) => Tunable<T>; controlRecords: Map<string, ControlRecord>; }) => DiscoverModule — Create the discovery subsystem that scans objects/tables for tunable fields and binds them as controls and probes.
260
262
 
261
263
  ## @jgengine/core/devtools/fallbackSeams
@@ -445,7 +447,7 @@
445
447
 
446
448
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
447
449
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
448
- - `VERSION` (const): const VERSION: "0.53.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
450
+ - `VERSION` (const): const VERSION: "0.54.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
449
451
 
450
452
  ## @jgengine/core/runtime/adapter
451
453
 
@@ -497,6 +499,11 @@
497
499
  - `ObjectSlotTransfer` (interface): interface ObjectSlotTransfer — Which player inventory a transfer moves between, and what it moves.
498
500
  - `SceneObjectSlots` (interface): interface SceneObjectSlots — Per-instance container contents for placed objects whose catalog entry declares `slotInventory` — the engine side of "install a GPU into a rack", "put an item in a chest". Contents live on `SceneObject.slots`, so they replicate and save with the object rather than needing a game-owned table keyed by `instanceId`, and every mutation is validated against the declared `accepts` and stack limits.
499
501
 
502
+ ## @jgengine/core/runtime/contextIdentity
503
+
504
+ - `ContextMap` (class): class ContextMap<T> — A `WeakMap` keyed by context identity that treats a wrapper of a context as the context itself ({@link aliasContext}). Use it for per-context state instead of `new WeakMap<GameContext, T>()`.
505
+ - `contextOf` (function): function contextOf(ctx: GameContext): GameContext — The context `ctx` stands for: itself, or the context a wrapper of it was aliased to.
506
+
500
507
  ## @jgengine/core/runtime/gameContext
501
508
 
502
509
  - `CatalogEntityRole` (type): type CatalogEntityRole = "player" | "enemy" | "hostile" | "npc" | "vehicle" — ⚠ undocumented
@@ -505,7 +512,7 @@
505
512
  - `GameAudio` (interface): interface GameAudio — Reachable audio seam on `ctx.game`: `play`, `music`, and `resume` route through the `audio.play`/`audio.music`/`audio.resume` events the shell's audio engine listens on, so game code triggers sound without importing the shell. Retained `loop`/`setLoop`/`stopLoop` add id-keyed loops with live pitch/gain control over `audio.loopStart`/`audio.loopSet`/`audio.loopStop` (#1051).
506
513
  - `GameContext` (interface): interface GameContext — The live engine handle a game's loop, systems, commands, and UI read and mutate — entities, objects, the reactive store, opted-in `game.*` subsystems, world queries, and the sim clock. One context = one running world.
507
514
  - `GameContextCards` (interface): interface GameContextCards { pile(id: string, config?: CardPileConfig): CardPile } — ⚠ undocumented
508
- - `GameContextCommands` (interface): interface GameContextCommands { define<TInput>(name: string, definition: CommandDefinition<GameContext, TInput>): void; has(name: string): boolean; names(): string[]; run(name: string, input: unknown): CommandResult<GameContext>; runAs(actorUserId: string, name: string,… — ⚠ undocumented
515
+ - `GameContextCommands` (interface): interface GameContextCommands { define<TInput>(name: string, definition: CommandDefinition<GameContext, TInput>): void; has(name: string): boolean; names(): string[]; isCheat(name: string): boolean; run(name: string, input: unknown): CommandResult<GameContext>; runAs(ac… — ⚠ undocumented
509
516
  - `GameContextContent` (interface): interface GameContextContent { defaultRarity?: string; itemById?(itemId: string): GameContextItemEntry | null | undefined; entityById?(catalogId: string): GameContextEntityEntry | null | undefined; objectById?(catalogId: string): GameContextObjectEntry | null | undefin… — ⚠ undocumented
510
517
  - `GameContextEconomy` (interface): interface GameContextEconomy { balance(userId: string, currencyId: string | CurrencyDefinition): number; grant(userId: string, currencyId: string | CurrencyDefinition, amount: number): void; charge(userId: string, currencyId: string | CurrencyDefinition, amount: number… — ⚠ undocumented
511
518
  - `GameContextEntityEntry` (interface): interface GameContextEntityEntry { stats?: StatCatalog; receive?: ReceiveMap; onDeath?: OnDeathSpec; movement?: PoseAllowedStates & {walkSpeed?: number}; role?: CatalogEntityRole; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; scale?: number } — ⚠ undocumented · used by `entityEntryFromCatalog` (@jgengine/core/editor): Turns an authored entity row into the runtime {@link GameContextEntityEntry} a spawn consumes: looks up `entityId` in the document's {@link …
@@ -536,7 +543,7 @@
536
543
  - `GameAudio` (interface): interface GameAudio — Reachable audio seam on `ctx.game`: `play`, `music`, and `resume` route through the `audio.play`/`audio.music`/`audio.resume` events the shell's audio engine listens on, so game code triggers sound without importing the shell. Retained `loop`/`setLoop`/`stopLoop` add id-keyed loops with live pitch/gain control over `audio.loopStart`/`audio.loopSet`/`audio.loopStop` (#1051).
537
544
  - `GameContext` (interface): interface GameContext — The live engine handle a game's loop, systems, commands, and UI read and mutate — entities, objects, the reactive store, opted-in `game.*` subsystems, world queries, and the sim clock. One context = one running world.
538
545
  - `GameContextCards` (interface): interface GameContextCards { pile(id: string, config?: CardPileConfig): CardPile } — ⚠ undocumented
539
- - `GameContextCommands` (interface): interface GameContextCommands { define<TInput>(name: string, definition: CommandDefinition<GameContext, TInput>): void; has(name: string): boolean; names(): string[]; run(name: string, input: unknown): CommandResult<GameContext>; runAs(actorUserId: string, name: string,… — ⚠ undocumented
546
+ - `GameContextCommands` (interface): interface GameContextCommands { define<TInput>(name: string, definition: CommandDefinition<GameContext, TInput>): void; has(name: string): boolean; names(): string[]; isCheat(name: string): boolean; run(name: string, input: unknown): CommandResult<GameContext>; runAs(ac… — ⚠ undocumented
540
547
  - `GameContextContent` (interface): interface GameContextContent { defaultRarity?: string; itemById?(itemId: string): GameContextItemEntry | null | undefined; entityById?(catalogId: string): GameContextEntityEntry | null | undefined; objectById?(catalogId: string): GameContextObjectEntry | null | undefin… — ⚠ undocumented
541
548
  - `GameContextEconomy` (interface): interface GameContextEconomy { balance(userId: string, currencyId: string | CurrencyDefinition): number; grant(userId: string, currencyId: string | CurrencyDefinition, amount: number): void; charge(userId: string, currencyId: string | CurrencyDefinition, amount: number… — ⚠ undocumented
542
549
  - `GameContextEntityEntry` (interface): interface GameContextEntityEntry { stats?: StatCatalog; receive?: ReceiveMap; onDeath?: OnDeathSpec; movement?: PoseAllowedStates & {walkSpeed?: number}; role?: CatalogEntityRole; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; scale?: number } — ⚠ undocumented · used by `entityEntryFromCatalog` (@jgengine/core/editor): Turns an authored entity row into the runtime {@link GameContextEntityEntry} a spawn consumes: looks up `entityId` in the document's {@link …
@@ -699,7 +706,7 @@
699
706
  - `PoseHistory` (interface): interface PoseHistory — Previous-step pose buffer: capture at each fixed step, sample between steps for rendering.
700
707
  - `PoseHistoryState` (interface): interface PoseHistoryState — Serializable previous-step poses keyed by entity id.
701
708
  - `RenderPose` (type): type RenderPose = [x: number, y: number, z: number, rotationY: number] — A render pose sampled between two simulation steps: `[x, y, z, rotationY]`.
702
- - `createPoseHistory` (function): function createPoseHistory(options?: { snapDistance?: number }): PoseHistory — Previous-step pose buffer for render interpolation under a fixed-step loop. Reuses one 4-slot array per entity so a step over N entities allocates only for entities seen for the first time.
709
+ - `createPoseHistory` (function): function createPoseHistory(options?: { snapDistance?: number }): PoseHistory — Previous-step pose buffer for render interpolation under a fixed-step loop. Reads poses straight from the store's {@link EntityStore.columns} into one flat array indexed by handle, so a step over N entities allocates nothing and does no id lookups. Each captured pose remembers its handle's generation: a handle reused by a new entity, or a body respawned under the same id, renders live until the next step.
703
710
  - `lerpAngle` (function): function lerpAngle(from: number, to: number, t: number): number — Shortest-arc interpolation between two yaw angles.
704
711
 
705
712
  ## @jgengine/core/runtime/prediction
@@ -48,6 +48,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
48
48
 
49
49
  - `readQuantity` (function) · `import { readQuantity } from "@jgengine/core/commands/commandInput"`
50
50
 
51
+ ## context-map — per-context state keyed by the context a wrapper stands for, so a wrapped context shares its model
52
+
53
+ - `ContextMap` (class) · `import { ContextMap } from "@jgengine/core/runtime/contextIdentity"`
54
+
51
55
  ## default-drive-codes — stock WASD/arrow throttle, brake, steer and handbrake key codes
52
56
 
53
57
  - `DEFAULT_DRIVE_CODES` (const) · `import { DEFAULT_DRIVE_CODES } from "@jgengine/shell/gameKit"`
@@ -46,7 +46,7 @@ Use [capabilities.md](capabilities.md) for intent-to-import discovery, [api.md](
46
46
  - A viewer's interest is the game's `replication: { aoiRadius, aoiAlways }`. The host asks the scene spatial index for entities within `aoiRadius` of the viewer's own entity, adds `aoiAlways` ids (a list or a function), and sends each connection a diff of only what entered, changed or left its view. A viewer with no entity yet holds every entity.
47
47
  - A hosted world auto-saves a changed world at most once per `saveIntervalMs` (the node server and the browser host default to `DEFAULT_HOST_SAVE_INTERVAL_MS`, 1000); `0` saves after every changed tick, which copies the whole world 20 times a second. `bun scripts/multiplayer-load.ts` reports host ms per tick, and bytes allocated per tick under `BUN_JSC_useGC=false`.
48
48
  - A module that throttles itself with `priority` still gets a projected baseline each tick instead of diffs.
49
- - Measure with `bun scripts/multiplayer-load.ts` (bytes and host ms per tick for N clients and E entities) and `bun scripts/multiplayer-two-clients.ts` (a real game, two clients).
49
+ - Measure with `bun run perf server` (N players posing in one room: host wall and CPU ms, MB and messages out per game second, from presence fan-out), `bun scripts/multiplayer-load.ts` (world replication: bytes and host ms per tick for N clients and E entities) and `bun scripts/multiplayer-two-clients.ts` (a real game, two clients).
50
50
 
51
51
  ## Traps
52
52
 
@@ -52,6 +52,8 @@ Once a session resolves, the shell (`GameServerHost`'s internal player) wires it
52
52
 
53
53
  A synced pose (`@jgengine/ws/protocol`'s `WsPose`/`WsPresenceRow`) carries an optional `appearance: Record<string, string | number | boolean>` alongside position/rotation — primitive-valued client-set tags (skin id, mount id, active emote, team color) riding the same pose-sync frames as position, so a remote player's cosmetic state updates on the existing presence channel instead of a second round-trip.
54
54
 
55
+ The ws host router (`createHostRouter`) sends each room's full presence roster at most once per pose interval (`poseRules.minIntervalMs`, 100 ms by default), however many players move: the first move in a quiet room goes out at once, later moves and appearance changes ride the roster that closes the window, and a player's first pose or a leave goes out immediately. Every broadcast (presence, chat, voice) is encoded once per codec and the same frame goes to every subscriber, so a room's cost grows with players × subscribers, not with its cube.
56
+
55
57
  **Server side** — `@jgengine/convex/server` ships an entire authoritative Convex backend as factories, not a template to copy: `jgengineTables()` (schema spread), `createGameServerFunctions({ runtimes?, auth? })`, `createLeaderboardFunctions({ auth? })`, `createPresenceFunctions({ auth?, freshWindowMs?, idleCutoffMs?, poseRules?, resolveSpawn? })`, `createChatFunctions({ auth?, historyLimit?, maxBodyLength?, minIntervalMs? })`, and `jgengineCronSpecs()` (tick/flush interval metadata for a `crons.ts`). A consumer's `convex/` directory is ~25 lines: `schema.ts` (`defineSchema({ ...jgengineTables() })`), four one-line re-export files (`runtime.ts`, `leaderboard.ts`, `presence.ts`, `chat.ts`, each just calling its factory), and `crons.ts` registering the tick (1s) + flush (60s) internal mutations — see `examples/convex-host` for the reference shape. No game-specific code lives there; any JGengine game can point at the same deployment. Games without a registered `GameRuntime` fall back to a no-save runtime that only understands `engine.ping`; pass `createGameServerFunctions({ runtimes: [createGameRuntime({ gameId, commands, loop, save })] })` to make `runCommand` / tick / save actually do something. When a host mutation must pair snapshot work with its own table writes in one transaction, use the factory's `helpers` (`loadSnapshot`, `applyCommand`, `persistSnapshot`, `runCommand` — same registry and auth mode) or the standalone `loadServerSnapshot` / `persistServerSnapshot` exports.
56
58
 
57
59
  **How big a hosted world gets.** One server is one Convex document holding the world row, the roster, and every member's session state, so member count is a per-transaction cost. `slotsPerServer` is capped at `JG_MAX_MEMBERS_PER_SERVER` (256) and `createGameServerFunctions` throws at wiring time on anything larger — a bigger number was never a bigger world, just a document that overflows later. World geometry that outgrows one row goes in `jgWorldChunks`: hydrate by key (`loadServerSnapshot(ctx, server, runtime, { players, chunkKeys })`), read from clients with the generated `getChunks`, and key cells with `chunkKeyAt` / `chunkKeysInRadius` from `@jgengine/core/runtime/worldChunks`. Mutate chunks through `setChunk` / `updateChunk` / `deleteChunk` — a raw `snapshot.chunks[key] = …` is never marked dirty and never persists.