@jgengine/rapier 0.18.1
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 +1112 -0
- package/README.md +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/rapierBackend.d.ts +10 -0
- package/dist/rapierBackend.js +223 -0
- package/package.json +39 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
The lockstep game SDK set is `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets}`
|
|
4
|
+
(this file’s 0.x.y releases). The CLI package `jgengine` and `@jgengine/github` use
|
|
5
|
+
**separate** version lines and may lag. Format follows [Keep a Changelog](https://keepachangelog.com);
|
|
6
|
+
each release **leads with a Migrate block** — the concrete steps to move a game
|
|
7
|
+
from the previous version onto the new APIs — because the point of a bump is to
|
|
8
|
+
let consumers pick up the better stuff, not just to list what moved.
|
|
9
|
+
|
|
10
|
+
Agents building on the published SDK can also read this programmatically:
|
|
11
|
+
`import { VERSION, CHANGELOG } from "@jgengine/core/meta/changelog"` gives the
|
|
12
|
+
same data as typed values, so an updater can diff its installed version against
|
|
13
|
+
the latest and surface the migration steps. Easier still: `npx jgengine upgrade`
|
|
14
|
+
run inside a game project diffs the installed `@jgengine/*` versions against the
|
|
15
|
+
latest release and prints every Migrate step and Adopt-worthy addition in
|
|
16
|
+
between (`--json` for structured output).
|
|
17
|
+
|
|
18
|
+
## [Unreleased]
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Separate SDK publication validation from external Games quality checks while retaining the full local and main-branch gates.
|
|
23
|
+
|
|
24
|
+
- Correct foot-IK endpoint recomputation after hip rotation; verify reachable targets and clamping without changing bone lengths.
|
|
25
|
+
- Restore release CI prerequisites and make catalog-loader tests independent of the external Games checkout.
|
|
26
|
+
|
|
27
|
+
<!--
|
|
28
|
+
Every PR that changes `packages/*/src` records its consumer-facing change here, so
|
|
29
|
+
the next release's notes are complete by construction. Add a bullet under the right
|
|
30
|
+
subhead below. `bun run check-changelog` fails a source PR whose `[Unreleased]` block
|
|
31
|
+
is untouched; pure refactors/tests bypass with `[skip changelog]` in a commit message.
|
|
32
|
+
At publish, rename this heading to the new version and mirror the entries into
|
|
33
|
+
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
34
|
+
-->
|
|
35
|
+
|
|
36
|
+
## 0.18.1
|
|
37
|
+
|
|
38
|
+
### Migrate
|
|
39
|
+
|
|
40
|
+
- **Bump lockstep SDK packages to `^0.18.1`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.15.1`; `@jgengine/github` is `0.5.1`.
|
|
41
|
+
- Declare `topology: "shared"` on runtime definitions. Shared Convex hosts migrate the bounded legacy roster into indexed membership and per-player session rows; use `getServerCapacity` for stable lobby subscriptions.
|
|
42
|
+
- Put every command's read scope on its definition. Omitted scopes now load the actor and no chunks; explicitly request whole-world reads only where needed. Trusted host mutations can compose writes through `helpers.runCommand`.
|
|
43
|
+
- Handle typed join outcomes (`ok` and `reason`) or use `useServerSession` and `JoinGate`. Pass runtimes to `jgengineCronSpecs`; tick registration requires an actual tick hook.
|
|
44
|
+
- Declare currency precision with `decimals` and remove game-side cent conversions. Public amounts stay in major units and wallet writes round through integer minor units.
|
|
45
|
+
- Replace custom presence, chat, territory and tick batching with `createWorldPresenceStore`, chat helpers, chunk territory, and `forEachOnlinePlayer`. Legacy claim backfills must finish before removing their old schema.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- Indexed shared membership and capacity rows, bounded neighborhood poses with 100 ms server/client gates, durable rate limits, authenticated diagnostics and bounded retention.
|
|
50
|
+
- Territory footprint planning and atomic placement claims, chat validation and headless chat/join/territory surfaces, elapsed accrual, safe quantities, and player-profile reset through the initializer.
|
|
51
|
+
- An explicit `--patch` release option.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- Added smooth interpolation for remote presence player poses.
|
|
56
|
+
- WS ping/pong messages and client RTT sampling on the backend object.
|
|
57
|
+
- Added skin-backed HUD chrome variables, font loading, and the procedural `HudSkinPreview` fixture.
|
|
58
|
+
|
|
59
|
+
- Core gamepad snapshot model, deadzone/curve resolver, and controller glyph labels.
|
|
60
|
+
- Shell gamepad source polls connected pads, merges analog actions, supports synthetic `?gamepad=1` input, and exposes `ctx.input.rumble`.
|
|
61
|
+
- Core action context stacks layer gameplay, menus, and passthrough overlays with snapshot/restore; shell action tracking now respects the active context.
|
|
62
|
+
- Shell audio now supports listener orientation and per-sound Web Audio panners. `setListenerPose` accepts `{ position, forward, up }`; bare positions remain supported for one release.
|
|
63
|
+
- Core perception tracks deterministic sight, hearing, occlusion, and bounded observation memory for AI.
|
|
64
|
+
- `syncWorldColliders` now keeps terrain and static object colliders synchronized with a physics backend.
|
|
65
|
+
- Core decision graphs provide deterministic selector, sequence, condition, action, and utility runtimes with snapshot/restore.
|
|
66
|
+
- Core behavior descriptors can run registered decision graphs at the interest-scheduler cadence.
|
|
67
|
+
- Core navigation meshes provide deterministic polygon routing, off-mesh links, closest-point queries, and nav raycasts.
|
|
68
|
+
- WS sessions now issue resume tickets and retain disconnected memberships for a 15-second grace window, allowing reconnects to rejoin without replacing player state.
|
|
69
|
+
- Shell entity sprites can play atlas-backed sprite clips while preserving raw texture sprites.
|
|
70
|
+
- Tilemap layers can render atlas-backed textured tiles as one instanced quad mesh with parallax.
|
|
71
|
+
- Core pixel-perfect orthographic frustum and pixel-grid camera snapping are available to shell cameras.
|
|
72
|
+
- Core sprite atlas adapters and deterministic 2D sprite clip playback primitives.
|
|
73
|
+
- `src/art-direction.md` scaffold and `check-art-direction` gate for created games.
|
|
74
|
+
- Host-authoritative shell sessions now expose each accepted world frame to the local prediction buffer, reconciling and snapping the possessed pose only when drift exceeds its threshold.
|
|
75
|
+
- Shared GLTF loader configuration for Draco and KTX2-compressed assets, with CDN defaults and renderer capability detection.
|
|
76
|
+
- Material map overrides now support metalness, emissive, and height maps on standard and physical materials.
|
|
77
|
+
- Presentation and editor documents now accept gradient, HDRI/EXR, and cube-map environment sources.
|
|
78
|
+
- Authored `light` markers now feed bounded point/spot punctual lights with an eight-light default budget and editor warning.
|
|
79
|
+
- Graphics profiles now resolve per-tier render scale, shadow budgets, culling distance, particles, cascades, and post stages; games can override them with `defineGame({ graphics })`.
|
|
80
|
+
- `defineGame({ physics: { backend } })` now adopts a capsule character controller for shell/headless movement and registers the backend simulation after movement, including floor collision and step-up behavior.
|
|
81
|
+
- Added the `physics-probe` dev scene for Rapier crate, ragdoll, and vehicle motion.
|
|
82
|
+
- Assets now expose import-spec validation and magic-byte classification for supported source files.
|
|
83
|
+
- Hosted WS sessions now support spectator joins, viewer roles, throttled low-priority snapshot modules, and default command rate limits.
|
|
84
|
+
- `@jgengine/navbake` bakes deterministic navigation meshes and editor documents can store named bake data.
|
|
85
|
+
- Core tactical queries derive cover points and score bounded candidate positions.
|
|
86
|
+
- Added createSnapshotBuffer for delayed snapshot interpolation.
|
|
87
|
+
|
|
88
|
+
### Migrate
|
|
89
|
+
|
|
90
|
+
- `AudioEngine.setListenerPose` now accepts `{ position, forward, up }`. Bare `Vec3` positions remain compatible for one release and use forward `[0, 0, -1]` with up `[0, 1, 0]`.
|
|
91
|
+
- **Hosted world stores are asynchronous.** Implement `HostedWorldStore.load()` and `save()` as `Promise`-returning methods and construct persisted sessions with `createHostedWorldSessionAsync`; synchronous tests can keep using `memoryWorldStore`.
|
|
92
|
+
- **Look presets now default to `neutral`.** Games that relied on the previous implicit cinematic
|
|
93
|
+
sky/post stack should set `look: "photoreal"` (or keep `look: "cinematic"`) explicitly.
|
|
94
|
+
|
|
95
|
+
- **`ServerTickPlan.due` is `{ id, runs }[]` instead of a repeated id list.** `planServerTick` repeated a system id once per missed interval, and the obvious way to consume that — `due.includes(id)` or a `Set` — ran the system once while the returned anchor had already advanced by every repeat, silently dropping the catch-up work. A host now reads `runs` (or `tickRunCount(plan, id)`) and loops that many times.
|
|
96
|
+
- **`CommandDef.validate` / `CommandDef.apply` take a fourth argument, `nowMs`.** Existing implementations that ignore it are unaffected. Commands that took a `now` in their `input` should read this instead — the client supplied the old one.
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
|
|
100
|
+
- Hosted game runner queues authoritative inputs by simulation tick and applies the in-force frame each fixed step.
|
|
101
|
+
- Create scaffolds now use a 60 Hz simulation with interpolation enabled by default.
|
|
102
|
+
|
|
103
|
+
- Rigged `ModelConfig` entries can opt into shell foot IK with named thigh/shin/foot bones; the driver raycasts the shared scene terrain, solves each chain, and fades corrections while airborne.
|
|
104
|
+
|
|
105
|
+
- Animation graph clips may carry root-bone position tracks; states with `rootMotion: true` return the sampled displacement and the shell applies it to the entity while restoring the root bone's bind translation.
|
|
106
|
+
|
|
107
|
+
- Added renderer-free two-bone, FABRIK, and look-at inverse-kinematics solvers to `@jgengine/core`.
|
|
108
|
+
|
|
109
|
+
- Shell model animation now routes states and one-shots through the shared animation graph runtime.
|
|
110
|
+
|
|
111
|
+
- CI now clones the external games repository with `GAMES_CLONE_TOKEN` and runs the game shape, content, front-end, art-direction, and feel gates against it; those gates fail loudly in CI when the checkout is missing while remaining no-ops locally.
|
|
112
|
+
|
|
113
|
+
- `jgengine create` now accepts `--player`, `--ground`, and `--scene` so generated games can choose their model, terrain ground, and empty or starter scene.
|
|
114
|
+
|
|
115
|
+
- **Directional shadow frustum config actually reaches the GPU.** `shadowCameraSize` on a single (non-cascaded) shadow light was silently inert: the shell wrote the bounds through R3F pierced props and nothing refreshed the shadow camera's projection matrix, so three rendered depth with its default ~10-unit box at the world origin — the reason no open-world game had cast shadows away from spawn. The shell now syncs the projection (and reallocates the shadow map when `shadowMapSize` changes). `bloom.threshold`'s doc also now states it measures raw HDR before the exposure stage.
|
|
116
|
+
- **CSM (`cascades > 1`) no longer over-lights streamed meshes or clobbers material shaders.** Cascade scoping is injected per material, but the scan ran every 30 frames, so a model that streamed in rendered lit by every cascade light at once (a cascades× sun) until the next scan — a sub-second flash on desktop, the whole shot on capture rigs. The scan now runs every frame behind a WeakSet, and `setupMaterial` chains with a material's own `onBeforeCompile` (rim light, detail maps) instead of replacing it.
|
|
117
|
+
|
|
118
|
+
- **The host's wall clock reaches the runtime hooks.** `RuntimeInitContext` (so `onInit`, `onNewPlayer`, and `onTick` alike) carries `nowMs`, the host timestamp for that call. Anything keyed to real time — a UTC date rollover, a `lastTickAt` anchor read by non-tick code, an RNG seed or `createdAt` stamp — reads it rather than accumulating `dtSeconds` against an epoch persisted in the snapshot, which drifts by whatever the tick loses to stalls and `maxCatchUp` clamping. `GameRuntime.hydrate` takes an optional `nowMs`; `tick`, `joinPlayer`, and `runCommand` take an optional trailing `nowMs`. All default to `Date.now()`, and the shipped WS and Convex hosts pass the timestamp they already computed to plan the tick.
|
|
119
|
+
- **Imported models cast and receive shadows.** `cloneModelScene` never set `castShadow`/`receiveShadow` on a GLB's meshes (only the magenta fallback box had them), so every catalog model in every game rendered shadowless however the lights were configured. Cloned models now default to `ModelConfig.shadows: "both"`; set `"cast"`, `"receive"`, or `"none"` per model (foliage cards, decals, viewmodels).
|
|
120
|
+
- **`jgengine create` installs `game-design` and `jgengine-ui` by default.** The minimal skill set was intake, editor, and verify, so a real project never saw the greenfield design contract or the UI art-direction template unless someone ran `jgengine skills --all`; `MINIMAL_GAME_SKILLS` now carries both.
|
|
121
|
+
|
|
122
|
+
### Added
|
|
123
|
+
|
|
124
|
+
- **Host-authoritative world replication over WS.** Hosted sessions expose revision-cursor pulls, so server subscribers receive compact world diffs and automatically recover with a fresh baseline after missed revisions; viewer-specific projections still receive per-viewer baselines.
|
|
125
|
+
- **Animation graph — `ModelAnimationConfig.graph` and `@jgengine/core/anim/animGraph`.** A serializable, layered animation state machine: `clip`, `blend1D`, and `blend2D` states, transitions as data (parameter comparisons, consumed triggers, exit times, crossfade durations), masked and additive layers, and clip events. `createAnimGraphRuntime` is headless and owns every clip's playback time and weight, so the shell's mixer only seeks and weights actions (`useModelAnimation` takes the graph path when `graph` is set) and a headless host or test advances the same graph without three.js. The shell feeds the entity's smoothed ground speed as `speed`, merges parameters from the entity blackboard key `ANIM_PARAMS_KEY`, arms triggers from `entity.animation` / `combat.hitReaction` / `entity.died`, and emits the new `animation.event` for clip events (foot plants, hit frames). `locomotionGraph()` builds the previous idle/walk/run plus one-shot behavior as one authored graph.
|
|
126
|
+
- **`PhysicsBackend` — one interface over interchangeable rigid-body solvers.** `@jgengine/core/physics/physicsBackend` defines bodies (`BodyDesc` with a box / sphere / capsule / convex / trimesh / heightfield shape union, quaternion rotation, angular velocity, layers and mask, CCD flag), joints, `raycast`, `shapecast`, `overlap`, contact events, and `snapshot`/`restore` with stable handles; `capabilities` says what a backend really does. `createPhysicsWorldBackend` puts the zero-dependency `PhysicsWorld` behind it (translation-only, non-box shapes collide as their bounding box, warned once), and `runPhysicsBackendConformance` is the behavioral contract any adapter runs. `createCharacterController` (`@jgengine/core/movement/characterController`) is the first consumer: a capsule collide-and-slide controller with step-up, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry, pure over any backend and retunable during play.
|
|
127
|
+
- **Fixed-step simulation with render interpolation — `defineGame({ simulation: { hz } })` and `ctx.sim`.** Every driver (shell frame, HUD-only, headless runner, authoritative host) now steps through one `SimLoop` (`@jgengine/core/runtime/simLoop`): a fixed `hz` accumulates real time and runs equal-dt steps behind a catch-up cap (`maxCatchUpSteps`, default 5) instead of spiralling; the default stays `"variable"` (one step per frame) so existing games are unchanged until they opt in. Under a fixed rate the shell renders entities at `ctx.sim.renderPose(id)` — the pose interpolated between the previous and current step (`@jgengine/core/runtime/poseInterpolation`), with teleports past `snapDistance` snapping — so a 20 Hz sim no longer stutters. `ctx.sim.tick()` counts steps and `InputFrame.tick` carries it over the wire; `ctx.sim.addStage({ phase, run })` inserts per-step work before movement, after movement, or after `onTick` (the hook a physics backend or netcode buffer registers through). System fixed-rate groups gain the same substep cap. For replay and rollback: `seededRng` streams expose `state()`/`restore()` (`rngStateOf`/`restoreRng` for any `ctx.rng`), `createSimSnapshotRegistry`/`createContextSimSnapshot` capture world, clock, loop, and rng cursor together, `createInputRecorder` logs frames by tick, and `HeadlessRunner` gains `snapshot()`/`restore()`/`tick()` — a test proves that replaying recorded input from a snapshot reproduces every entity pose.
|
|
128
|
+
- **City and street races share one generator.** `extractCircuitRoute` lifts a sealed-off race lap from a city street network; city rules, editor baking, playground modes, and the shell renderer carry the same street dials through the resulting race surface and junction dressing.
|
|
129
|
+
- **`ctx.particles` — the game-reachable particle seam.** Game logic queues one-shot bursts (`ctx.particles.burst(config, count)`) and keyed standing emitters (`attach`/`retune`/`detach`, with `follow` to track a scene entity) as plain serializable data (`@jgengine/core/vfx/particleDirector`); the shell auto-mounts a renderer that turns them into pooled GPU point clouds via the existing `createParticleSystem`/`ParticleField`, caps pool sizes by graphics quality (low 128 / medium 256 / high 512 per effect, degrading density instead of dropping effects), and reaps finished bursts. Dust, sparks, exhaust, and debris no longer require forking the shell; headless tests assert the requested effects straight off the director.
|
|
130
|
+
- **`resolveSourceWalkerStep(source, cache, position, stepX, stepZ, options)`** (`@jgengine/core/movement/solidObstacles`) — `resolveWalkerStep` with no `GameContext`, for movement rules that live in a pure package with no React or three.js. `SolidObstacleSource`, `ObstacleReachCache`, `createObstacleReachCache`, `sourceObstacleReach`, `sourceObstaclesNear`, and `slideStep` are public alongside it instead of `@internal`, so a game no longer has to push the resolver up into its app layer and inject it back down. `resolveWalkerStep`'s doc now states that it already resolves X and Z separately, so a caller doing its own axis-split must hand it one axis per call.
|
|
131
|
+
- **`tickRunCount(plan, id)`** (`@jgengine/core/time/serverTick`) — how many runs a plan owes a system, or 0 when it is not due.
|
|
132
|
+
- **`grass()` exposes the clump/lighting knobs its shader already had.** `GrassEnvironmentConfig` gains optional `bladeBend`, `tuftRadius`, `colorVariation`, and `normalLift`, forwarded by the shell's environment renderer — a world descriptor can now author bushy wind-combed scrub instead of the fixed lawn-blade look; defaults unchanged.
|
|
133
|
+
- **`TerrainDetailConfig.sweeps`** — the detail shader's two macro colour sweeps (sun-dried patches, cooler wet pockets) take per-world RGB multipliers instead of hardcoded meadow tints. The old green-leaning "lush pocket" default painted moss onto arid/ashen biomes; defaults are unchanged, so a desert world should set warm/neutral `sweeps` explicitly.
|
|
134
|
+
- **`@jgengine/rapier`** — a Rapier-backed `PhysicsBackend` adapter with native capsules, shapecasts, rotation, CCD, joints, and collision queries.
|
|
135
|
+
|
|
136
|
+
## 0.18.0
|
|
137
|
+
|
|
138
|
+
### Migrate
|
|
139
|
+
|
|
140
|
+
- **Bump lockstep SDK packages to `^0.18.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.15.0`; `@jgengine/github` is `0.5.0`.
|
|
141
|
+
- **Register the presence reaper and redeploy your schema.** `jgPoses` gains `sessionId` / `kind` / `label` / `revokedAt` (all optional) and a `by_updated` index, and `createPresenceFunctions` now also returns `reapIdlePresence`. Re-export it (`export const { list, sync, leave, reapIdlePresence } = createPresenceFunctions(...)`) and add `crons.interval("jg presence reap", { seconds: 60 }, internal.presence.reapIdlePresence, {})`. Without it nothing deletes the row of a player who closed the tab — `list` filtered stale rows at read time but the table grew forever. `jgengineCronSpecs()` carries the new entry, and every spec now names the `module` (`"runtime"` / `"presence"`) it is exported from.
|
|
142
|
+
- **`presence.sync` returns `{ pose, lastSeenAt, displaced }` instead of `null`,** and `presence.leave` marks the row revoked instead of deleting it (the reaper collects it once idle). A caller that ignored the return value is unaffected.
|
|
143
|
+
|
|
144
|
+
### Changed
|
|
145
|
+
|
|
146
|
+
- **`presence.sync` accepts a call with no `pose` at all,** which holds the stored position and moves only the liveness stamp. `createConvexPresenceSync` sends one itself when the pose gate has suppressed writes for 10s, so a standing player keeps their row alive without a pose write — and without the reaper collecting a live session.
|
|
147
|
+
- **`PresencePoseRow` carries optional `sessionId` / `kind` / `label`,** so a nameplate reads off the presence row instead of joining against users per frame.
|
|
148
|
+
|
|
149
|
+
### Added
|
|
150
|
+
|
|
151
|
+
- **`createPresenceFunctions` gains the policy hooks the pose lane was missing** — `resolveSpawn(ctx, { serverId, userId, kind })` for where a session with no row starts (async and ctx-bearing, so a spawn can read the world, replacing a hardcoded origin), `poseRules` as a function of the actor `kind` so an agent need not obey a human's speed cap, `idleCutoffMs` / `maxReapPerRun` for the reaper, and optional `sessionId` on `sync` / `leave`: a second tab is a second row, syncing revokes the others, and the displaced session learns it on its next call through `displaced: true`.
|
|
152
|
+
|
|
153
|
+
- **A command declares what it touches, so `runCommand` hydrates that and nothing else.** `CommandDef` gains an optional `scope(input, actorUserId)` returning `{ players?, chunkKeys? }`, evaluated before hydration; the registered `runCommand` mutation also accepts an explicit `scope` argument, which wins over the declared one. A game that never sets either keeps the whole-world default. `ServerLoopHooks.joinScope(userId, isNew)` does the same for `onNewPlayer`.
|
|
154
|
+
- **`GameRuntime.commandScope` / `GameRuntime.joinScope`** expose those declarations to any host, alongside the pure helpers `resolveCommandScope` / `scopeWithActor` / `commandScopeEscape` (`@jgengine/core/runtime/commandRunner`).
|
|
155
|
+
|
|
156
|
+
### Fixed
|
|
157
|
+
|
|
158
|
+
- **`runCommand` no longer reads the whole world on every command.** `LoadSnapshotScope` landed in 0.17 but only `helpers.loadSnapshot` accepted one, so the supported way to run a command — the registered `runCommand` mutation, a one-line delegation to `helpers.runCommand` — hydrated every member profile and every chunk however narrow the command was, and a game could only get bounded hydration by reimplementing the mutation. `runCommand`, `joinServer`, and `leaveServer` now hydrate a scope; a command whose `apply` dirties a player or chunk its scope never named is refused rather than persisted, since writing an unhydrated chunk overwrites the stored one with a blank. `flushSave` / `flushServerIfDue` / `tickActiveServers` / `flushDirtyServers` still hydrate everything, which is correct for what they write.
|
|
159
|
+
|
|
160
|
+
## 0.17.0
|
|
161
|
+
|
|
162
|
+
### Migrate
|
|
163
|
+
|
|
164
|
+
- **Bump lockstep SDK packages to `^0.17.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.14.0`; `@jgengine/github` is `0.4.0`.
|
|
165
|
+
- **A deleted player profile stays deleted.** When a game persists players (`save.scope` includes `player`), hydrating a member with no `jgPlayerProfiles` row no longer falls back to the copy of their state kept on the server row — that copy was resurrected and then written back as authoritative, so a world reset that cleared profiles but not servers silently restored every player's old balance. A game whose save scope excludes players (`"none"`, `"chunks"`) is unchanged: the server-row copy is still their state. If you seed members onto a server row without going through `joinServer`, write their profile row too.
|
|
166
|
+
- **`slotsPerServer` is capped at 256** (`JG_MAX_MEMBERS_PER_SERVER`). `createGameServerFunctions` now throws at wiring time on a larger value instead of accepting it and failing at the write that overflows the document. One server is one Convex document holding the world row, the roster, and every member's session state; a bigger number was never a bigger world. Shard across servers, or move world state into `jgWorldChunks`.
|
|
167
|
+
- **The tick cron only sweeps games that declare `loop.onTick`.** A game with no tick hook used to pay a full hydrate plus a full persist every second per running server, for a snapshot `runtime.tick` returned unchanged; it now costs nothing while idle. If you relied on the tick cron to autosave a hookless game, the 60s `flushDirtyServers` cron and the `flushSave` mutation still do it.
|
|
168
|
+
|
|
169
|
+
### Changed
|
|
170
|
+
|
|
171
|
+
- **Walking NPCs now stop at the geometry that stops the player.** `wander`, `patrol`, and `ctx.scene.entity.moveToward` consulted no obstacles at all, so a pedestrian walked through the building the player cannot enter. All three now slide their step against the scene's blocking physical colliders — the same query `stepPlayerMovement` reads. A patrol's path state still advances on its own clock (an authored route is not rewound by contact), but the entity can no longer end a tick inside a solid. Pass `avoidSolids: false` to `moveToward` for a mover meant to ignore the world (a flier, a camera rig).
|
|
172
|
+
- **`@jgengine/assets` verify fails a model source that contributes no index entries.** A model pack that has never been pulled is a pin nothing has ever checked — that is how a Street Pack archive sat in the catalogue titled "Downtown City MegaKit". Record why a pack is not pulled in the new `AssetSource.unpulled` string; materials and sprites pull on demand and are exempt.
|
|
173
|
+
- **`persistServerSnapshot` returns `boolean` and skips a write that would change nothing.** Every subscriber of a query that read a server row re-receives the whole result when that document changes, so an unconditional write on the 1 Hz tick re-pushed the entire world to every client once a second whether or not anything moved. `ServerPersistPlan` gains `changed` (`serverState` / `sessionPlayers` / `revision` / `any`) for hosts that write field-by-field.
|
|
174
|
+
- **`sessionPlayers` is merged over the stored map, not rebuilt from the snapshot**, and entries for non-members are pruned. This is what makes a partially hydrated snapshot safe to persist.
|
|
175
|
+
- **`tickActiveServers` is bounded** to `maxServersPerTick` (default 32) servers per transaction, oldest tick anchor first, instead of pulling every running server of every game into one transaction. `flushDirtyServers` is bounded the same way.
|
|
176
|
+
|
|
177
|
+
### Added
|
|
178
|
+
|
|
179
|
+
- **`solidObstaclesNear` / `resolveWalkerStep` (`@jgengine/core/movement/solidObstacles`)** — the engine's single answer to "what is solid here", now shared rather than re-derived per consumer. `solidObstaclesNear(ctx, position, reachX, reachZ)` returns the blocking physical colliders near a point through a bounded `inBox` broadphase; `resolveWalkerStep(ctx, position, stepX, stepZ)` slides a horizontal step against them with the player's axis-separated sliding. Reach for these instead of deriving obstacles from a game catalog's own solidity flags.
|
|
180
|
+
- **Bounded hydration and a client chunk read** — `loadServerSnapshot(ctx, server, runtime, { players, chunkKeys })` hydrates only the members and chunks a mutation will touch instead of a document read per member plus one per chunk, and the generated `getChunks` query lets a client read chunk state by key (capped at `MAX_CHUNKS_PER_QUERY`). Chunks were the documented escape hatch for world state too large for one row, but reads were unbounded and clients could not see them at all, so moving geometry there made things worse. `getServerMeta` returns lobby state (status, member count, revision) without the world payload.
|
|
181
|
+
- **`@jgengine/core/runtime/worldChunks`** — `chunkKeyAt` / `chunkCoordAt` / `chunkKeyOf` / `parseChunkKey` / `chunkKeysInRadius` at a declared cell size, plus `setChunk` / `updateChunk` / `deleteChunk`, which mark the key dirty so the host actually persists it. `chunkKey` was an opaque game-chosen string and the engine never wrote `dirty.chunks`, so every game reimplemented the coordinate mapping and a game that forgot to push a key silently never saved that chunk.
|
|
182
|
+
- **Placed objects cull to `replication.aoiRadius`.** `objects` is the one large spatial replicated set and had no projector, so a world with a few thousand placements sent all of them to every viewer. It now joins the same area-of-interest policy as `entities` and `stats`. Unset `aoiRadius` still replicates everything.
|
|
183
|
+
- **`createGameServerFunctions({ matchmaking: "singleton" })`** — one shared world per game: auto-match always targets the existing world and a join past capacity fails loudly, instead of quietly creating a second world the player would be alone in. `joinServer` also reconciles a stored `slotsPerServer`/`save` that has drifted from the host options, which previously made raising the option a no-op on worlds that already existed. `selectJoinTarget` (`@jgengine/core/runtime/hostPolicy`) is the pure decision behind it.
|
|
184
|
+
- **`GameRuntime.hasTick`** reports whether a runtime declares `loop.onTick`, so any host's scheduler can skip a server whose tick is a no-op by construction.
|
|
185
|
+
|
|
186
|
+
### Removed
|
|
187
|
+
|
|
188
|
+
- **Asset source `quaternius-downtown-city`.** It was titled "Downtown City MegaKit" but pinned at the Street Pack archive, which ships Blends/FBX/OBJ only — a pull found no models. Nothing referenced it.
|
|
189
|
+
|
|
190
|
+
## 0.16.0
|
|
191
|
+
|
|
192
|
+
### Migrate
|
|
193
|
+
|
|
194
|
+
- **Bump lockstep SDK packages to `^0.16.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.13.0`; `@jgengine/github` is `0.3.0`.
|
|
195
|
+
- **Placed objects now replicate.** `objects` joined the always-on baseline snapshot modules, so `ctx.hydrate` replaces the client's placed object set with the host's, the way `entities` already did. A game whose peers all run the same authored placement is unaffected; a game that places props on the client only, after hydrate, must move that placement to the host or keep it out of `ctx.scene.object`.
|
|
196
|
+
|
|
197
|
+
### Changed
|
|
198
|
+
|
|
199
|
+
- **`GeneratedBuilding`'s `kit` prop is now the `BuildingKit`**; the per-part React escape hatch it used to name moved to `partRenderer`, so `kit` means the same thing on `GeneratedBuilding` and `InstancedBuildings`. Rename the prop if you passed a `BuildingKitRenderer`.
|
|
200
|
+
|
|
201
|
+
### Added
|
|
202
|
+
|
|
203
|
+
- **`modelLoadFallbacks()` from `@jgengine/shell/render/modelLoad`** lists every model the shared loader diagnosed as broken and replaced with a magenta placeholder. A placeholder was only ever a console warning, so a shipped game could render one in its starting area and nobody found out until someone looked at a screenshot; a capture host or smoke test can now require the list to be empty. `clearModelLoadFallbacks()` resets it between runs.
|
|
204
|
+
- **`worldHealthBars.occlude` / `WorldHealthBars`+`WorldNameplates` `occlude` prop.** World bars and nameplates are a screen-space overlay drawn outside the WebGL depth buffer, so nothing in the scene can hide them and an enemy's health bar reads straight through the building it stands behind. Set `occlude: true` to drop a bar when world geometry blocks the line from the player to the entity. Default `false` preserves the always-visible behaviour every shipped game currently relies on.
|
|
205
|
+
|
|
206
|
+
- **`DustField` + `WeatherLayer` mode `"dust"` (`@jgengine/shell/weather`) — wind-borne particulate.** `WeatherLayer` could only do `clear`/`rain`/`snow`/`mixed`, so an arid, volcanic, ashen, or blown-out world had no way to put anything in the air. Dust drifts on horizontal wind with a slow vertical bob rather than falling; `groundBias` packs density toward the surface, motes fade near the top of the volume so recycled ones do not pop against the sky, and faster motes ride higher. Set `dustAlways` to keep it in the air alongside rain or snow instead of replacing them. Defaults `DEFAULT_DUST_COUNT`/`DEFAULT_DUST_DENSITY` sit beside the rain and snow pair.
|
|
207
|
+
|
|
208
|
+
- **Authored `city` volumes render their near-LOD facades with a `BuildingKit`.** `CityRules.buildingKit` names a kit; a game registers the bound art with `registerBuildingKit(kit)` from `@jgengine/shell/structures/buildingKitRegistry` and the scene document carries only the name, so a city stays portable and free of model paths. Unset or unknown names keep the palette blocks. This puts the whole `city` primitive — not just the `building()` feature — on the kit path.
|
|
209
|
+
- **`BuildingKit` from `@jgengine/core/world/buildingKit`** binds a generated building's facade part slots to real models, so procedural buildings render as an instanced asset kit instead of untextured blocks. `defineBuildingKit({ parts: { wall: ["Wall_A.glb", …] } })` maps a part kind — or a `${facade}.${kind}` slot — to ordered variants that `kit.variant` indexes, with `fit`/`offset`/`rotation`/`scale`/`tint`/`autoOrient` per variant. Kinds a kit leaves unbound keep rendering the palette block, so a partial kit is a valid kit, and `omit` drops a kind entirely. Pass one through `building({ kit })` or straight to `InstancedBuildings`; `buildingKitVariantCounts(kit)` feeds `generateBuilding` so its seeded picks span exactly the models the kit binds.
|
|
210
|
+
- **`generateBuilding`, `generateBuildingDistrict`, and `createBuildingConfig` are now documented public API** (`@jgengine/core/world/buildings`) and appear in `capabilities.md` under `building-generator`.
|
|
211
|
+
- **`capture.views` on `defineGame`** — named framings (`GameCaptureView`: `look`, `lookFrom`, `spawn`, `state`, `run`, `settleMs`) that `shoot <game> --view <name>` and `drive --view <name>` replay exactly, so a before/after pair is the same view by construction. `shoot <game> --list-views` prints what a game declares; an unknown name fails listing them, and any explicit capture flag still wins over the view's value.
|
|
212
|
+
- **`modelLoadIdleMs()` from `@jgengine/shell/render/modelLoad`** reports how long the shared GLB loader has been idle (`0` while a model is in flight). Capture hosts wait on it instead of guessing a settle delay, so an establishing shot no longer races streaming models.
|
|
213
|
+
- **Per-instance state on placed objects** — `SceneObject.state` (opaque `Record`), set through `ctx.scene.object.setState`/`patchState`/`place({ state })`. It survives move/rotate/setVisual, replicates and saves with the object, and round-trips as `RuntimeObjectRow.flags` via `toRuntimeObjectRow`/`fromRuntimeObjectRow` (`@jgengine/core/multiplayer`). A chest's locked flag or a machine's fill level no longer needs a game-owned map keyed by `instanceId`.
|
|
214
|
+
- **`slotInventory` is a working feature** — `ctx.scene.object.slots` gives objects whose catalog entry declares it real per-instance container contents: `layout`/`state`/`count`, `put`/`take`, and `transferIn`/`transferOut` between a player inventory and the object, all validated against the declared slot count, `accepts` kind filter, and stack limits, with neither side mutated on a rejection. Contents live on `SceneObject.slots`, so they replicate and save with the placement.
|
|
215
|
+
- **`loadServerSnapshot` and `persistServerSnapshot` are public** (`@jgengine/convex/server`), and `createGameServerFunctions` also returns `helpers` (`loadSnapshot`, `applyCommand`, `persistSnapshot`, `runCommand`) bound to the same runtimes and auth mode. A host mutation can now pair snapshot work with its own table writes in one transaction; the registered `runCommand` mutation delegates to `helpers.runCommand`, so there is one implementation.
|
|
216
|
+
|
|
217
|
+
### Fixed
|
|
218
|
+
|
|
219
|
+
- **Open storefronts are no longer holes through the building.** `generateBuilding` emitted an open ground-floor store as a lone `storefront` panel with no wall behind it, while every upper-floor window was correctly backed by one. Storefront and window materials render at 0.56 opacity and a generated building is a hollow shell, so those bays let you see straight through the building to the world beyond — and open stores only land on the street-facing facade, so a row of shops along a road was the worst case. The bay now emits its wall first, exactly as an upper floor does.
|
|
220
|
+
- **`joinServer`, `flushSave`, `flushDirtyServers`, and the `auto` autosave can write player profiles again.** All four cleared the snapshot's dirty set before handing it to the persist plan, which gates profile writes on exactly that set — so the functions named "flush" flushed the server row and chunks only, and an `onNewPlayer` grant (starting money, starting items) never reached `jgPlayerProfiles`. `joinServer` now persists the snapshot it just marked; the flush paths mark every player dirty first, since hydrating from storage resets the dirty set. `planServerPersist` also clears `dirtyAt` once everything outstanding is written, so a successful flush stops autosave from re-firing forever, and a profile patch no longer clobbers a host-maintained `dirtyAt`.
|
|
221
|
+
|
|
222
|
+
## 0.15.0
|
|
223
|
+
|
|
224
|
+
### Migrate
|
|
225
|
+
|
|
226
|
+
- **Bump lockstep SDK packages to `^0.15.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.12.0`; `@jgengine/github` is `0.2.0`.
|
|
227
|
+
- **`jgengine create` scaffolds a textured world by default (#1550).** `--world` is now the default and `--no-world` is the opt-out (matching `--no-editor`); `--world` is still accepted. The default scaffold previously emitted untextured proxy geometry, which the AGENTS.md it ships declares a failing result. `create` now also pulls the starter asset packs into `public/models` after install, and never fails the scaffold if that pull cannot run (offline, blocked proxy, `--no-install`) — it prints `npx assets pull starter` in next steps instead, and the emitted AGENTS.md documents the same command. Skip the pull explicitly with `--no-assets`.
|
|
228
|
+
- **Start game code at `@jgengine/shell/gameKit` (#1541).** Prefer that surface for `defineGame`, `GameHost`, stores, systems, authored-scene helpers, and HUD building blocks. `@jgengine/core/authoring` remains for pure core helpers the kit does not re-export but is no longer a competing start-here path.
|
|
229
|
+
- **Every game must now declare its run-phase story — silence is a gate error (#1337).** The run-phase
|
|
230
|
+
contract used to be optional with a permissive default: a game that never published a phase read as
|
|
231
|
+
`"playing"`, so the shell painted the touch dock over title/menu/results screens (Vice Isle #1329).
|
|
232
|
+
`check-game-shape` now fails any game under `Games/*` that neither declares `lifecycle` in
|
|
233
|
+
`defineGame({...})` nor calls `setGamePhase(ctx, phase)` somewhere in `src/`. To satisfy it, a game
|
|
234
|
+
that runs live from boot with no menu/pause/end screens declares `defineGame({ lifecycle: "always-live" })`;
|
|
235
|
+
a game with run states declares a `LifecycleConfig` or calls `setGamePhase` at its
|
|
236
|
+
menu→playing→ended transitions. No runtime behavior changes for a game that already drove phases.
|
|
237
|
+
- **`generateCity` output is plot-first and no longer square by default.** Same options in, but the
|
|
238
|
+
layout changes: lots derive from block-frontage plots (varied sizes), the street net clips to a
|
|
239
|
+
seeded organic outline (`streets: { outline: 0 }` restores the legacy rectangle-filling footprint),
|
|
240
|
+
and the landmark/interior-fill passes are gone — `content.landmarks` now dials the share of grand
|
|
241
|
+
(block-scale) plots and `blockFill` dials frontage coverage/plot depth/park share.
|
|
242
|
+
`ResolvedCityLot` drops the `interior`/`park` flags (no street-less interior lots exist anymore)
|
|
243
|
+
and the `INTERIOR_LOTS_PER_BLOCK_CAP` export is removed; `GeneratedCity` gains `plots` + `parks`.
|
|
244
|
+
A city consumer that only reads `lots`/`lotContent` (center/rotation/footprint/pieces) needs no
|
|
245
|
+
code change.
|
|
246
|
+
- Walking-player object collision is now **on by default** (`PlayerMovementConfig.collideObjects`
|
|
247
|
+
defaults `true`): placed scene objects with blocking colliders stop, support, and are stood upon by
|
|
248
|
+
the player without opting in. A game that relies on walking through placed objects sets
|
|
249
|
+
`movement: { collideObjects: false }` (or gives those objects non-blocking colliders).
|
|
250
|
+
- Ground drops taller than `movement.stepHeight` (default 0.4) now fall under gravity instead of
|
|
251
|
+
snapping the feet down in one frame. Games that teleport-spawned players high above the ground will
|
|
252
|
+
see them fall to it.
|
|
253
|
+
- **Projectile tracers are now opt-in and never drawn for ballistic shots.** `presentationEffects.tracers`
|
|
254
|
+
defaults **off** (every other presentation channel still defaults on), so the shell no longer draws a
|
|
255
|
+
straight muzzle→impact line for a game that never asked for one — `fireProjectile` is a generic seam
|
|
256
|
+
(bullets, bolts, grenades, launchers), and a tracer only reads as a real shot for direct-fire weapons.
|
|
257
|
+
A game that wants bullet tracers now sets `presentationEffects: { tracers: true }`. Even with tracers on,
|
|
258
|
+
arced/exploding shots (anything ballistic — grenades, rocket/grenade launchers) never draw one, because a
|
|
259
|
+
straight line through an arc is a fake beam. The `projectile.settled` event and `ProjectileSettleReport`
|
|
260
|
+
gain a required `ballistic: boolean`; code that emits or consumes them directly must set/handle the field.
|
|
261
|
+
|
|
262
|
+
### Changed
|
|
263
|
+
|
|
264
|
+
- **Prerelease publishing (`0.15.0-next.0`).** Package versions now move together via `bun run version:prerelease` (`bun run check-version` verifies package versions, internal `@jgengine/*` ranges, and the `VERSION` mirror agree). The Publish workflow is `workflow_dispatch`-able from any ref and picks the npm dist-tag from the version, so prerelease builds land on `next` and never move `latest`. Install a prerelease with `npm i @jgengine/core@next`.
|
|
265
|
+
- **SDK remediation Phase 3a — AAA visual/sim ceiling items (#1545).** `ctx.rng` is a per-world seeded stream (loot tables default to it); post chains default to SMAA before tone-map; cinematic IBL is a procedural sky/ground(+sun) probe instead of RoomEnvironment gray box; directional lights support `cascades > 1` via three.js CSM; `stepFlock` no longer shares a module-global spatial grid across worlds.
|
|
266
|
+
- **SDK remediation Phase 1.2 + 2.3 — adoption ratchet + cheap seams (#1543).** `check-game-shape` now freezes module-level `export let` and local `Window`/`Bar`/`Chip`/`Slot` components via shrink-only `scripts/game-adoption-baseline.json` (new smells fail CI). Ships `grounded(ctx, x, z)` (`@jgengine/core/world/grounded`, also on `gameKit`), `DEFAULT_WALK_CODES` (WASD + Space on `@jgengine/shell/shellMovement` / `gameKit`), and a HudTheme-token default skin on `SettingsTrigger` (`SETTINGS_TRIGGER_CLASSNAME`) so bare triggers need no 8×8 className re-authoring.
|
|
267
|
+
- **SDK remediation Phase 0 — one start-here + correct UI guidance (#1541).** `@jgengine/shell/gameKit` is the sole happy-path game entrypoint; `@jgengine/core/authoring` no longer claims "Game code should begin here" (still a core helper barrel for pure exports the kit does not re-export). The whole-game recipe and scaffold `GameUI` header now match CLAUDE.md: compose shipped HUD building blocks (`StatBar`, `Hotbar`, `Coins`, …) and reskin via HudTheme — games own layout/terminology/art direction, not re-derivation. Studio Showcase and Tower Guard import kit-covered symbols from `gameKit`.
|
|
268
|
+
- **Kinematic vehicle drive feel** (#1515) — `createKinematicVehicle` gains a low-speed launch torque floor, softer reverse by default (`chassis.reverseForceScale`, default ~0.48 of engine force), handbrake rear-lock oversteer yaw, engine-braking that no longer fights powered reverse, and lighter ESC while handbraking. Existing chassis tunings pick this up with no migrate; override `reverseForceScale` only if you need the old full-force reverse.
|
|
269
|
+
|
|
270
|
+
### Added
|
|
271
|
+
|
|
272
|
+
- **`advanceStepCadence` (`@jgengine/core/audio/stepCadence`) — distance-driven footstep timing.** Pure state-in/state-out: feed it the ground distance a mover covered this frame and it reports how many steps landed and which foot led, so foley stays locked to distance instead of a wall-clock timer that desyncs the moment speed changes. Configurable stride, per-advance `strideScale` for crouch/sprint, and a shorter first step from rest so walking off is heard immediately.
|
|
273
|
+
- **`resolveMusicState` (`@jgengine/core/audio/musicState`) — combat-reactive soundtrack selection.** The engine could already crossfade themes but nothing decided *when*, so every game re-handrolled the threshold and got a track that flapped as one enemy walked in and out of range. Give it a tier ladder (intensity → theme) and it resolves the theme to play: escalation is immediate, de-escalation waits for the tier's `holdMs` and needs intensity to fall a `release` margin below the tier it is leaving. Pure and serialisable.
|
|
274
|
+
- **Synth voices take a `sustain` (`ToneVoice`/`NoiseVoice`).** Seconds held at full gain before the decay. Default 0 preserves every existing patch. Without it there was no way to author a *sustained* sound at all: every voice decayed to silence inside its duration, so a patch rendered once and looped as an ambient bed pulsed audibly once per loop. Set `sustain` to `duration` for a flat wind, rain, or machine-hum bed that loops seamlessly.
|
|
275
|
+
- **A street race is a section of a city, sealed off.** `extractCircuitRoute(network, rules)` (`@jgengine/core/world/raceCircuit`) lifts a closed lap out of an existing street network — the same asphalt the open world already drives on — and seals every side street leaving it. Returns checkpoints ready for `raceTrack()`, start/finish, grid, corners, kerbs, and sectors. Circuit-mode networks take the same path (their loop edges are the cycle). The city volume kind gains a Race group (`race`, `raceLapLength`, `raceArterial`, …) that resolves a world-space `ResolvedRace` on `ResolvedCity.race`; `CityRenderer` draws the lap surface, kerbs, start/finish, grid, and barrier seals. Playground adds a Street race mode over the same shared slider set.
|
|
276
|
+
- **Parked vehicles and junction signals on a city district.** `CityRules.parkedCars` / `signals` seed curb and pad fleets plus mast-arm signals (arterials) and sign posts (the rest); `ResolvedCity.vehicles` / `signals` hand them to `CityRenderer`.
|
|
277
|
+
- **`ModelMaterialOverride.rim` — a fresnel rim light on any entity/object model.** `{ color, strength, power }` adds a bright edge where the surface turns away from the viewer, patched into the material's own shader so it shadows, fogs, and tonemaps with the body rather than floating in front of it. This is the standard fix for an actor that shares a value band with its ground: no albedo choice separates a body from terrain lit by the same key, but an edge does. Omit for no rim (default).
|
|
278
|
+
|
|
279
|
+
### Fixed
|
|
280
|
+
|
|
281
|
+
- **Noise voices longer than the shared noise buffer no longer go silent partway through.** The 1-second buffer is sampled from a random offset, so any `NoiseVoice` whose duration exceeded the remainder ran off the end and finished in silence — invisible on short impact bursts, fatal for anything sustained. The source now wraps; the voice still ends on time.
|
|
282
|
+
|
|
283
|
+
- **CityRenderer junctions use the welded carriageway-union seam end-to-end.** District roads go through `buildTrimmedIntersections` + `buildIntersectionMarkings` with the same compact opts as StreetGeometryPreview / playground (`curbReturnRadius: 2`, `apronMargin: 0.25`). Plaza hull aprons, fallback discs, and fat zebra ladders are gone; race resurfacing stops at junction mouths so welded patches stay visible.
|
|
284
|
+
- **Denser urban fabric for metro/Manhattan districts.** Commercial class setbacks/spacing tighten toward a streetwall; slabs/towers pick L-plans and mid-block courts; dense blocks push footprints to the curb and paint courtyards as hard courts rather than lawn leftovers.
|
|
285
|
+
- **City and track are one slider surface again.** `CityRules` and the editor `generate_streets` bake now forward every `StreetNetworkRules` dial (`elevation`, `maxGrade`, `compactness`, `sidewalkWidth`, `outline`, `residentialBranches`); `check-street-rule-parity` fails CI if either drifts. Playground no longer hides layout sliders when you switch modes — modes are presets over one shared set.
|
|
286
|
+
- **City facades dissolve their window stripes before they alias into speckled static**, and ground floors now read as storefronts rather than repeating floor 30. Block interiors paint developed fabric instead of raw lawn, and crosswalks only land at real junctions.
|
|
287
|
+
- **`terrain({ detail: { material: { tint } } })` separates a ground texture's grain from its hue.** A detail material's colour map used to replace the terrain's authored colour outright, so the biome palette, macro sweeps, and slope-rock blend under it were painted over and a whole region flattened to the texture's single hue — despite the API documenting that it "blends with, never replaces". `tint` defaults to 1 (the existing behaviour, so no shipped game changes); drop it toward 0 to keep the authored colour and take only the map's light and shade.
|
|
288
|
+
- **`sky({ gradientExponent, cloudiness })` are now authorable — and `sky()` no longer silently drops them.** `gradientExponent` controls how fast the gradient reaches `zenithColor` off the horizon (default 0.65, unchanged): below 1 pulls the zenith hue down into the frame, which is what an eye-level camera in flat terrain actually needs — at the default the sky reads as one horizon-coloured wash however saturated the authored zenith is. `cloudiness` (default 0.35, unchanged) is the procedural cloud-band opacity.
|
|
289
|
+
- **A model that throws while rendering is now reported instead of vanishing (#1588).** `ModelFallbackBoundary` swapped in its fallback (usually nothing) and stayed silent: `probes.fallbacks` stayed empty, no warning was logged, and an entity simply disappeared. It now reports a new `renderError` fallback cause on every failed render and warns once with the model url and the caught error.
|
|
290
|
+
- **A failing or slow-loading piece of a part composition no longer erases the whole actor (#1588).** Parts, bone attachments, and material-map appliers each mount behind their own error/Suspense boundary, so one bad piece drops itself — the base mesh and healthy siblings still render.
|
|
291
|
+
- **`EntityModel` / `useModelAnimation` / `PartMotionRig` no longer require a running game (#1588).** They read the game context optionally, so `EntityPreview` renders a model in a character screen, inspector, or preview page instead of throwing `useGameContext must be used within <GameProvider>` and rendering nothing.
|
|
292
|
+
- **A model `animation` config naming clips the rig does not have now warns instead of silently posing wrong.** An explicit `ModelConfig.animation` always beats `"auto"`, and a missing state clip fell back to the GLB's *first* clip while missing one-shots were dropped — so a mistyped or wrong-pack clip name left an actor looping an arbitrary animation (on a KayKit rig, `1H_Melee_Attack_Chop`) with no death or hit reaction, and nothing in the logs. `useModelAnimation` now warns once per mount naming the missing clips plus the rig's real clip names, and points at `animation: "auto"`. No behavior change for a config whose clips all resolve.
|
|
293
|
+
- **City-scale player movement no longer freezes `pose` at ~3 fps (#1517).** Broadphase reach is split into capped horizontal vs vertical extents (tower height no longer inflates XZ/Y queries), `objectStore.inBox` bails to a linear object scan when the 1 m cell volume is huge, dense mesh-box colliders collapse to outer AABBs for walking, and `movement.frozen` skips the gather (seated drivers).
|
|
294
|
+
- **Generated intersections are compact carriageway unions, not plaza discs (#1511).**
|
|
295
|
+
Apron pull-back is the projected crossing half-width only (opposite through-arms are ignored; curb
|
|
296
|
+
return is no longer double-counted into the mouth distance). Exterior corners get small width-clamped
|
|
297
|
+
returns; T far sides stay straight through-curbs; degree-2 turns form L-unions via the outer edge
|
|
298
|
+
intersection. Sidewalk aprons offset along the local ring normal (not radially from the node), so
|
|
299
|
+
corners no longer become giant octagonal bands. `buildIntersectionMarkings` still joins degree-2
|
|
300
|
+
offsets and places stop lines at multi-arm mouths. Playground inspection hides buildings and does not
|
|
301
|
+
render cuboid traffic. Deterministic fixture: `bun run shoot --fixture StreetGeometryPreview`.
|
|
302
|
+
- **Managed capture no longer steals Windows focus or waits on a background supervisor.** Daemon start
|
|
303
|
+
launches hidden Chrome directly, while Git, taskkill, Vite, Chrome, and ffmpeg subprocesses all suppress
|
|
304
|
+
console windows. Website query state hydrates before Three.js boot, preserving fail-fast route capture.
|
|
305
|
+
Plot frontage is re-resolved after block-corner subdivision so buildings retain the correct street, and
|
|
306
|
+
`lots.variety` now controls the deterministic plot-tier mix.
|
|
307
|
+
- **`extractBlocks` warns (dev-mode) instead of silently collapsing on wandered / arc-filleted streets (#1502).** Proximity welding assumes near-aligned centerlines; wandered arc-filleted streets weld into a degenerate face set (a real case went 38 streets → 2 faces with no signal, so the block fabric looked adoptable but was empty). `extractBlocks` now emits a `console.warn` when the extracted face count is implausibly low for the street count (≥8 streets yielding fewer than street-count/4 faces), pointing callers at `extractGraphBlocks` — which traces faces from the exact street graph and is robust against the centerlines that defeat welding. No-ops in a production build; no runtime behavior change.
|
|
308
|
+
- **`assets pull` / `assets add` default output dir now lands where the dev server serves models** (`@jgengine/assets` CLI, #1339) — inside the monorepo a bare `pull`/`add` previously wrote to a cwd-relative `public/`, so running it under `packages/assets` (or any subdir) dropped GLBs into a folder no game serves. It now defaults to the served root `apps/dev/public` when that exists (falling back to the historical cwd-relative `public` for out-of-monorepo consumers), so pulled bytes land in `apps/dev/public/models/<pack>` where the runner reads them. `--dir` still overrides.
|
|
309
|
+
- **The missing-model diagnostic now names the served target dir, and a bare `assets reindex` no longer disagrees with `assets pull` (#1499).** The runtime 404 / HTML-fallback diagnostic (`classifyAssetResponse` in `@jgengine/core/scene/assetDiagnostics`) told you to run `assets pull <source>` but never named where the bytes must land; it now points at the dev server's served models dir (`apps/dev/public/models` in the monorepo) and states no manual copy is needed. Completing #1339, a bare `assets reindex` / `assets reindex-sprites` (no dir arg) now defaults to that same served root via `resolveDefaultReindexDir` instead of a cwd-relative `public/`, so it can never scan an empty dir while `pull` writes to the served one. Explicit dir args are unchanged.
|
|
310
|
+
|
|
311
|
+
### Added
|
|
312
|
+
|
|
313
|
+
- **An `rng` that falls back to `Math.random` now says so (#1582)** — twelve core primitives (`createMobBrain`, `applyStatus`, `rollCheck`, `autoTarget`, `rollCapture`, `createLootRegistry`, `createRunDraft`, `createResourceNode`, `worldItem`, `selectLane`, `assignRoles`, `scheduleDelivery`) took an injectable `rng` and defaulted it silently, so a caller who forgot to pass `ctx.rng` got a simulation that could not replay, lockstep, roll back, or be pinned by a test — with no signal. They now route through `resolveRng` (`@jgengine/core/random/resolveRng`), which warns once per call site in dev and stays silent in production. The fallback itself is unchanged, so no behavior moves; four sites whose `rng` was a defaulted parameter now take it as optional, which is source-compatible.
|
|
314
|
+
- **The two parts a game front-end could not reach for (#1577)** — `useMenuRouter(root)` (`@jgengine/react/menuRouter`) is the screen stack behind title → save select → settings → credits: `open`/`back`/`replace`/`reset`, `path`, and Escape-to-back on by default; `menuStackReducer` exposes the same transition as a pure function. For credits, `creditsForSourceIds(ids)` (`@jgengine/assets/credits`) turns the pack ids a game already declares to `buildCatalog` into licensed, attributed sections, `mergeCredits`/`groupCredits` (`@jgengine/core/game/credits`) fold and shape them, and `CreditsScreen` (`@jgengine/react/creditsScreen`) renders a `CreditsDocument` HudTheme-skinned. Attribution is now derived from what the game ships instead of hand-maintained.
|
|
315
|
+
- **`ResourcePool` and `ComboPoints` can hand their state back (#1575)** — both gain `snapshot()` / `restore(state)` over a plain-data `ResourcePoolSnapshot` / `ComboPointsSnapshot`, so a save file, a server host, or a test can carry them across a rebuild. `ResourcePool` also gains `retune({ max, regenPerSecond, decayPerSecond })`, so a buff or difficulty change no longer needs a fresh pool (regen/decay were frozen at construction). Additive: existing calls are unchanged.
|
|
316
|
+
- **`HudTheme` now covers the whole `--jg-*` vocabulary (#1573)** — new `surface`, `status`, `font`, and `rarity` groups plus `-deep`/glow palette companions, so `hudThemeVars(theme)` emits all 53 tokens the shipped components read instead of the 28 the bars/frames/slots needed. One theme spread on a HUD ancestor now skins the `@jgengine/react` primitives and the `registry/jgengine` blocks together; previously a game had to author two disjoint token sets. New `hudTheme({ ... })` fills a theme from per-group partials, and `deriveHudTheme({ accent, surface })` builds a coherent full theme (including frames, troughs, and slots) from a seed pair. `ember` / `synthwave` / `fieldkit` join `HUD_THEME_PRESETS` with their registry values unchanged. Additive: existing themes and every rendered surface are unaffected.
|
|
317
|
+
- **Runtime-mutable arcade car tuning + hops (#1550)** — `KinematicVehicle.retune(tuning)` swaps a vehicle's whole tuning block while keeping position, heading, velocity and gear state, for vehicles whose stats change during play (installed parts, damage, debuffs). New tuning fields `hopGravity` (enables `vehicle.hop(impulse)`, reported as `step.airOffset` / `step.airborne` and applied on top of the ground sample by `tickDrivableVehicle`), `coastDeceleration` (flat coast road-load, versus `rollingResistance`'s proportional decay), and `reverseForceScale` (reverse push on the arcade path, previously reachable only via a full `chassis` block). New per-tick modifier `brakeScale`. All additive: omit them and every existing vehicle behaves exactly as before.
|
|
318
|
+
- **`assets pull starter` (#1550)** — one meta-id that pulls every pack the starter catalog needs (`kaykit-adventurers`, `kaykit-dungeon`, `kaykit-furniture`, `kaykit-city-builder`, `quaternius-stylized-nature`) into `public/models`, so a game does not have to name five pack ids to make `asset:` references resolve.
|
|
319
|
+
- **`CastConfig.moveDeadzone` (#1550)** — per-tick movement at or below this is treated as the caster's noise floor and not accumulated toward `moveTolerance`. A 3D game's pose jitters every frame from terrain-following ground snap, and because `moveTolerance` accumulates, feeding those raw deltas in silently cancels casts the player never interrupted. Declare the noise floor once in `begin` instead of filtering at every `tick` call site. Defaults to `0`, so existing callers are unchanged.
|
|
320
|
+
- **`LootDrop` + `lootDropsOf(data)` (#1550)** — the granted-reward row is now a named type, and `lootDropsOf` safely reads the drops out of a `loot.granted` payload that arrived as `unknown` (a `FeedEntry.data`, a replicated event, a persisted row), returning `[]` for anything not shaped like a loot grant and discarding malformed rows. Replaces the `entry.data as { drops?: ... }` cast that four call sites — three games plus `ToastStack`'s own default renderer — were each writing by hand.
|
|
321
|
+
- **`createRegenShield({ pool })` — stat-backed shields (#1550).** A `RegenShieldPool` port (`current`/`max`/`set`) lets a regen shield read and write storage the game already owns — an entity stat, a replicated store — instead of keeping the value privately, which is what blocked adoption in games whose shields live in the stat system that damage, HUD and replication all read. A pool-backed shield also detects *external* decreases between ticks and restarts its regen grace period, replacing per-entity snapshot bookkeeping. Adds `noteDamage()` (restart the grace period without moving the value, for hits the shield cannot see) and per-tick `tick(dt, { regenPerSecond, regenDelayMs })` rate overrides for talent- or buff-derived rates. The grace period now advances on simulated `dt` rather than wall clock, so a paused game no longer regenerates.
|
|
322
|
+
- **`ctx.rng` + `createGameContext({ seed, rng })` (#1545)** — deterministic per-world `[0,1)` stream for sim paths; same seed → same sequence across worlds/hosts.
|
|
323
|
+
- **Post `aa: "smaa" | "msaa" | false` (#1545)** — SMAA default on post chains (before OutputPass) for alpha-tested edge crawl.
|
|
324
|
+
- **Procedural sky IBL + directional `cascades` CSM (#1545)** — `EnvironmentLighting` bakes sky/ground/sun into PMREM; `cascades > 1` on directional lighting mounts cascaded shadows.
|
|
325
|
+
- **`grounded(ctx, x, z)` (#1543)** — returns `[x, groundY, z]` from the live world surface; stop re-deriving `[x, ctx.world.groundHeightAt(x, z), z]`. Import from `@jgengine/core/world/grounded` or `@jgengine/shell/gameKit`.
|
|
326
|
+
- **`DEFAULT_WALK_CODES` (#1543)** — stock WASD + Space map for the shell walk controller (`@jgengine/shell/shellMovement`, re-exported from `gameKit`).
|
|
327
|
+
- **`SETTINGS_TRIGGER_CLASSNAME` + default `SettingsTrigger` skin (#1543)** — HudTheme-token 8×8 chrome button; omit `className` for the stock look.
|
|
328
|
+
- **City generation now enforces plot clearance and planar streets (#1454).** Building plots clear
|
|
329
|
+
one another and street corridors, building massing fits its plot, landmark footprints avoid roads
|
|
330
|
+
and overlap, and generated street chords and branches cannot cross without a junction. Interior
|
|
331
|
+
arterial stubs are demoted, dead ends receive cul-de-sac bulbs, and the playground exposes a
|
|
332
|
+
linkable plan view plus plot spacing and variety controls.
|
|
333
|
+
|
|
334
|
+
- **`lifecycle: "always-live"` sentinel + `GameLifecycle` type (#1337).** `defineGame`'s `lifecycle`
|
|
335
|
+
now accepts the string sentinel `"always-live"` alongside a `LifecycleConfig`. It is an explicit,
|
|
336
|
+
truthful declaration that the game has no menu/pause/end screens and runs live from boot — runtime
|
|
337
|
+
behavior is identical to declaring no lifecycle at all (no phase sync, no start/restart commands),
|
|
338
|
+
but the intent is now *stated*, not implied by silence. `defineGameDefinition` resolves the sentinel
|
|
339
|
+
to `undefined`, so downstream consumers still only ever see a real `LifecycleConfig` or nothing.
|
|
340
|
+
`@jgengine/core/gameplay` exports the `GameLifecycle` union type. Pairs with the new
|
|
341
|
+
`check-game-shape` run-phase requirement (see Migrate).
|
|
342
|
+
- **`debug_snapshot().probes.textureErrors` surfaces in-GLB texture-load failures (#1342).**
|
|
343
|
+
`@jgengine/core/devtools/textureErrors` adds an allocation-aware collector
|
|
344
|
+
(`armTextureErrors`/`reportTextureLoadError`/`resetTextureErrors`/`textureErrorsSnapshot`) that the
|
|
345
|
+
shell's shared GLB loading manager feeds on every texture/image `onError`. Previously only whole-model
|
|
346
|
+
catalog fallbacks (missing mapping/pack/scene) reached the `fallbacks` probe, so a model that resolved
|
|
347
|
+
but whose textures 404'd read as a clean scene to `debug_snapshot` — the exact signal `jgengine-verify`
|
|
348
|
+
tells agents to trust. The new `probes.textureErrors` list (`{ url, count }[]`) makes those failures
|
|
349
|
+
visible so a texture-404'd scene can be treated like a model fallback. Dev-only (armed with devtools);
|
|
350
|
+
a pure no-op in production.
|
|
351
|
+
- **Result/option types of public barrel functions are now re-exported (#1319).** The
|
|
352
|
+
`@jgengine/core/gameplay` barrel re-exports `ChargeResult`, `ChargeOptions`, and `Overdraft`
|
|
353
|
+
alongside `charge`/`chargeAll`/`canAfford`, and `@jgengine/core/combat` re-exports
|
|
354
|
+
`DefenseResolution` (from `resolveDefense`) and `ResolvedShot` (from `resolveShot`). Consumers can
|
|
355
|
+
now name these return/parameter types directly instead of resorting to `ReturnType<>`/`Parameters<>`
|
|
356
|
+
gymnastics. Purely additive — no runtime or signature change.
|
|
357
|
+
- **Conflict-aware key-rebinding session.** `@jgengine/core/input/rebindSession` adds
|
|
358
|
+
`createRebindSession({ actions | input, overrides?, now? })` — an observable key-remap
|
|
359
|
+
editor over the existing action-binding model: it tracks the effective binding per action
|
|
360
|
+
(default merged with `applyBindingOverrides`), exposes `rows()` with key glyphs + per-row
|
|
361
|
+
conflict sets, groups every clash via `conflicts()`, drives click-to-capture rebinds
|
|
362
|
+
(`beginCapture`/`capture`/`cancelCapture`, codes normalized), resets to defaults, and hands
|
|
363
|
+
back `BindingOverrides` to persist, with `snapshot`/`restore`. React `KeybindingMenu` +
|
|
364
|
+
`useRebindSession` (`@jgengine/react`) are the drop-in controls-settings surface — one row
|
|
365
|
+
per action with key glyph, conflict badges, per-row and Reset-all, HudTheme-skinnable.
|
|
366
|
+
Demo: `key-rebinding`.
|
|
367
|
+
- **Vendor / shop stock + grid.** `@jgengine/core/economy/shopStock` adds a serializable, observable
|
|
368
|
+
`createShopStock({ entries })` — entries carry a free-string `kind`, a `price` in a free-string
|
|
369
|
+
`currency`, a finite or unlimited (`qty: null`) count, and an optional `sellPrice`. `buy`/`sell`
|
|
370
|
+
operate over a **caller-owned** `WalletState` (reusing the existing `wallet` model — `charge`/`grant`/
|
|
371
|
+
`canAfford`), returning the debited/credited wallet for the caller to adopt; plus `restock`/`setPrice`/
|
|
372
|
+
`add`/`remove`/`list`/`get`/`canAfford`/`subscribe`/`snapshot`/`restore`. `@jgengine/react`'s `ShopGrid`
|
|
373
|
+
host + `useShopStock` hook render it as a token-themed grid of item cards (icon, price with currency
|
|
374
|
+
glyph, stock count or "∞", afford-aware Buy, optional Sell) with a wallet balance readout.
|
|
375
|
+
|
|
376
|
+
- **Save-slot / profile select menu.** `@jgengine/core/game/saveSlots` adds `createSaveSlots(config)` — a
|
|
377
|
+
serializable, observable index of per-slot *display* metadata (`{ id, name?, empty, savedAt?, meta }`
|
|
378
|
+
with free-string `meta` the game fills: level, playtime, chapter, thumbnail ref, …) that complements
|
|
379
|
+
`createSaveStore` (which owns the real payload). Ops: `write`/`clear`/`rename`/`get`/`list`/`mostRecent`
|
|
380
|
+
(powers Continue) plus `subscribe`/`snapshot`/`restore`. `@jgengine/react/saveSlots` ships the drop-in
|
|
381
|
+
`SaveSlotMenu` host (+ `useSaveSlots` hook) rendering the index as New / Continue / Load / Delete cards
|
|
382
|
+
with meta chips and relative save times, HudTheme-skinnable. Demo: `save-slots`.
|
|
383
|
+
- **Event-log / kill-feed ticker.** `@jgengine/core/game/eventTicker` adds a thin, serializable,
|
|
384
|
+
observable `createEventTicker({ now?, limit?, ttlMs? })` over the existing `appendFeed`/`pruneFeed`
|
|
385
|
+
helpers: a single rolling, count-capped, time-fading list of free-string `{ kind, text, icon? }`
|
|
386
|
+
entries (`push`/`recent`-with-`fade`/`entries`/`clear`/`subscribe`/`snapshot`/`restore`). `recent()`
|
|
387
|
+
prunes expired entries and returns them newest-first with a `fade` `0..1` (age / `ttlMs`). The React
|
|
388
|
+
`KillFeed` host + `useEventTicker` hook render it as a fading, newest-on-top stack of per-kind
|
|
389
|
+
iconned, accent-colored rows over HudTheme tokens; `kill-feed` demo included.
|
|
390
|
+
- **Interaction prompt registry ("Press E to …").** `@jgengine/core/world` adds `createPromptRegistry()` — a thin
|
|
391
|
+
observable, serializable store over the existing `resolveActivePrompt` resolver that owns positioned proximity
|
|
392
|
+
prompts (`register`/`update`/`unregister`/`clear`/`all`), resolves the nearest in-range prompt as the player moves,
|
|
393
|
+
and notifies subscribers only when the active prompt *changes* (so a HUD does not thrash per frame), with
|
|
394
|
+
`snapshot`/`restore`. React `@jgengine/react` adds `InteractionPrompt` (a screen-anchored callout rendering the active
|
|
395
|
+
prompt — key cap + label, gauge hold bar, or plain label, theme- and per-prompt-accent skinnable) and
|
|
396
|
+
`useInteractionPrompt(registry, playerPosition)`. Demo: `interaction-prompt`.
|
|
397
|
+
- **Seeded trauma-based camera shake.** `@jgengine/core/vfx/cameraShake` adds `createCameraShake(config?)` —
|
|
398
|
+
a serializable, deterministic camera-shake/impulse controller: `add(amount, kind?)` raises trauma `0..1`
|
|
399
|
+
on impacts (free-string `kind` the game styles), `update(dt)` decays it, and `offset()` returns a pooled
|
|
400
|
+
`{ x, y, z, pitch, yaw, roll }` kick (`trauma^exponent` × per-axis maxima × seeded value-noise) with
|
|
401
|
+
snapshot/restore. `@jgengine/react` ships `CameraShakeMeter`/`useCameraShake` (trauma meter + kind label)
|
|
402
|
+
and `@jgengine/shell` ships `ControllerCameraShake`, an R3F consumer that applies the offset to the active
|
|
403
|
+
camera each frame so the view visibly shakes. Demo: `camera-shake`.
|
|
404
|
+
|
|
405
|
+
- **Observable wave/spawn runner + drop-in HUD.** `@jgengine/core/ai/waveRunner` adds
|
|
406
|
+
`createWaveRunner(config)` — a thin, stateful, observable wrapper over the seeded `spawnDirector`
|
|
407
|
+
that owns a `SpawnDirectorState`, ticks it from `update(dt, ctx?)`, forwards each `SpawnRequest` to an
|
|
408
|
+
optional `onSpawn` sink (so the model never instantiates entities), and exposes a pooled `view()`
|
|
409
|
+
readout (1-based `WAVE N`, wave progress `0..1`, budget/alert, spawned-this-wave/total, done) plus
|
|
410
|
+
`forceNextWave`/`raiseAlert`/`subscribe`/`snapshot`/`restore`. `@jgengine/react/waveHud` adds
|
|
411
|
+
`WaveHud`/`useWaveRunner` — a theme-skinnable panel with a big WAVE N label, wave-progress bar, and
|
|
412
|
+
spawn/budget/alert readouts. Spawn-entry kinds stay free strings the runner never interprets.
|
|
413
|
+
- **Count-based combo / multiplier meter.** `@jgengine/core/combat/comboMeter` adds
|
|
414
|
+
`createComboMeter({ windowMs, tiers?, dropStep?, multiplierPerTier? })` — an integer hit chain that
|
|
415
|
+
climbs on `hit(kind?)`, resets a decay window each hit, and drops (to 0, or by `dropStep`) when the
|
|
416
|
+
window elapses, driven by an injected `now` and/or `update(dt)`. Free-string `tiers` derive the active
|
|
417
|
+
`tier()` and a score `multiplier()`, with `peak()`, a pooled `view()`, `subscribe`, and
|
|
418
|
+
`snapshot`/`restore`. React `@jgengine/react/comboMeter` ships `ComboMeterHud` (big live count, tier
|
|
419
|
+
label, draining window bar, multiplier — per-tier colored from a caller map) and a `useComboMeter` hook.
|
|
420
|
+
- **Off-screen objective / waypoint markers.** `@jgengine/core/ui/screenMarkers` adds a serializable,
|
|
421
|
+
observable `createWaypointTracker()` (`set`/`remove`/`clear`/`all`/`subscribe`/`snapshot`/`restore`,
|
|
422
|
+
free-string `kind`s the game styles) plus a pure, allocation-aware `layoutScreenMarker(projection,
|
|
423
|
+
viewport, options?)` that passes an on-screen point through and clamps an off-screen or behind-camera
|
|
424
|
+
point to the viewport edge with a bearing `angle` — the edge-clamp/arrow half that `layoutEntityFrames`
|
|
425
|
+
culls. `@jgengine/react`'s `WaypointMarkers` renders on-screen pins and off-screen directional arrows
|
|
426
|
+
with distance labels over any caller-owned `project` (e.g. shell `useWorldProjection`), skinnable via
|
|
427
|
+
HudTheme tokens and a per-`kind` color map. Demo: `waypoint-markers`.
|
|
428
|
+
|
|
429
|
+
- **Scoreboard / leaderboard ranking.** `@jgengine/core/game/leaderboardRank` adds `rankLeaderboard(rows, options)`
|
|
430
|
+
— a pure, allocation-bounded selector that turns raw leaderboard rows (accepts `LeaderboardRow[]` straight from
|
|
431
|
+
`createLeaderboard().snapshot()`) into a render-ready ranked table: stable value sort (`desc`/`asc`), correct tie
|
|
432
|
+
handling (`standard` → 1,2,2,4; `dense` → 1,2,2,3), `isTie`/`isLocal` flags via `highlightUserId`, and top-N `limit`
|
|
433
|
+
— plus `medalFor(rank)` returning free-string `gold`/`silver`/`bronze` podium tokens. `@jgengine/react`'s reskinnable
|
|
434
|
+
`Scoreboard` table renders it with medal-colored podium icons, a highlighted local row, and HudTheme `--jg-*` tokens.
|
|
435
|
+
- **Talent/upgrade tree from any unlock rule.** `@jgengine/core/game/talentTreeView` adds
|
|
436
|
+
`talentTreeViewFrom(nodes, status, totals?)` — a general builder that places a node graph (branch/tier
|
|
437
|
+
layout, prerequisite edges, learned/available/locked/maxed state) from a caller-supplied per-node
|
|
438
|
+
`{ rank, allocatable }`, so unlocks can come from a currency threshold, a level, a quest flag, or
|
|
439
|
+
nothing — not only point-spend. `talentTreeView(nodes, tree)` is now the point-spend adapter over it.
|
|
440
|
+
The React `TalentTree` widget accepts a precomputed `view` (plus `showPoints`) alongside the existing
|
|
441
|
+
`nodes`+`tree`, so the same widget renders a buy-with-points talent tree *or* a money-gated upgrade tree
|
|
442
|
+
with no renderer change.
|
|
443
|
+
- **Floating combat text / damage numbers.** `@jgengine/core/ui/floatingText`' `createFloatingTextField` is a
|
|
444
|
+
genre-agnostic, deterministic, allocation-aware field of world-anchored text pops (damage, crits, heals, XP/gold,
|
|
445
|
+
status, barks): `emit({ position, text, kind?, color?, size?, rise?, drift?, lifetime? })`, `update(dt)` (rise +
|
|
446
|
+
seeded drift + fade + swap-remove reaping), `active()` views, a capped pool that recycles the oldest, `clear`,
|
|
447
|
+
`subscribe`, and serializable `snapshot`/`restore`. `kind` is a free string the presentation styles — no combat
|
|
448
|
+
coupling. New `@jgengine/react`: `layoutFloatingText` (pure project/cull/depth-sort over the `EntityFrames`
|
|
449
|
+
projector seam) and `FloatingText` (per-`kind` skinnable overlay). New `@jgengine/shell/vfx/WorldFloatingText`
|
|
450
|
+
binds the live R3F camera and advances the field. New `floating-text` demo.
|
|
451
|
+
- **Codex / bestiary.** `@jgengine/core/game/codex`' `createCodex` tracks discovery over a fixed set of
|
|
452
|
+
defined entries — `discover`, `isDiscovered`, category-filtered `list`, `categories`, `discoveredCount`/
|
|
453
|
+
`total`/`completion`, an `onDiscover` seam, secret masking, and serializable `snapshot`/`restore`; the view
|
|
454
|
+
list keeps a stable identity for React. New `@jgengine/react` `Codex` gallery: category tabs, discovered vs.
|
|
455
|
+
locked cards (secret entries masked until found), and a completion header. First adopter: the apps/dev
|
|
456
|
+
`codex` demo.
|
|
457
|
+
- **Generated street elevation** (`@jgengine/core/world/streetGenerator`) — new optional `elevation` (0..1 relief dial, default 0 = flat, byte-identical output) and `maxGrade` (default 0.07) rules: a seeded smooth field emits per-point `Street.heights` (grade-capped, loop-continuous across a circuit's start/finish) and a shared `StreetNetwork.elevationAt(x,z)` so renderers drape roads, junction welds, sidewalks, and building bases off one consistent surface. Distinct from `context.heightAt` terrain (bridges/tunnels unchanged).
|
|
458
|
+
- **Per-corner radius classes on circuits** (`@jgengine/core/world/streetGenerator`) — circuit corners now fillet from a seeded radius mix (hairpins near `minCurveRadius`, standard corners at 2-4x, 1-3 sweepers per lap at 5-8x, inversely correlated with turn magnitude) so track layouts show genuinely different corner radii instead of one pinched minimum.
|
|
459
|
+
- **`compactness` circuit dial — space-filling kart-style tracks** (`@jgengine/core/world/streetGenerator`) — 0..1 (default 0 = the open flowing hull loop, byte-identical): rising compactness lays the lap as the wall-follower cycle around a seeded direction-biased spanning tree on a corridor grid — provably one self-avoiding loop that folds back through its own footprint with parallel corridors at a clearance-safe pitch, switchback mazes, leaf hairpins, and a ≥3-pitch main straight — then runs the same spline/curvature-floor pipeline. At 1 a default-seed lap triples in length with 19-37 corners and ~61-77% footprint usage.
|
|
460
|
+
- **`blockFill` density dial** (`@jgengine/core/world/cityGenerator` / `buildingLots`) — 0..1 (default 0.45): rising fill packs street frontage toward a touching streetwall, deepens plots toward the block spine, and reserves fewer whole blocks as parks — Manhattan-dense cities at 1, sparse suburbs near 0. Block interiors are never filled with street-less buildings; every plot fronts a street.
|
|
461
|
+
- **Plot-first city generator** (`@jgengine/core/world/cityGenerator`) — `generateCity` now extracts closed blocks from the street network's exact graph (`extractGraphBlocks` in `world/cityBlocks`) and subdivides every block's frontage into size-tiered polygonal PLOTS (`small`/`medium`/`large`/`grand` — many different plot sizes per city) via the new `deriveCityPlots`. `GeneratedCity` gains `plots` (`CityPlot[]`, `plots[i]` pairs with `lots[i]`) and `parks` (whole-block open space). The landmark cluster-merge pass and the street-less interior fill are gone: a landmark is just a grand plot (same pass, biggest tier — `content.landmarks` dials its share, `0` disables), building class now follows plot size (wide plots pull towers/slabs, narrow plots pull rowhouses/houses), and every building stands on street-fronting land by construction.
|
|
462
|
+
- **`outline` street dial** (`@jgengine/core/world/streetGenerator`) — 0..1 (default 0 = the legacy full-rectangle footprint, byte-identical): rising values clip the lattice to a seeded organic boundary blob (radial harmonics), so every seed grows a differently-shaped city — lobes, bays, shaved corners — reduced to its largest connected component. `generateCity` defaults it to 0.4.
|
|
463
|
+
- **Residential branch streets and cul-de-sacs** (`@jgengine/core/world/streetGenerator` / `cityGenerator`) — new `residentialBranches` rule (0..1 share, default 0 = legacy alleys byte-identically) grows two spur kinds off the mains under the `branching` dial: service alleys (the legacy lane stubs) and RESIDENTIAL BRANCHES — winding 1–3-segment street-level side streets that may fork once, never cross existing roads, end in a turning-bulb cul-de-sac, and are lined with small/medium house plots down both sides by the plot pass (dead-end corridors carry their street/level through `extractGraphBlocks`). Dead-ending chains are never classified arterial, so no bulb-capped boulevards. `generateCity` defaults `branching` to 0.4 and `residentialBranches` to 0.6.
|
|
464
|
+
- **`TrimmedIntersections.junctionIndices`** (`@jgengine/core/world/roads`) — each welded junction surface now reports which input junction it belongs to, so renderers can color crossing patches by the junction's street level instead of one global junction color.
|
|
465
|
+
- **`trimBandAtJunctions`** (`@jgengine/core/world/roads`) — clip a sidewalk/parallel band polyline out of every junction apron (arm-derived radius widened by the band's half-width + clearance), returning the surviving sub-paths — so sidewalks end at crossings instead of sailing through them. The playground consumes it for `Street.sidewalks`.
|
|
466
|
+
- **Shared map drawings (multiplayer).** `@jgengine/core/world/sharedAnnotations`' `createSharedAnnotations`
|
|
467
|
+
makes a map annotation layer collaborative: local `addStroke`/`addShape`/`addNote`/`remove`/`clear` apply
|
|
468
|
+
then broadcast a serializable edit over the party feed (the same seam `createPingSystem` pings ride), and
|
|
469
|
+
`apply` mirrors inbound edits from other players while dropping our own echoes. Ids are globally unique per
|
|
470
|
+
client (`owner:n`) so two players' strokes never collide; transport-agnostic (`ANNOTATION_FEED_ACTION` +
|
|
471
|
+
any replicated feed). Pings were already shared via `createPingSystem`; this completes the pair. Verified by
|
|
472
|
+
a two-client sync test.
|
|
473
|
+
- **Terrain surface shading + layered grass wind** (`@jgengine/shell/terrain`, #1373) — plain (untextured) ground now reads as terrain instead of a two-tone gradient: `buildGroundGeometry`-based grounds (`ProceduralGround`, field ground) gain slope-aware darkening/desaturation and seeded, deterministic hash mottling, tunable via the new optional `TerrainSurfaceColorOptions` (`FieldGroundOptions.surface`; defaults in `DEFAULT_TERRAIN_SURFACE_COLOR`, strengths `0` restore the old flat lerp). Grass wind gains a layered rolling gust field (`GrassWindOptions.layered`, default on — three phase-offset directional waves so visible fronts sweep the meadow; `false` restores the single sine) plus a warm lit-tip sheen, and the default grass distance fade widens from 35–95 m to 55–150 m so meadows read expansive at no extra instance cost. `createProceduralGroundGeometry` now delegates to the shared ground builder (behavior-identical geometry, minus the duplication).
|
|
474
|
+
- **Debounced control commits: `useDebouncedCommit`** (`@jgengine/react/useDebouncedCommit`, #1372) — a live-mirrored, trailing-debounced commit binding for slider/number/color/text controls whose commit path is expensive (scene-document patches that regenerate streets, scatter, or grass). The control renders from the local mirror for instant thumb/readout feedback; the real commit fires once per pause (default 180 ms) and flushes on release/blur/unmount; external changes (undo, RPC echo) resync the mirror unless a drag is in flight. The editor's inspector fields (`SchemaInspector`, `SliderRow`/`NumberField`, vegetation and scatter-coverage density) and the shell settings sliders now commit through it, so one drag lands one patch instead of ~50.
|
|
475
|
+
- **Stage / objective banner.** `@jgengine/core/ui/objectiveBanner`' `createObjectiveBanner` is a genre-agnostic,
|
|
476
|
+
serializable announcement queue: a game calls `announce({ title, subtitle?, kind?, holdMs?, inMs?, outMs? })` and the
|
|
477
|
+
controller flies the classic transient centered title stamp ("WAVE 3", "VICTORY", "OBJECTIVE COMPLETE") in, holds it,
|
|
478
|
+
and fades it out — one banner at a time — on an injected clock (`advance()`), exposing the current banner plus its
|
|
479
|
+
phase (`in`/`hold`/`out`) and `progress` `0..1` for a renderer to animate. `kind` is a free label the game styles; the
|
|
480
|
+
model never interprets it. Allocation-aware (the `current()` view is pooled) with `subscribe` + `snapshot`/`restore`.
|
|
481
|
+
`@jgengine/react`' `ObjectiveBannerHost` subscribes, drives the clock per frame, and maps phase + progress to
|
|
482
|
+
opacity/scale/translate, HudTheme-skinnable and colorable per `kind` via `kindThemes`. Demo: `objective-banner`.
|
|
483
|
+
- **Talent / skill-tree widget.** `@jgengine/react`'s `TalentTree` is a drop-in widget over the existing
|
|
484
|
+
talent model (`@jgengine/core/game/talents`): pass the node definitions plus a live `createTalentTree`
|
|
485
|
+
instance and it lays nodes out by branch column and prerequisite-depth tier, draws SVG prerequisite
|
|
486
|
+
edges, styles each node learned/available/locked/maxed with an icon + rank badge, and fires
|
|
487
|
+
`onLearn(nodeId)` on allocatable clicks. A new thin core selector `@jgengine/core/game/talentTreeView`
|
|
488
|
+
(`talentTreeView`) flattens the model into a serializable per-node render view (tier, state, met/unmet
|
|
489
|
+
edges) so the widget never re-derives topology or eligibility. HudTheme-token skinned; node ids and
|
|
490
|
+
branches stay opaque game data.
|
|
491
|
+
- **Countdown / timer HUD.** `@jgengine/core/time/timerSet`' `createTimerSet` is a serializable set of named
|
|
492
|
+
countdown/countup timers on an injected clock — one primitive for round timers, respawn clocks, and ability
|
|
493
|
+
cooldown/charge (identical mechanics; `id`/labels are free strings the engine never interprets). Start, pause,
|
|
494
|
+
resume, stop, reset, and read `{ remainingMs, elapsedMs, durationMs, progress01, running, expired }` per timer
|
|
495
|
+
(allocation-aware `read(id, out)`), observe structural changes via `subscribe` and expiry edges via
|
|
496
|
+
`poll`/`onExpire`, and `snapshot`/`restore` round-trip through a save. `@jgengine/react`'s `TimerReadout`
|
|
497
|
+
(live digital mm:ss/m:ss.d) and `TimerRing` (SVG radial fill/drain), plus the `useTimerRead` per-frame hook,
|
|
498
|
+
render it HudTheme-skinned — no hand-rolled interval math. See the `countdown-timer` dev demo.
|
|
499
|
+
- **Branching dialogue UI.** `@jgengine/core/game/dialogueGraph`'s `createDialogueRun` / `selectDialogueView` add a
|
|
500
|
+
thin, serializable branching-conversation model over the existing `features.dialogue` open/close bridge — a graph of
|
|
501
|
+
nodes (free-string speaker + line + choices that name the node they lead to) with choose-to-advance traversal, visited
|
|
502
|
+
history, and snapshot/restore. `@jgengine/react`'s `DialogueView` (+ `useDialogueRun`) is the drop-in view: speaker
|
|
503
|
+
name, an optional portrait slot, the current line, and clickable response buttons that advance the run — a game passes
|
|
504
|
+
its dialogue graph and gets working node traversal and choice state with no hand-rolled walk. Genre-agnostic:
|
|
505
|
+
speaker/choice `kind` are free strings the model never interprets, surfaced as `data-*` for game styling; HudTheme-skinned
|
|
506
|
+
via `--jg-*` tokens. See the `dialogue` demo.
|
|
507
|
+
- **Damage-direction indicators.** `@jgengine/core/vfx/damageDirection`' `createDamageDirectionTracker` is a
|
|
508
|
+
serializable, allocation-aware "hit-from" brain: `registerHit({ angle, intensity?, kind? })` (angle in radians,
|
|
509
|
+
`0` = front, free-string `kind` never interpreted) turns each hit into a directional indicator that fades over a
|
|
510
|
+
duration on an injected clock, with `active()` reporting live indicators at eased current intensity, optional
|
|
511
|
+
same-direction merging, a bounded pool, `subscribe`, and `snapshot`/`restore`. `@jgengine/react`'s
|
|
512
|
+
`DamageDirectionOverlay` (+ `useDamageDirection` hook) renders the classic red arcs flaring around a center reticle
|
|
513
|
+
toward each recent hit, opacity/scale from intensity, color per `kind`/HudTheme, `pointer-events: none`. New
|
|
514
|
+
`damage-direction` dev demo. Genre-agnostic, renderer-free core.
|
|
515
|
+
- **Turnkey day-night cycle.** `@jgengine/core/time/dayNightCycle`' `createDayNightCycle` is a genre-agnostic,
|
|
516
|
+
serializable brain that advances a normalized day fraction on an injected clock and blends per-keyframe phase labels
|
|
517
|
+
and tint/light colors (`DayNightKeyframe` → `DayNightSample`), with `pause`/`play`/`setSpeed`/`setDayFraction`,
|
|
518
|
+
`subscribe`, `snapshot`/`restore`, and a `calendar()` adapter so it drops straight into any `{ dayFraction }` sky
|
|
519
|
+
seam. The shell's `@jgengine/shell/environment` adds `DayNightSky`, a drop-in R3F presenter that drives the existing
|
|
520
|
+
`SkyDome` shader and lights from the model each frame — wire one thing for a moving, color-graded day-night sky
|
|
521
|
+
instead of hand-rolling a clock plus a color lerp. `phase`/`kind` strings stay free-form; the model never interprets
|
|
522
|
+
them. New `day-night` dev demo.
|
|
523
|
+
- **Status-effect timeline HUD.** `@jgengine/react`'s `StatusEffectBar` renders active buffs/debuffs as a row of
|
|
524
|
+
painted icons, each with a radial countdown ring and stack-count badge, driven off the existing status model
|
|
525
|
+
(`combat/statusApplication`'s `StatusInstance`). A thin genre-agnostic core selector,
|
|
526
|
+
`@jgengine/core/combat`'s `toStatusEffectViews` / `toStatusEffectView` (+ `statusEffectRemainingFraction`),
|
|
527
|
+
adapts live `StatusInstance`s into serializable `StatusEffectView`s (`id`, free-string `kind`, `remainingMs`,
|
|
528
|
+
`durationMs`, `stacks`) so the widget re-derives no ring math or timers. `kind` is never interpreted by the engine —
|
|
529
|
+
the game supplies icon/color/label; icons come from the existing game-icons.net icon system. Demo: `status-effects`.
|
|
530
|
+
- **Cutscene / sequence director.** `@jgengine/core/scene/sequenceDirector`' `createSequenceDirector` is a
|
|
531
|
+
serializable, genre-agnostic timeline: an ordered list of typed cues (`{ atMs, kind, payload }`) advanced by one
|
|
532
|
+
injected clock, firing each cue once and in order as its time passes — even across a large `seek` — with
|
|
533
|
+
`play`/`pause`/`seek`/`skip`/`stop` and `snapshot`/`restore`. The director only schedules and emits cues via
|
|
534
|
+
`onCue`; it never interprets a `kind`, so the same primitive drives camera moves, dialogue, fades, or any game
|
|
535
|
+
event without a genre kit. `@jgengine/react`'s `useSequenceDirector` runs the per-frame tick loop and exposes
|
|
536
|
+
playhead/progress + controls, and `CutsceneLetterbox` is a reskinnable cinematic bars + caption + Skip overlay.
|
|
537
|
+
See the `cutscene` dev demo.
|
|
538
|
+
- **Modal / confirmation dialog system + reskinnable pause menu.** `@jgengine/core/ui/modalStack`'s
|
|
539
|
+
`createModalStack` is a genre-agnostic, serializable stack of opaque modal records (`push`/`pop`/`resolve` with a
|
|
540
|
+
free-form result, `top`/`isOpen`/`depth`, optional injected-clock auto-dismiss via `tick`/`timeRemaining`,
|
|
541
|
+
`subscribe`, `snapshot`/`restore`) that never interprets a modal's `kind` or result. `@jgengine/react`'s `ModalHost`
|
|
542
|
+
renders the top modal over a dimmed backdrop with a focus trap and Esc/backdrop-to-cancel, `ConfirmDialog` is a
|
|
543
|
+
generic two-button confirm/cancel dialog, and `PauseMenu` is a drop-in Resume + game-filled Settings/Quit slot list.
|
|
544
|
+
All `HudTheme`-token-driven common parts — the game owns final layout, terminology, and skin. Demo: `pause-menu`.
|
|
545
|
+
- **Screen-state effects (postfx).** `@jgengine/core/vfx/screenEffects`' `createScreenEffects` is a genre-agnostic,
|
|
546
|
+
serializable screen-feedback controller: a game triggers transient full-screen flashes and edge vignettes
|
|
547
|
+
(`flash`/`vignette`) or sustained, optionally oscillating tints (`pulse`, e.g. a low-health breathe) — each a
|
|
548
|
+
free-string `kind` the game styles, plus color, peak intensity, easing, and duration. It is clock-driven
|
|
549
|
+
(`advance()` against an injected `now`) and allocation-aware (the `composite()` array and entries are pooled, so
|
|
550
|
+
steady-state ticking never allocates), with `subscribe` + `snapshot`/`restore`. `@jgengine/shell/postfx`'
|
|
551
|
+
`ScreenEffectsOverlay` subscribes to the model and paints the composite as pointer-transparent color-grade layers
|
|
552
|
+
over the canvas. Demo: `screen-effects`. The model never interprets `kind`; flash/vignette/pulse are just
|
|
553
|
+
parameterizations of the same data (region shape, decay easing, sustained oscillation).
|
|
554
|
+
- **Coach-marks / tutorial hints.** `@jgengine/core/ui/coachMarks`' `createCoachMarkSequence` is a genre-agnostic
|
|
555
|
+
onboarding model: an ordered list of `CoachMarkStep`s (title, body, optional `anchor`/`placement`, and a data-first
|
|
556
|
+
string `condition`), a persisted "seen" set so completed hints never re-show, condition-gating via `satisfy`/
|
|
557
|
+
`setSatisfied`, `current`/`advance`/`dismiss`/`skipAll`, `isComplete`/`remaining`, `subscribe`, and serializable
|
|
558
|
+
`snapshot`/`restore`. New `@jgengine/react`: `useCoachMarks` plus `CoachMark` and `CoachMarkHost` (anchored or
|
|
559
|
+
centered callout with an "N of M" counter, a dimmed spotlight backdrop, Next/Skip, and a `CoachMarkTheme` reskin).
|
|
560
|
+
New `coach-marks` demo.
|
|
561
|
+
- **Fast-travel network.** `@jgengine/core/world/fastTravel`' `createFastTravelNetwork` is a genre-agnostic
|
|
562
|
+
discovered-destinations model: game-defined `TravelPointDef`s (world-XZ position, region, icon, `initial`)
|
|
563
|
+
with per-player discovery tracking — `discover`, `isDiscovered`, distance-sorted `list(from)` and
|
|
564
|
+
`destinations(from)`, `nearest(from, { exclude })`, a `canTravel` gate, `discoveredCount`/`total`, an
|
|
565
|
+
`onDiscover` seam, and serializable `snapshot`/`restore` (re-seeds `initial` points). New `@jgengine/react`:
|
|
566
|
+
`useFastTravel` (re-render on discovery) and `FastTravelMenu` (region-grouped, distance-labeled destination
|
|
567
|
+
picker with a discovery counter and "you are here" flag). New `fast-travel` demo.
|
|
568
|
+
- **Generic particle system.** `@jgengine/core/vfx/particles`' `createParticleSystem` is a genre-agnostic,
|
|
569
|
+
deterministic, allocation-aware particle emitter: a fixed pool, data-only `EmitterConfig` (rate, position,
|
|
570
|
+
lifetime, speed, cone `spread`, `gravity`, `drag`, size/color/alpha over life), burst `emit(n)` and continuous
|
|
571
|
+
`rate`, `update(dt)` integration with swap-remove reaping, Structure-of-Arrays render `buffers()` (positions,
|
|
572
|
+
sizes, colors, alphas) clamped to the live count, injected-seed determinism, and serializable `snapshot`/
|
|
573
|
+
`restore`. New `@jgengine/shell/vfx/ParticleField` renders it as a one-draw-call soft-point GPU cloud with
|
|
574
|
+
per-particle size/color/alpha (additive or normal blending). New `particle-vfx` demo (fire / smoke / sparks).
|
|
575
|
+
- **Notification center.** `@jgengine/core/game/notifications`' `createNotificationCenter` is a persistent,
|
|
576
|
+
read-tracked notification log (newest-first, capped, serializable, observable) — the durable counterpart to
|
|
577
|
+
transient toasts: `push`, `list({ kind, unreadOnly })`, `unreadCount`, `markRead`/`markAllRead`, `remove`,
|
|
578
|
+
`clear`, `snapshot`/`restore`. New `@jgengine/react`: `useNotifications`, `NotificationBell` (icon + unread
|
|
579
|
+
badge), and `NotificationCenter` (scrollable panel with kind markers, relative time, unread highlight, and
|
|
580
|
+
mark-all-read/clear). First adopter: the apps/dev `notification-center` demo.
|
|
581
|
+
- **Photo mode.** `@jgengine/core/ui/photoMode`'s `createPhotoModeStore` is a serializable, observable
|
|
582
|
+
photo-mode state (active + hide-HUD) a game binds its capture flow to. New `@jgengine/react`
|
|
583
|
+
`usePhotoMode` + `PhotoModeControls` (hide-HUD toggle, capture, exit). New `@jgengine/shell/render/
|
|
584
|
+
sceneCapture`: `captureCanvas(gl)` reads the live frame to a PNG data URL (the shell canvas already sets
|
|
585
|
+
`preserveDrawingBuffer`), `downloadImage` saves it, and `SceneCaptureBinding` (mount in `WorldOverlay`)
|
|
586
|
+
hands the in-Canvas capture fn to the HUD's Capture button. The game reads `hideHud` to drop gameplay
|
|
587
|
+
chrome for a clean shot. First adopter: the apps/dev `photo-mode` demo.
|
|
588
|
+
- **Welded street intersections** (`@jgengine/core/world/roads`, #1363) — `trimPathAtJunctions` cuts every road back to an arm-derived apron at each crossing (through-roads split in two) and `buildJunctionSurface` welds a curb-return-filleted junction polygon whose boundary vertices are bitwise-shared with the trimmed ribbon ends; `buildTrimmedIntersections` runs the whole pipeline for a street network in one call. Junctions read as real intersections instead of overlapping ribbons under a floating disc (`buildJunctionPatch` is deprecated but still works). The shell's authored-road and city renderers consume it out of the box.
|
|
589
|
+
- **`GROUND_DECAL_LAYERS`** (`@jgengine/core/world/roads`, #1366) — one owning table for ground-decal elevations (terrain < road = junction < marking < glow) replacing scattered per-callsite Y epsilons; ribbon/junction builders default from it, and markings/glow overlays pair it with renderer-side `polygonOffset` so road surfaces stop z-fighting at distance.
|
|
590
|
+
- **Real curves and sidewalks in the street generator** (`@jgengine/core/world/streetGenerator`, #1364, #1368) — corners are now sampled circular-arc fillets honoring `minCurveRadius` instead of single bevels; street hierarchy comes from sampled betweenness centrality with an arterial-connectivity repair pass instead of a length percentile; and boulevard/avenue/street chains carry `sidewalks` offset polylines (new optional `sidewalkWidth` rule, default 2).
|
|
591
|
+
- **Race-track synthesis in circuit mode** (`@jgengine/core/world/streetGenerator`, #1365) — circuits are grown from seeded scatter → convex hull → inward midpoint displacement with chicane/ess/hairpin corner templates, an enforced start/finish straight, deterministic self-clearance retries, and a pit lane that leaves and rejoins the loop at two real junctions — an actual track layout instead of a wobbled ellipse.
|
|
592
|
+
- **City lot content resolution** (`@jgengine/core/world/cityGenerator`, #1367) — `resolveCityLotContent` (or `generateCity`'s new `content` option) enriches frontage lots with their zone band, building class from cityContent's weighted mixes (street-level-biased), floors, and massing pieces; a deterministic landmark pass merges clusters of adjacent lots into a few block-scale `hall`/`arena`/`market`/`campus` buildings (dial: `content.landmarks`, default 0.04). `GeneratedCity` gains an optional `lotContent`; the bare-lot path is unchanged.
|
|
593
|
+
- **Quick menu takes many forms.** The radial menu grows into a full quick-menu family. `@jgengine/core/ui/radialMenu`
|
|
594
|
+
gains a `RadialArc` (`startAngle`/`sweep`) so the wheel can be a partial **arc** (bottom half, quarter, ping bar)
|
|
595
|
+
with bounds-aware selection. `@jgengine/react`'s `RadialMenu` now renders per-slice **hotkeys, count badges,
|
|
596
|
+
cooldowns, and submenu markers**, and takes an `arc`. New `@jgengine/react` **`QuickMenu`** presents one item
|
|
597
|
+
model in four forms — `radial` · `arc` · `list` · `grid` — with section headers, hotkeys, badges, cooldowns,
|
|
598
|
+
disabled states, and **nested submenus** (drill in / back). First adopter: the apps/dev quick-menu demo (form
|
|
599
|
+
switcher across all four).
|
|
600
|
+
- **Per-context touch control modes** (`@jgengine/core/input/touchScheme` `TouchControlsConfig.modes` + `@jgengine/core/input/touchControlsMode` `setTouchControlsMode`/`activeTouchControlsMode`, re-exported from `gameplay`, #1370) — a game whose verbs change at runtime (on foot vs driving vs flying) declares one named touch config per context; each mode's fields replace the matching base fields and gameplay switches with `setTouchControlsMode(ctx, "car")` / `null`. The shell re-derives the dock the same frame, so a runner on foot never sees flight or vehicle chrome. Blind derivation past 8 buttons now logs a dev warning naming the curation seams. Vice Isle (on-foot/car/aircraft) is the first adopter.
|
|
601
|
+
- **Analog action values through the input snapshot** (`@jgengine/core/runtime/inputSnapshot` `publishAnalog`/`value`/`analog`, `InputFrame.analog`, #1370) — a continuous source (the virtual joystick today, gamepad sticks tomorrow) publishes per-action magnitudes (0..1) alongside the digital held set; `ctx.input.value(action)` reads them, `ctx.input.axis(...)` samples them per bound action (so steer/pitch/roll bound to action names become proportional automatically), and the walk controller consumes them via `resolveMovementIntent`'s new analog path — a slight stick tilt walks slowly in that direction at speed scaled by deflection, instead of slamming a full-speed digital strafe. Serializable: analog rides `InputFrame` to an authoritative host unchanged.
|
|
602
|
+
- **Floating joystick variant + Settings selector** (`@jgengine/shell` touch dock, `@jgengine/core/input/touchScheme` `TouchJoystickVariant`, #1370) — two shipped stick behaviors, player-selectable under Settings → Controls → "Joystick": `floating` (default; the dock corner is a capture zone with a faint resting ring and the stick spawns centered under the thumb) and `fixed` (classic always-visible stick). Both share the analog vector math with a radial deadzone (travel rescaled from 0.16), replacing the old per-axis 0.42 digital thresholds as the movement path.
|
|
603
|
+
- **Radial / quick menu (weapon/emote wheel).** `@jgengine/core/ui/radialMenu` ships the pure geometry —
|
|
604
|
+
`radialSlices`, `radialIndexFromAngle`, `radialIndexFromVector` (pointer/stick vector → slice with a
|
|
605
|
+
neutral dead zone), `radialSlicePosition` — with slices centered from "up" clockwise. New `@jgengine/react`
|
|
606
|
+
`RadialMenu` renders the wheel: pointer angle drives the highlight, the neutral hub closes, click/confirm
|
|
607
|
+
fires `onSelect`; the game supplies option icons/labels and skins it (`highlightIndex` for controller/test
|
|
608
|
+
control). First adopter: the apps/dev `radial-menu` demo.
|
|
609
|
+
- **Accessibility options.** `@jgengine/core/ui/accessibility`'s `createAccessibilityStore` is a
|
|
610
|
+
serializable, observable store of genre-agnostic accessibility preferences — reduced motion, high
|
|
611
|
+
contrast, `textScale` (clamped), colorblind mode, and captions — with `snapshot`/`restore`. Ships the
|
|
612
|
+
`COLORBLIND_MATRICES` (`feColorMatrix` values for protanopia/deuteranopia/tritanopia/grayscale),
|
|
613
|
+
`clampTextScale`, and a `reducedMotionDuration` animation gate. New `@jgengine/react` layer:
|
|
614
|
+
`AccessibilityProvider` (exposes a `--jg-text-scale` CSS var, sets `data-reduced-motion` /
|
|
615
|
+
`data-high-contrast` / `data-colorblind` / `data-captions`, and wraps the subtree in the selected
|
|
616
|
+
colorblind filter), `useAccessibility`, `usePrefersReducedMotion` (OS `prefers-reduced-motion`), and
|
|
617
|
+
`ColorblindFilters`. Input rebinding already existed; this fills the rest of the a11y surface. First
|
|
618
|
+
adopter: the apps/dev `accessibility` demo.
|
|
619
|
+
- **Quest / objective tracker HUD.** `@jgengine/core/game/quest`'s `describeTrackedQuest(def, instance)`
|
|
620
|
+
joins a static `QuestDef` with a player's live `QuestInstance` into a flat, renderer-free `TrackedQuestView`
|
|
621
|
+
(title, status, labelled objective progress — `defaultObjectiveLabel` derives "Defeat 5 Wolves" and can be
|
|
622
|
+
overridden). New `@jgengine/react` `QuestTracker` draws it as a compact HUD panel: per-objective checkmark +
|
|
623
|
+
strike on complete, `progress/count`, and a thin progress bar, with `maxObjectives` truncation. A widget over
|
|
624
|
+
the existing quest models — no runtime coupling to `QuestJournal`. First adopter: the apps/dev `quest-tracker`
|
|
625
|
+
demo.
|
|
626
|
+
- **Localization / i18n.** `@jgengine/core/i18n` (also on the `@jgengine/core/ui` barrel)'s `createI18n`
|
|
627
|
+
translates a message `Catalog` with active-locale lookup, a fallback-locale chain, `{param}`
|
|
628
|
+
interpolation, and `Intl.PluralRules`-based `plural(key, count)`; it's observable so `setLocale` re-renders
|
|
629
|
+
bound UI, and the catalog stays caller-owned static data. New `@jgengine/react` layer: `I18nProvider`,
|
|
630
|
+
`useT()` / `usePlural()` / `useLocale()` hooks, and a `<Trans k=… />` component — every HUD string binds to
|
|
631
|
+
the catalog and swaps live on language change. First fully-absent-system gap closed; genre-agnostic, no
|
|
632
|
+
content shipped. First adopter: the apps/dev `localization` demo (EN / ES / 日本語 switcher).
|
|
633
|
+
- **Click-to-place custom marker kinds** (`@jgengine/core/scene` `definePlaceableMarkerKind` + `@jgengine/editor`) — a game declares its own logical marker kind (a stash, checkpoint, bounty…) in one call and it becomes a click-to-place tool in the editor's `+ Add` menu, grouped under its `category`: pick it, click the world to drop a marker, Shift-click to keep placing, tune its `fields` in the Inspector. The light path skips a full studio's schema/resolver boilerplate; the runtime is unchanged (the game still reads the kind off `editorLayers.markers`). The Add menu now groups registered kinds by their `addCategory` header instead of lumping every one under "Studios". Register from a module the editor loads (a game's `editorLayers` import graph). Pairs with the `add_marker` RPC verb: `definePlaceableMarkerKind` is the designer's click-to-place path, `add_marker` the scripted one.
|
|
634
|
+
- **`add_marker` editor verb** (`@jgengine/editor`, #1360) — the one-shot counterpart to `add_path`: place a bare point marker (a spawn, checkpoint, pickup, bounty, or a game's own custom kind) at `x`/`z` (optional `y`) in a single `--rpc`/MCP call, no `export_document`/`import_document` roundtrip and no hand-editing `editor.scene.json`. `kind` is required; `color`/`label`/`rotationY`/`meta` are optional and `meta` is validated against the kind's registered schema when one exists. Re-ids on a document-global id collision and returns the landed id, exactly like `add_path`. Use `place_asset` when the marker should carry a mesh; `add_marker` is for logical, mesh-free markers a game reads off the document by kind.
|
|
635
|
+
- **World-anchored entity frames (overhead nameplates / health bars)** (`@jgengine/react/entityFrames`, `@jgengine/shell/world/WorldEntityFrames`, #1266) — a data-first, caller-owned seam for floating frames over entities. `EntityFrames` takes a plain `{ id, worldPosition, ... }[]` plus a `project` (world→screen) and a `renderFrame` that composes the shipped `HealthBar` + a name (reskinned via `barTokens`/`HudTheme`); it owns only the reusable behavior — projection offset, off-screen/behind-camera culling, and nearest-on-top stacking. The pure `layoutEntityFrames` core is unit-tested in isolation and imports no React DOM/Three.js. R3F games get the projector for free from `WorldEntityFrames` / `useWorldProjection` / `projectWorldToScreen` (`@jgengine/shell`), which sample the live camera and compose the react primitive — overhead enemy nameplates/health bars from a game's own entity array in a few lines, with no `GameProvider` or entity-store mirroring. See the `jgengine-ui` overhead-entity-frames recipe. Complements the store-driven `WorldNameplates`.
|
|
636
|
+
- **Per-entity render hide: `SceneEntity.hidden`** (`@jgengine/core/scene/entityStore`, `@jgengine/shell`, #1299) — a new optional render-only flag on scene entities, settable at `spawn` and via `update({ hidden })`. When `true` the shell's entity marker skips the visual entirely each frame while the entity keeps simulating (position, stats, AI targeting, queries untouched). Built for seated vehicle riders whose character model otherwise pokes through the vehicle body; equally fits cutscene actors and stealth cloaks. Absent/`false` renders exactly as before.
|
|
637
|
+
- **`ChaseCameraTuning` covers the full driving-feel surface** (`@jgengine/core/runtime/cameraDirector`, #1299) — the runtime `ctx.camera.setChaseTuning` patch now also accepts `shakePerSpeed` and `velocityYaw`, so a game can keep its static `camera.chase` block as a calm on-foot baseline and overlay the whole speed-reactive package (speed→FOV, lead, bank, speed shake, drift-lag) only while a vehicle is piloted. Existing tuning patches are unchanged.
|
|
638
|
+
- **Draw on the map: annotation layer + FullscreenMap draw tool.** `@jgengine/core/world/mapAnnotations`'
|
|
639
|
+
`createAnnotationLayer` is a serializable player-drawing layer — freehand `strokes`, area `shapes`, and
|
|
640
|
+
pinned `notes`, all world-XZ — whose `routes()`/`zones()` project straight into the `routes`/`zones` props
|
|
641
|
+
`Minimap`/`WorldMap`/`FullscreenMap` already render, so drawing needs no new renderer; `snapshot`/`restore`
|
|
642
|
+
round-trip through a save. `@jgengine/react/map`'s `FullscreenMap` gains a `tool` prop (`"pan"` | `"draw"`):
|
|
643
|
+
in draw mode a pointer drag paints a freehand stroke committed to `onStrokeComplete(points)` (with a live
|
|
644
|
+
in-progress preview via `drawTone`/`drawWidth`), while panning and click-to-place are suppressed. First
|
|
645
|
+
adopter: the apps/dev extraction-map demo (Pan/Draw/Clear palette).
|
|
646
|
+
- **`jgengine shoot` / `jgengine drive` CLI verbs** (`jgengine` CLI, #1262) — the browser capture and play-driving rungs are now first-class CLI verbs, so a standalone project (scaffolded with `npx jgengine create`, outside this monorepo) can screenshot and playtest its own game without any monorepo-only tooling. Run inside a project that already ships `scripts/shoot.mjs` / `scripts/drive.mjs` and the verb delegates to them (parity with `bun run shoot`); run in one that lacks them (an older scaffold, or one whose scripts were removed) and it materializes the same dependency-free harness the scaffold embeds and drives the project's dev server. Chrome/Chromium is the only capture engine — no Playwright and no npm deps travel with the published CLI; a missing browser fails with an install hint. Makes the "visual claims are screenshot-judged" rule enforceable for the exact outside users it targets.
|
|
647
|
+
- **Engine preview-fixture capture route** (`@jgengine/react` `PREVIEW_FIXTURES`, #1272) — a registry of deterministic, engine-level preview fixtures (the real exported components `HudThemePreview`, `BarsPreview`, `IconsPreview`, …) keyed by name, with `previewFixtureNames()` and `resolvePreviewFixture(name)`. The dev runner mounts one from a `?fixture=<name>` URL overlay and `bun run shoot --fixture <name>` screenshots it with no game boot and no hand-rolled `--url` page; `bun run shoot --list` (or `--fixture` with no name) prints the registered set. Register any additional deterministic preview component in `PREVIEW_FIXTURES` and the route and CLI pick it up with no further wiring — genre-agnostic, additive.
|
|
648
|
+
- **Per-capture player-spawn override** (`@jgengine/core/world/spawnOverride`, #1255) — `installSpawnOverride`/`clearSpawnOverride`/`readSpawnOverride` and the pure `parseSpawnOverride("x,y,z"|"x,y,z,yaw")` install a session-scoped spawn that `authoredSpawnPosition`/`authoredSpawnRotation` honor for the *default* player-spawn resolution only (an explicit marker `id` or non-`player_spawn` kind still reads the document), so a close-up capture can relocate the player without editing `editor.scene.json`. The dev runner installs one from a `?spawn=` URL overlay (mirroring `?cam=`), and `bun run shoot`/`bun run drive` gain a `--spawn x,y,z` flag. Default is no override, so nothing changes until one is installed.
|
|
649
|
+
- **Aerial editor camera in one call** (`@jgengine/editor`, #1256) — `camera_goto` and `camera_frame` take optional `distance`, `pitch` (degrees above the horizon; 90 = straight-down), `yaw`, and `height`, and the `EditorFocusTarget` carries them so the orbit camera repositions instead of only panning. `camera_frame` with a `pitch` but no `distance` auto-fits the document bounds into view (`orbitCameraPosition`/`frameDistanceForBounds` are exported and unit-tested), so composing a district aerial no longer takes many guess-the-y-offset round-trips or buries the camera in terrain.
|
|
650
|
+
- **Achievements / trophies** — `@jgengine/core/game/achievements`' `createAchievementTracker`
|
|
651
|
+
is a serializable tracker for counter-goal (`progress`/`setProgress`) and boolean (`unlock`)
|
|
652
|
+
achievements, with `score()`, `completion()`, an `onUnlock` seam (wire to a toast queue, feed,
|
|
653
|
+
or sound), and `snapshot`/`restore`; its view `list()` keeps a stable identity so React reads it
|
|
654
|
+
through `useSyncExternalStore` without re-projecting each frame. New `@jgengine/react` presentation:
|
|
655
|
+
`useAchievements(tracker)`, `AchievementGallery` (unlocked/locked grid with counter progress bars,
|
|
656
|
+
secret masking, and a completion/score header), and `AchievementToast` (unlock banner). Genre-agnostic
|
|
657
|
+
and additive — no defaults or content shipped.
|
|
658
|
+
- **World-space pings** (`@jgengine/shell/world/WorldPings`, `@jgengine/shell/world/pingPulse`) — the
|
|
659
|
+
in-scene side of a ping/marker: a bobbing downward arrowhead pointing at the spot, a ground ring, and a
|
|
660
|
+
billboarded callout (glyph + `meta.callout`/`label`), colored by marker kind and fading in/out over the
|
|
661
|
+
marker's lifetime. Reads a `MarkerSet` (typically the one a `createPingSystem` writes to), a marker
|
|
662
|
+
source, or a static array — mount through `PlayableGame.WorldOverlay`. `pingsOnly` (default true) limits
|
|
663
|
+
it to `meta.ping` markers; `renderCallout` overrides the chip. The pure lifecycle math (`pingOpacity`,
|
|
664
|
+
`pingBobOffset`) is exported and unit-tested. First adopter: the apps/dev `world-pings` demo.
|
|
665
|
+
- **Difficulty-aware AI driver** (`@jgengine/core/ai/driver`, #1311) — `driveStep(state, dt, pose, goal, profile, tuning, rng, obstacleAhead?)` turns a vehicle pose + goal into the shared `AxisInput` for `tickDrivableVehicle`, with a `DifficultyProfile` (#1301) deciding how well the car drives: a reaction delay-line makes low tiers chase where the target *was*, corners shed speed instead of full-throttle orbiting, obstacles are perceived late by `speed × reactionSeconds`, steering wobbles by `executionJitter` resampled on an interval, and a wall-grinding car reverses out with counter-steer after a tier-scaled delay. `pathTargetAhead` walks a road/route polyline to a pure-pursuit lookahead point so the same brain follows authored streets and circuits. State is one serializable object; randomness injected; no world scans. First adopter: vice-isle cruiser pursuit, whose cops now drive by wanted level (3★ sloppy rookies → 5★ sharp interceptors) instead of `steer = error*2, throttle = 1`.
|
|
666
|
+
- **Effect primitives: named hooks for the recurring `useEffect` shapes** (`@jgengine/react/hooks`, `@jgengine/shell/render/useDisposable`, #1298) — `useTicker(hz)` re-renders HUD elements at a steady rate for time-derived readouts (cooldowns, cast/swing bars); `useDomEvent(resolveTarget, type, handler, options?)` attaches a DOM listener with automatic cleanup and a stable handler ref; `useRafLoop(onFrame, active?)` runs a cancellable requestAnimationFrame loop with delta seconds; `useAutoScroll(dep)` pins a log/chat/console panel to its newest line; and shell's `useDisposable(create, deps)` (+ `disposeAll`) memoizes a three.js resource or tuple and disposes it on change/unmount, replacing the hand-rolled `useMemo` + dispose-effect pair. Prefer these over direct `useEffect` for their shapes — a lint restriction on direct `useEffect` outside the primitives layer follows in a later phase.
|
|
667
|
+
- **Three-tier AI decision quality** (`@jgengine/core/ai/difficulty`, #1301) — one serializable `DifficultyProfile` (reaction time, decision noise, execution jitter, ability discipline, perception scale, plan depth/width) owns how well any opponent decides, with canonical `easy`/`standard`/`expert` tiers (`DIFFICULTY_TIERS`, `difficultyProfile(tier, overrides)`) and deterministic appliers at the universal decision seams: `advanceReactionGate` (delay acting on new information), `pickScored` (noisy argmax over any caller-scored option list), `executionError` (aim/lead/timing fuzz), `shouldUseAbility` (spend-or-hold specials from a scored opportunity window), and `planLookahead` (depth/width-bounded negamax or own-sequence search over a game-owned `moves`/`apply`/`evaluate` domain — greedy at easy, multi-ply tactics at expert). Composes with the existing substrates: scale `MobBrainConfig.aggroRadius` by `perceptionScale`, route `acquireTarget` scores through `pickScored`, gate `advancePursuit` swaps behind the reaction gate.
|
|
668
|
+
- **Authored per-placement rig animation applies in game** (`@jgengine/core/world/authoredObjects`, `@jgengine/core/scene/objectStore`, #1276) — a placement's editor-authored `marker.meta.animation` (role→clip states, `"auto"`/`"none"`, walk/run/fade, one-shot bindings; the stable contract from #1274) now flows through `resolveAuthoredObjects` → `placeAuthoredObjects` → `ObjectStore.place` onto the placed object's `ModelConfig.animation`, so the override applies at play time instead of being write+preview only. `AuthoredObject.animation`, `PlaceOptions.animation`, and `SceneObject.animation` are new optional fields; the new `markerAnimation(marker)` reader exposes the contract. Markers with no override are unchanged — animation still comes from catalog resolution.
|
|
669
|
+
- **`EffectResult` carries the slain entity's identity** (`@jgengine/core/combat/effects`, #1263) — a lethal `ctx.scene.entity.effect()` hit now returns `slain: { catalogId, name?, userId? }` on the per-target result, captured before the death system despawns the target. Kill credit / XP reads the victim's `catalogId` (its spawn kind) straight off the result — no game-side spawn-time registry mirroring instance ids to kinds. Non-lethal hits omit `slain`. The new `EffectSystemDeps.resolveSlainIdentity` seam is optional and additive, so existing effect-system compositions keep their exact shape.
|
|
670
|
+
- Interactive fullscreen map + player waypoints. `@jgengine/core/world/waypoints`'
|
|
671
|
+
`createWaypointStore` is a serializable player-waypoint layer (place/track/clear,
|
|
672
|
+
snapshot↔restore) that mirrors pins into a `MarkerSet` so every map surface renders
|
|
673
|
+
them, and reports bearing/distance `guidance` for an on-screen arrow. New React
|
|
674
|
+
primitives in `@jgengine/react/map`: `FullscreenMap` (wheel-zoom/drag-pan overlay
|
|
675
|
+
that never fires a click after a pan), `WorldMapSurface` (the viewport-aware map
|
|
676
|
+
`<svg>` now shared by `WorldMap` and `FullscreenMap`), `MapLegend` (marker-kind key),
|
|
677
|
+
and `WaypointArrow` (HUD guide needle). A new `waypoint` entry joins
|
|
678
|
+
`DEFAULT_MARKER_KINDS`.
|
|
679
|
+
- Editor RPC/CLI verb `add_path` (`@jgengine/editor`): author a new path/route into the scene
|
|
680
|
+
document from an ordered list of ≥2 `{x,z}` (optional `y`) points in one call, without an
|
|
681
|
+
`export_document`/`import_document` roundtrip. `kind` defaults to `route`; `meta` is schema-validated
|
|
682
|
+
like `set_path`, and a colliding `id` re-ids in the document-global namespace.
|
|
683
|
+
|
|
684
|
+
### Changed
|
|
685
|
+
|
|
686
|
+
- **`check-game-shape`/doctor accept editor-import-graph modules by convention** (`packages/jgengine` `gameShape`, #1377) — a top-level `src/editor<Name>.ts` module (plus its colocated `.test.ts`) is now an allowed game-shape extra, matched by the `editor<Name>.ts` naming convention instead of an exact-filename enumeration. Fixes `editorKinds.ts` tripping the shape gate and stops the next editor-graph module from re-reddening `main` the moment it lands. No game needs changes; `editorLayers`/`editorCatalogs`/`editorKinds` and any future sibling are all recognized.
|
|
687
|
+
- **Road ribbons no longer self-intersect at bends** (`@jgengine/core/world/roads`) — `buildRoadRibbon` now miter-joins the inner edge of a bend and welds any residual fold when the local turn radius drops under the half-width, so dense arc-sampled corners render as one clean surface instead of a doubled bowtie. Straight ribbons and terminal cross-sections stay byte-identical (junction welds unaffected).
|
|
688
|
+
- **Junction surfaces stopped emitting sliver triangles** (`@jgengine/core/world/roads`) — `buildJunctionSurface` grouped approach corners globally by angle, which interleaved unequal-width approaches and produced shard/sliver fans; corners now stay grouped per approach with wrap-safe ordering and outward curb-return arcs, and the fan runs over an angle-monotonic simple boundary.
|
|
689
|
+
- **Street corner arcs sample at ≤~9° per vertex** (`@jgengine/core/world/streetGenerator`) — corner fillets previously stepped up to `maxTurnAngle` (~28°) per vertex and read as hard polygons at road width; both net and circuit corners now sample finely, and sidewalk offsets became true parallel offsets (outside arcs, inside miter-clamp + weld) that never pinch into the road surface.
|
|
690
|
+
- **Circuit centerlines are curve-first splines** (`@jgengine/core/world/streetGenerator`) — the folded layout polygon (hull + deep inward displacement, guaranteed hairpin/ess) now serves as control points for a periodic centripetal Catmull-Rom spline with a curvature floor at `minCurveRadius` (weighted Laplacian relaxation), so a lap is mostly flowing sweepers and continuous esses with 1-3 deliberate straights — not straight segments with corner caps. Fitted radii form a smooth continuum (~1.1-11x the floor); property tests pin curvature continuity, curved-share, and hairpin+sweeper coexistence.
|
|
691
|
+
|
|
692
|
+
- **Chase camera yaw is smoothed by default** (`camera.chase.yawResponse`, `@jgengine/shell` chase rig, #1370) — the boom now eases toward the followed body's facing with `1-exp(-response*dt)` smoothing (default response 5) instead of rigidly equaling it every frame, so a strafe-flipped facing arcs the camera around the character rather than teleporting it to the far side. Set `yawResponse: Infinity` to restore the legacy rigid follow; drift-lag (`velocityYaw`) keeps its own response as before.
|
|
693
|
+
- **Editor content-browser thumbnails no longer crash the editor when a GLB's textures fail** (`@jgengine/editor`, #1270) — `getGlbThumbnailState` (the `getSnapshot` behind `useGlbThumbnail`/`AssetThumbnail`) returned a fresh object literal on every call, violating the `useSyncExternalStore` stable-snapshot contract; React's post-commit consistency check then forced a re-render every commit ("Maximum update depth exceeded"), and the `GameUiErrorBoundary` blanked the whole editor chrome. It now reuses the prior snapshot object while a URL's observable state (idle/loading/ready/error) is unchanged, so a permanently-failing asset settles to a stable error/glyph fallback instead of looping. No API change; behavior only.
|
|
694
|
+
- **Offline autosave no longer starves in a living world** (`@jgengine/core/runtime/runtimeSave`) — `autosave` mode previously debounced by *resetting* its timer on every world change, so an open world that mutates every frame (AI, physics, the day/night clock) pushed the timer forward faster than it could fire and never persisted; a game that drove no explicit `checkpoint()` effectively never saved. Autosave is now a trailing timer: the first unsaved change arms it, later changes ride the same timer, and it captures the latest snapshot when it fires — at most one write per `autosaveMs` and always within one interval, even while the world never idles. No API change; behavior only.
|
|
695
|
+
- **Effect consolidation phase 2: subscriptions on `useSyncExternalStore`, dispose pairs on `useDisposable`** (`@jgengine/react`, `@jgengine/shell`, `@jgengine/editor`, #1304) — hand-rolled `subscribe(() => setTick)` re-render mirrors across the editor chrome/panels, shell settings (`useSettingsRevision`), camera director, WorldHud selection, and AuthoredScene live-document hook now ride `useSyncExternalStore` (fewer redundant render passes, identical update timing); ~15 shell renderers moved their `useMemo` + dispose-effect pairs onto `useDisposable`; derived-state/prop-reset/focus effects were removed in favor of render-time derivation and `autoFocus` (editor inspector fields, hierarchy active row, animation panel, play-control mirrors via the new shared `usePlayControl`, GamePlayerShell binding overrides — kills a one-frame stale-keybinds render on game switch; `GameViewport` now renders `data-jg-layout-collision` declaratively). No public API removals; `usePlayControl` (`@jgengine/editor`) is new.
|
|
696
|
+
|
|
697
|
+
- **Blocking colliders are now walkable surfaces, and collision is on by default** (`@jgengine/core`
|
|
698
|
+
movement, #1402) — the walking controller treats placed objects' blocking colliders as ground:
|
|
699
|
+
`collideObjects` defaults `true`; a ledge within the new `PlayerMovementConfig.stepHeight`
|
|
700
|
+
(default 0.4) is stepped up instead of walling; a jump that clears an object's top **lands standing
|
|
701
|
+
on it** (new `obstacleSupportHeight`, integrated in absolute space so the arc stays continuous over
|
|
702
|
+
changing ground) instead of sinking inside the box and being rubber-banded out by depenetration; and
|
|
703
|
+
walking off a drop taller than a step falls under gravity instead of teleporting the feet down.
|
|
704
|
+
- **Movement and vehicles no longer tunnel through solids** (`@jgengine/core`, #1402) — the shared
|
|
705
|
+
axis clamp in `resolveObstacleStep` is now swept: a step whose target lands *beyond* a box's far
|
|
706
|
+
face stops on the near face instead of passing straight through (fast cars, dashes/impulses, low-FPS
|
|
707
|
+
frames, thin walls). The walking broadphase also derives its reach from the scene's actual largest
|
|
708
|
+
blocking collider instead of a hardcoded 4 m bound, so the player no longer walks through the edges
|
|
709
|
+
of wide buildings whose center point sat outside the query; and the kinematic-vehicle clamp passes
|
|
710
|
+
collider `offset`/compound `boxes` through instead of silently dropping them.
|
|
711
|
+
- **Walking collision now depenetrates instead of locking.** `resolveObstacleStep`
|
|
712
|
+
(movement) previously slid along box faces but had no escape once the capsule was
|
|
713
|
+
strictly *inside* a solid box — its per-axis clamps returned zero on both axes and the
|
|
714
|
+
player was stuck forever. It now pushes an enclosed capsule out along the box's
|
|
715
|
+
shallowest face before sliding. Resting exactly on a face still reads as contact, so
|
|
716
|
+
normal wall-sliding is byte-for-byte unchanged; only the previously-unrecoverable
|
|
717
|
+
"wedged inside a building" case now frees itself.
|
|
718
|
+
- `import_document` (`@jgengine/editor`) now answers a missing or mis-keyed document param with an
|
|
719
|
+
error naming the expected `json` param instead of surfacing a raw `JSON Parse error: Unexpected
|
|
720
|
+
identifier "undefined"`.
|
|
721
|
+
|
|
722
|
+
### Removed
|
|
723
|
+
|
|
724
|
+
### Fixed
|
|
725
|
+
|
|
726
|
+
- **A missing/mis-served GLB no longer white-screens the whole game — it degrades to one placeholder primitive** (`@jgengine/shell/render/modelLoad`, #1340). A Vite dev server returns its `index.html` fallback (HTTP 200) for a missing `/models/*.glb`; `GLTFLoader` then throws parsing HTML as a GLB, and that failure surfaced as a rejected `useLoader` Suspense promise which does **not** reliably re-throw into a per-model React error boundary inside the react-three-fiber reconciler — so it escaped to the app-level `GameUiErrorBoundary` and blanked the scene. `sharedGltfLoader` (`DiagnosticGLTFLoader`) now probes a failed URL and, for a diagnosed broken asset (missing / HTML fallback / corrupt / unsupported), **resolves to a `createFallbackModel` placeholder box instead of rejecting**, containing the failure at the load seam for every consumer with no reliance on boundary recovery; a genuine parse error over valid-looking bytes is still surfaced. Emits a dev console warning naming the broken path. No API change; `useLoader(sharedGltfLoader, url)` benefits with no call-site change.
|
|
727
|
+
|
|
728
|
+
## 0.14.0
|
|
729
|
+
|
|
730
|
+
### Migrate
|
|
731
|
+
|
|
732
|
+
- **Nothing required** — this release is additive drop-in primitives, editor authoring, CLI discovery, and fixes. Bump `@jgengine/*` pins to `^0.14.0` (CLI `jgengine` is 0.11.0; `@jgengine/github` unchanged at 0.1.0) and rebuild.
|
|
733
|
+
- One behavior fix to know about: in a `rig: "chase"` game, on-foot movement and aim are now camera-relative (the chase rig reports its yaw to the shell like every other rig). If a game somehow relied on world-yaw-0 movement under a chase camera, that was the bug this fixes.
|
|
734
|
+
|
|
735
|
+
### Added
|
|
736
|
+
|
|
737
|
+
- One authoring gesture for scatterable coverage: `@jgengine/core/world/scatterCoverage` owns shared density/budget semantics (per-kind unit, requested→count→capped, one clamp-and-warn phrasing) for `grass_field`/`scatter`/`city`; the editor inspector leads each kind with the same Area → Assets → Density coverage section, scatter truncates to the shared 250k instance budget, and city single-sources its 2,600-lot cap.
|
|
738
|
+
- **Created games ship a drive/playtest script** (#1248) — `npx jgengine create` scaffolds now include `scripts/drive.mjs` (`bun run drive`): dependency-free headless play/testing of the running game — ordered `--click`/`--key`/`--wait`/`--shot`/`--rpc` steps against the agent bridge, plus `--playtest --strict` progress/softlock verdicts off `capture.probe`. Shared Chrome/CDP machinery lives in `scripts/browser.mjs`; `scripts/shoot.mjs` is unchanged in behavior, now a thin CLI over it.
|
|
739
|
+
- Editor: viewport clip preview for rigged assets (Animation dock "Clips" mode — pick a rig or placed instance, play/scrub/loop/speed any catalog clip) and an Inspector "Animation" section that authors a placement's `ModelConfig.animation` (role→clip dropdowns, auto/none, walk/run/fade, one-shot event bindings) as undoable `marker.meta.animation` edits. New subpaths: `@jgengine/editor/shell/clipPreview`, `@jgengine/editor/modelAnimationAuthoring`, `@jgengine/editor/ClipPreviewLayer`.
|
|
740
|
+
- **`npx jgengine find <intent>`** — active capability discovery: searches every shipped domain's `capabilities.md` (staged inside the CLI tarball, so it works regardless of which `@jgengine/*` packages a project installed) and prints the drop-in primitive + its import for an intent like `"toggleable window"`, `inventory`, or `minimap`. Scaffolded games are now briefed (in `AGENTS.md` and the `jgengine` intake skill) to reach for it before hand-rolling a HUD/inventory/window/rig.
|
|
741
|
+
- **`EntityPreview`** (`@jgengine/shell/render/EntityPreview`) — drop-in live 3D entity portrait for character screens, unit inspectors, and loadout viewers. Owns the nested `<Canvas>`, the `GameContextBridge`, and a `StudioStage` lighting rig with optional turntable / face-camera; the game passes its own `renderEntity` as children (or a native `model` + `instanceId`) and, bound to a live entity, the portrait walks/flinches/topples in sync with the world.
|
|
742
|
+
- **`GameContextBridge`** (`@jgengine/react`) — re-provide the running `GameContext` across a nested React reconciler boundary (the R3F `<Canvas>`), so a game building its own preview canvas no longer re-derives the bridge by hand.
|
|
743
|
+
|
|
744
|
+
### Changed
|
|
745
|
+
|
|
746
|
+
- **Chase rig reports its yaw to the shell** — `ChaseRig` now writes the camera yaw back to the shared yaw ref like every other player-facing rig, so on-foot movement and aim in a `rig: "chase"` game are camera-relative instead of frozen to world yaw 0 (WASD no longer fights the camera the moment it swings behind the player). Interior views (cockpit/hood/rear) report the vehicle heading.
|
|
747
|
+
- **Combat VFX no longer render as black squares under AO/DOF post-processing** (#1247) — GTAO/Bokeh scene prepasses skip overlay effects. Games with custom additive overlay effects opt out the same way: spread `POSTFX_OVERLAY_USERDATA` (`@jgengine/shell/postfx/postfxOverlay`) onto the overlay group's `userData`.
|
|
748
|
+
- **`PanelHost` windows stack above the HUD by default** (`@jgengine/react`) — the host establishes its own stacking context at `zIndexBase` (default 40), so open windows always paint over stat bars / nameplates / frames instead of bleeding through. Overridable per instance.
|
|
749
|
+
|
|
750
|
+
## 0.13.0
|
|
751
|
+
|
|
752
|
+
### Migrate
|
|
753
|
+
|
|
754
|
+
- **Nothing required** — this release is additive fixes, defaults, and new drop-in primitives. Bump `@jgengine/*` pins to `^0.13.0` (CLI `jgengine` is 0.10.0; `@jgengine/github` unchanged at 0.1.0) and rebuild.
|
|
755
|
+
- Two defaults changed in the player's favor, both overridable: **offline games now persist to localStorage automatically** (#1207) and **rigged catalog assets animate by default** via clip metadata + semantic clip roles (#1209). If a game relied on rigged models staying frozen or on saves being memory-only, opt out explicitly; otherwise you get better behavior for free.
|
|
756
|
+
|
|
757
|
+
### Added
|
|
758
|
+
|
|
759
|
+
- **Drop-in inventory grid, window manager, and character sheet** (#1220) — `@jgengine/react` ships `InventoryGrid` (real drag/stack/split), a toggleable-window system (`panels`: bag, character sheet, spellbook-style windows with `@jgengine/core/ui/panelModel` state), and a `CharacterSheet` paperdoll, all HudTheme-skinnable — wire in a line or two instead of re-deriving from raw divs.
|
|
760
|
+
- **Serializable runtime snapshots** (#1179) — capture/restore of live runtime state, including magazine state (`@jgengine/core/combat/magazine`) and stat modifiers, for saves and replication.
|
|
761
|
+
- **Portable weapon plumbing** (#1211) — `@jgengine/core/combat/weaponFire` composes trigger → magazine → spread → projectile/hitscan as data, no game-local glue.
|
|
762
|
+
- **Portable leveling** (#1206) and **portable damage-pool access** (#1208) — progression and damage pools exposed as genre-agnostic seams on the game context.
|
|
763
|
+
- **Named combat VFX presets** (#1204) — `@jgengine/core/combat/vfxPresets`: a visible attack is one line.
|
|
764
|
+
- **Runtime-measured render-bounds hitboxes** (#1227) — colliders wrap what the shell actually mounts, not the raw asset bounds.
|
|
765
|
+
- **Character motion feel** — procedural part motion for rig-less part-composed characters (#1222) plus squash & stretch and death splat (#1229).
|
|
766
|
+
- **City fabric** (#1191) — `streetGenerator` + `cityGenerator`, editor bake verb, street-aware lots, road junctions, grounding, and a web playground.
|
|
767
|
+
- **`jgengine recipe` CLI** (#1214, #1231) — vetted wired compositions, compile-pinned against the SDK, listed and copied from the CLI.
|
|
768
|
+
- **Editor/debug modes mirror to the URL** with a proper editor exit (#1203); game template ships a dependency-free WebGL screenshot script (#1202).
|
|
769
|
+
|
|
770
|
+
### Changed
|
|
771
|
+
|
|
772
|
+
- Offline games persist to localStorage by default (#1207); rigged assets animate by default (#1209).
|
|
773
|
+
- RTS camera pan inversion fixed (#1228); sprite raycast warning silenced and duplicate nameplate health bars removed (#1194); scaffolded F2+E editor Tailwind `@source` renders its theme (#1195).
|
|
774
|
+
- Outside-game DX: stat-pool no-op stability, camera-transparent decor, cleaner install/version output, headless UI-intent seam (#1223); new-game briefs direct engine bugs/gaps upstream (#1200).
|
|
775
|
+
|
|
776
|
+
## 0.12.0
|
|
777
|
+
|
|
778
|
+
### Migrate
|
|
779
|
+
|
|
780
|
+
- **Author games through `defineGame` from `@jgengine/shell`** — it is the single public authoring entry (and carries the define-game capability tag). Core's constructor was renamed `defineGame` → `defineGameDefinition` (`@jgengine/core/game/defineGame`); only hosts and tooling call it. If you imported `defineGame` from core, switch the import to `@jgengine/shell`. `GamePlayer`/`GamePlayerShell` are `@internal` now — `GameHost` is the documented mount, and it owns the editor summon (`?mode=editor`, F2+E, dev save endpoint), so delete any per-game editor bootstrap.
|
|
781
|
+
- **Declare your world with `world()`** (`@jgengine/core/world/place`): `world({ id, ground: { mode, size, surface? }, physics? })` with `flat` / `round` / `voxel` / `board` ground. New scaffolds emit a thin infinite flat place instead of the old 96 m meadow. `environment()` still works but is demoted to the legacy consumption path for editor-written scene data — **nothing required** for existing games; new worlds should use `world()` and author all dressing in the editor.
|
|
782
|
+
- Everything else is additive — stat pools, minimap marker sources, auction house, city/path generation, editor workspaces require no changes to existing games.
|
|
783
|
+
|
|
784
|
+
### Added
|
|
785
|
+
|
|
786
|
+
- **World = place: `world()` API** (#1178) — a world is substrate + laws, not a coded diorama. `@jgengine/core/world/place` ships `world({ id, ground: { mode, size, surface?, generator? }, physics? })`; size is discriminated by mode (flat `{x,z}` with `Infinity` axes, round `{radius}`, voxel generator domain, board `{x,y}` cells — TS and runtime both reject mismatches). `ground.surface` carries matter/feel laws (metal vs slime vs felt) read by the same rule systems; per-place `physics` resolves over the game default; multiple worlds per game are first-class. The shell renders place substrates (`PlaceScene`) under the engine default sky; seeds derive from world id + save via `seedForPlace`.
|
|
787
|
+
- **`@jgengine/shell/gameKit`** — happy-path authoring surface: authoring, mount, stores, systems, authored-scene helpers, HUD primitives, seeded rng in one import.
|
|
788
|
+
- **Portable generic stat pools** — `@jgengine/core/stats/statPool`: bounded named pools (health, energy, heat, ammo…) with the entity-stats integration rebuilt on them; recipe in skill `jgengine-combat`.
|
|
789
|
+
- **Auction-house economy primitives** (#1086) — `@jgengine/core/economy/auctionBook` (timed bid auctions, escrowed bids, outbid refunds, optional buyout, anti-snipe close extension, seller settlement minus house cut) plus market price history.
|
|
790
|
+
- **Portable minimap marker sources** — core world seam feeding game minimaps; recipe in skill `jgengine-ui`.
|
|
791
|
+
- **Unified seed-driven path network + city generation v2** (#1103, #1106) — streets/blocks/buildings with grid-ness, curviness, branching and block-size dials; zone bands, weighted building-class mixes, massing pieces, balance presets, junction-centered clustering, road-derived block polygons and parcels. Shell renders realistic roads: real intersections, curbs, lane lines, tunnels, and roads riding bridge decks.
|
|
792
|
+
- **Physical fidelity** — mesh-accurate hitboxes (shots pass through holes in concave models, #1075), auto-fit entity/object colliders from measured model bounds (#1072), movement obstruction reading fitted boxes + compound-box openings (#1077).
|
|
793
|
+
- **Editor overhaul** (#1110) — materials / lighting / scripting / animation-timeline / multiplayer workspaces, command palette, minimap bake wired end-to-end (#1036), real offscreen GLB thumbnails in the content browser, hierarchy drag-drop + keyboard + context menus + per-object visibility/lock, multi-select inspector, console RPC, asset drag-place, ortho projection, pivot modes, play chrome.
|
|
794
|
+
- **Editor authoring pipeline** — author entity definitions and starter catalogs (#1012), author catalogs/schema fields in the Data tab (#1043), promote an authored scene folder into a runnable game (#1011), authored triggers promoted to a shared primitive with goal/win rules (#1013), asset import rewrites the game's `game/assets.ts` (#1042).
|
|
795
|
+
- **Skills** — research-backed `game-design` and `level-design` skills (#962); every-game-owns-custom-UI invariant codified (#1148).
|
|
796
|
+
|
|
797
|
+
### Changed
|
|
798
|
+
|
|
799
|
+
- `npx jgengine create` scaffolds a thin infinite flat `world()` place; the editor `blankWorld` no longer pre-bakes seeded grass. Dressing belongs in `editor.scene.json`.
|
|
800
|
+
- Environment studio visual-quality pass — turf tufts, depth-aware water, relief soil, weathered poles; grass fields no longer rebuild geometry per frame (#1090).
|
|
801
|
+
|
|
802
|
+
## 0.11.0
|
|
803
|
+
|
|
804
|
+
### Migrate
|
|
805
|
+
|
|
806
|
+
- **Nothing required** — classic `loop.onTick` fan-out still runs after systems. Prefer moving per-frame work into `defineGame({ systems: [...] })` with `defineSystem` (`@jgengine/core/game/defineSystem`); install a system with `feature: "quest"` (etc.) instead of a redundant `features` flag. See skill `jgengine` → [reference-systems.md](.claude/skills/jgengine/reference-systems.md).
|
|
807
|
+
|
|
808
|
+
### Added
|
|
809
|
+
|
|
810
|
+
- **Composable game systems** (#842) — `defineSystem` / `compileSystemSchedule` / `composeGameLoop` / `installSystems`. `defineGame({ systems })` is the single authoring path: fixed / frame / interval / event-only / manual ticks, multi-subscribe channels, deterministic stage order + optional `before`/`after`/`dependsOn`, system-owned save / replicate / reset / dispose. Installing a system activates its `feature` without a separate flag. `GameLoop` gains optional `onReset` / `onDispose`. `ctx.game.registerSave` / `registerReplicate` for system modules. Adopters: tower-guard, claudecraft (runtime without a giant tick fan-out).
|
|
811
|
+
|
|
812
|
+
## 0.10.0
|
|
813
|
+
|
|
814
|
+
### Migrate
|
|
815
|
+
|
|
816
|
+
- **Nothing to change for existing games** — the shared mobile-composition system (viewport allocation, region collision, `--jg-*` viewport vars) is wired automatically by the shell for every game; `HudPanel`s inside a `HudCanvas` register themselves. Legacy `orientation: "landscape" | "portrait"` keeps its advisory dismissible-hint behavior unchanged.
|
|
817
|
+
- **Opt a driving/landscape game into the strict gate**: replace the advisory `orientation` with the contract form — `orientation: { mobile: "landscape-required" }` (rules: `any` · `portrait`/`landscape` advisory · `portrait-required`/`landscape-required` · `unsupported`). In portrait the engine now shows a polished `RotateDeviceScreen`, suppresses input, freezes the sim, and unmounts the HUD/controls until the device is landscape.
|
|
818
|
+
- **Declare HUD intent** where useful: `HudPanel` gains `priority` (`critical`/`secondary`/`tertiary`), `mobileBehavior` (`hidden` unmounts on phones, `transient` softens collision, …), `allowOverlapWith`, and `collisionGroup`. Optional — omitting them keeps prior behavior.
|
|
819
|
+
- **Mobile validation now also fails on overlap**: `bun run shoot <game> --device mobile|mobile-landscape|both` exits non-zero on forbidden inter-element collisions (not just viewport overflow), naming both regions.
|
|
820
|
+
|
|
821
|
+
### Added
|
|
822
|
+
|
|
823
|
+
- Shared mobile layout composition — `@jgengine/core/ui/gameLayout` (pure geometry: `resolveLayoutMode`, `intersects`, `overlapArea`, `detectLayoutCollisions`, `computeGameplayRect`, `LayoutRegion`, `GameViewportLayout`) and `@jgengine/core/ui/orientation` (`resolveOrientationRequirement`, `orientationGateActive`, `MobileOrientationRule`). `@jgengine/react` adds `GameViewportProvider` (mounted by the shell), hooks (`useGameViewportLayout`, `useGameLayoutMode`, `useGameOrientation`, `useReservedControlZones`, `useLayoutCollisions`, `useRegisterLayoutRegion`), and a headless `RotateDeviceScreen`. The provider tracks `window.visualViewport`, publishes `--jg-viewport-*` / `--jg-visual-viewport-*` / `--jg-safe-*` CSS vars, resolves the explicit layout mode, and detects forbidden region overlaps (dev `data-jg-layout-collision` + console diagnostic).
|
|
824
|
+
- Mandatory orientation contract — `PlayableGame.orientation` accepts `{ mobile: <rule> }`; `landscape-required`/`portrait-required` render an engine-owned rotate gate that blocks gameplay (input suppressed, simulation frozen, HUD/controls unmounted), self-correcting when the device is turned. Reduced-motion and safe-area respected; `visualViewport`-sized for mobile Safari/PWA.
|
|
825
|
+
- Reserved touch-control zones — the touch dock registers its joystick / action-cluster / utility rectangles (runtime-measured) as `control` regions, so HUD placement and collision validation know exactly where controls sit.
|
|
826
|
+
- HUD placement metadata — `HudPanel` gains `priority`, `mobileBehavior`, `allowOverlapWith`, `collisionGroup`, and registers as a collision-tracked layout region.
|
|
827
|
+
- Mobile-landscape shoot device — `bun run shoot --device mobile-landscape` (844×390) plus collision-gate reads that fail mobile validation on forbidden overlaps.
|
|
828
|
+
- Canyon Chase migrated as the reference landscape-required game — declares `orientation: { mobile: "landscape-required" }`, composes its HUD through coordinated `HudPanel` regions (pursuit distance critical, border secondary, survey map hidden on mobile, radio transient) instead of independent fixed corners.
|
|
829
|
+
|
|
830
|
+
### Changed
|
|
831
|
+
|
|
832
|
+
- First-person projectile tracers now originate from the weapon muzzle instead of the camera/eye centerline. Hit detection is unchanged (still crosshair-accurate) — only the drawn tracer's start point moved to the viewmodel muzzle, which tracks full aim yaw and pitch. Non-viewmodel games and enemy tracers are unaffected.
|
|
833
|
+
|
|
834
|
+
## 0.9.0
|
|
835
|
+
|
|
836
|
+
### Added
|
|
837
|
+
|
|
838
|
+
- Mobile HUD fit — design-resolution UI scaling now applies to every game by default: `HudCanvas` measures the live viewport and scales the whole HUD from `PlayableGame.hudFit.designSize` (default 1600×900, clamps `minScale`/`maxScale` 0.4–1) so desktop-authored layouts shrink to fit a phone instead of overflowing it; `hudFit.mobile` overrides tune the phone fit separately. Pure math lives in `@jgengine/core/ui/hudScale` (`hudScaleForViewport`, `resolveHudFit`, `rectOverflow`, `overflowingPanels`); the shell mounts `@jgengine/react`'s `HudViewportProvider` around `GameUI` so games need no wiring.
|
|
839
|
+
- Graphics → UI scale — a player-facing `graphics.uiScale` slider (0.5–1.5, default 1) multiplies the computed HUD scale on every platform; the same resolution system drives desktop preference and mobile shrink.
|
|
840
|
+
- HUD overflow gate — `HudCanvas` measures every `HudPanel` against the viewport at runtime and reports offenders on a `data-hud-overflow` attribute (plus a console warning); `bun run shoot <game> --device mobile|both` now exits non-zero naming the panels that escape the viewport.
|
|
841
|
+
- Automatic visibility & streaming defaults — an engine-level `VisibilitySystem` (exported from `@jgengine/core`) gives every scene renderer-agnostic culling and asset-streaming policy with no per-object wiring: distance culling, preload margins, hysteresis, delayed unloading, multi-camera awareness (including cameras excluded from streaming), and per-object overrides (always-visible, never-unload, disabled culling/streaming, custom render distance and preload margin).
|
|
842
|
+
- Self-hosted asset mirror — `DEFAULT_RELEASE_BASE` now points at this repo's rolling `packs` release instead of the upstream host; a catalog-driven `mirror-assets` workflow (weekly cron + manual dispatch) keeps the mirror in sync with the asset catalog.
|
|
843
|
+
|
|
844
|
+
### Changed
|
|
845
|
+
|
|
846
|
+
- Mobile HUD fit is on by default — design-resolution HUD scaling now applies to every game with no config change (previously opt-in via `platforms: ["web", "mobile"]`). A desktop-only game keeps the legacy fixed 0.85 compact zoom by declaring `platforms: ["web"]` (without `"mobile"`).
|
|
847
|
+
|
|
848
|
+
## 0.8.0
|
|
849
|
+
|
|
850
|
+
Transport-agnostic multiplayer (socket.io, WebRTC p2p, LAN/Fly adapters) plus grid/voxel
|
|
851
|
+
and puzzle primitives, HUD-only presentation, cumulative leveling, and a round of
|
|
852
|
+
controller/camera/sensor additions. **Breaking:** the `gameui` component kit moved out of
|
|
853
|
+
`@jgengine/react` onto the shadcn registry.
|
|
854
|
+
|
|
855
|
+
### Migrate
|
|
856
|
+
|
|
857
|
+
- Bump every `@jgengine/*` dependency to `^0.8.0` (the eight packages version in lockstep).
|
|
858
|
+
- Additive only, except for `gameui` — every other 0.7.0 API is unchanged; opt into any of the below by importing it directly.
|
|
859
|
+
- **Breaking:** replace any `@jgengine/react/gameui` import with the equivalent registry component (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), and swap `GameUiThemeProvider` for `--jg-*` CSS variables on a wrapper element. `GameIcon` and friends moved to `@jgengine/react/gameIcons`.
|
|
860
|
+
- `leveling({ thresholdMode: 'cumulative' })` is opt-in; the default `perLevel` behavior is unchanged.
|
|
861
|
+
- `defineGame.physics.gravity`/`jumpVelocity` are now read by the built-in kinematics controller every frame — if a game already set them expecting a no-op, jump/fall now actually reflects them; omit both to keep the previous defaults.
|
|
862
|
+
|
|
863
|
+
### Added
|
|
864
|
+
|
|
865
|
+
- Cumulative leveling — `leveling({ thresholdMode: 'cumulative' })` tracks xp as a lifetime total that resolves upward across levels and clamps at the max-level threshold once capped (#12).
|
|
866
|
+
- Direction-aware pool depletion — `EffectSystem.canReceive(instanceId, effect, magnitude?)` takes an optional signed magnitude; negative checks the opposite direction and returns `pools-depleted` only when every stat in the receive order is already at max, so heals reach fully-depleted targets (#168).
|
|
867
|
+
- Puzzle primitives — `puzzle/cellGrid` (uniform-cell boards: line-clear, match-3 cascade, run detection) and `puzzle/fallingPiece` (rotation-state shapes, ghost drop, lock delay, classic gravity/level/score curves) for Tetris/match-3 games (#166).
|
|
868
|
+
- Voxel field — `world/voxelField` (`createVoxelField`, chunked block lattice, neighbors/exposedFaces, 3D DDA raycast, dirty-tracked `chunkVersion`) for voxel games and instanced renderers; assert on `field.summary()` the way environment worlds assert on `summarizeEnvironment` (#166).
|
|
869
|
+
- `defineGame` games may omit `assets` (an empty catalog is injected); `PlayableGame.presentation: 'hud'` mounts no 3D canvas/camera/pointer for board/card/menu games; an `environment()` world auto-renders as the shell's backdrop when `PlayableGame.environment` is unset (#166).
|
|
870
|
+
- Declared-action intent board — `turn/intent` `createIntentBoard` for one-turn-ahead intents (Slay-the-Spire style): declare/peek/all/consume/clear (#168).
|
|
871
|
+
- `turnLoop` lifecycle hooks — `config.onTurnStart`/`onTurnEnd` fire on every `advanceTurn()`; `ctx.game.turn.loop(id, config)` lazily creates/returns a notify-wrapped `TurnLoop` so every mutation (`advanceTurn`/`advancePhase`/`advanceRound`/`spend`/`gain`/`refill`/`setOrder`/...) auto-bumps `ctx.version()` with no manual wiring (#163/#168).
|
|
872
|
+
- `ctx.game.store` — a reactive per-game keyed store (`set`/`delete`/`get`/`has`/`subscribe`/`mapSnapshot`/`arraySnapshot`) plus `@jgengine/react`'s `useGameStore` selector hook, replacing hand-rolled module-level stores for ad-hoc reactive game state; `ctx.game.cards.pile(id, config?)` lazily creates/returns a notify-wrapped `CardPile` the same way; `createCardPile` gained an `onChange` hook for headless use; `CommandDefinition.apply` may return void for side-effect-only commands (#163).
|
|
873
|
+
- Camera — `sideScroll` rig (fixed lateral follow for 2.5D platformers/beat-'em-ups), a `none` rig (no camera mounted; pairs with `PlayableGame.presentation: 'hud'`), `rts.pan: false` (static backdrop camera: no pan/edge-scroll/rotate/zoom, still re-centers on the follow target), and the `observer` rig now defaults to the local player when `bind` is unset (#167).
|
|
874
|
+
- Sensors + session — `sensor/concealment` (`colorDistance`/`concealmentScore`/`createConcealmentSensor`), `sensor/freezeMonitor` (`createFreezeMonitor`), `session/roles` (`assignRoles`); `createRoundState`'s `RoundConfig.teams` accepts per-team roles and an optional `winCondition` that `evaluate()` checks each tick, and takes an optional `phaseOrder` for arbitrary named phase cycles (`concludeRound`/`evaluate` settle only while the current phase is neither the first nor the last entry) (#151).
|
|
875
|
+
- Appearance replication — presence rows carry an optional per-slot appearance channel (cosmetic ids, hex tints, model keys) alongside pose, riding the existing pose message with no protocol bump; wire `ctx.player.cosmetics.get(userId)` into the outgoing pose (#151).
|
|
876
|
+
- `ctx.input` — a per-frame held-action snapshot (`publish(held)`/`isDown(action)`/`held()`) without bumping `ctx.version()`; action bindings gained `repeatMs` (repeat-fire while held); every command resolved from a bound action now carries `aim`; `pointer.secondaryCommand` runs a command on right-click off the same raycast as move/ping (#164).
|
|
877
|
+
- Object spatial queries + entity patching — `ctx.scene.object.at`/`inBox`/`raycast`/`raycastAll` over unit-box objects; `ctx.scene.entity.update(id, patch)` for name/position/rotation/role/movement/behaviors/meta; per-instance `renderObject`/`objectStyles` overrides; `pointerService.worldHitCenter()` + pointer-lock center-ray aiming (#165).
|
|
878
|
+
- Controller movement config — `PlayerMovementConfig` (`mode`: free/axis/grid, `axis`, `cellSize`, `collideObjects`, `beforeCommit` pre-commit hook) for the shell-driven walk controller; `defineGame.physics.gravity`/`jumpVelocity` are honored by the built-in walk controller (distinct from the standalone `physics/physicsWorld` rigid-body sim); `ctx.player.motion.impulse`/`setVerticalVelocity`/`setY`/`takePending` (`MotionIntents`); `entity.spawnPoseOf`/`resetToSpawn`/`resetAllToSpawn` (#162).
|
|
879
|
+
- Model material/animation + paint — `ModelConfig.tint`/`metalness`/`roughness`/`animation` (GLTF clip playback, paused pose holds); `PointerHit.uv` + `pointerService.sampleSurface()` for material-aware picking; `ctx.scene.entity.paint` runtime paint layer, auto-rendered via a per-instance canvas texture with no per-game wiring; remote-player appearance tint recolors the shell's default capsule (#151).
|
|
880
|
+
- **Transport pipe seam** (`@jgengine/ws/pipe`) — `createWsBackend` runs over any bidirectional string channel, not just a raw `WebSocket`: `TransportPipe`/`TransportPipeHandlers`/`TransportPipeFactory`, with `webSocketPipe(url, webSocketFactory?)` as the default. `createWsBackend({ userId, url?, pipe? })` — `url` stays the common case, `pipe` opens the seam to socket.io, WebRTC, and in-process loopback below.
|
|
881
|
+
- **Browser-safe authoritative host** — `createGameHost` and `memoryPersistence` moved from `@jgengine/node` to `@jgengine/ws/host` (zero Node dependencies; `@jgengine/node` re-exports both unchanged from `@jgengine/node/host` / `@jgengine/node/persistence`, so existing imports keep working). `@jgengine/ws/hostRouter`'s `createHostRouter({ host, authenticate?, poseRules?, positionHistoryMs?, chatRateLimit?, chatHistoryLimit?, chatMaxBodyLength?, now? })` extracts the ws wire-protocol session logic out of `createGameWsServer` into a transport-agnostic `HostRouter` (`connect(transport) → { handleRaw, close }`, `rewind`, `close`); `@jgengine/node`'s `createGameWsServer` is now a thin binding of this router onto the `ws` npm package, same public API. `loopbackPipe(router): TransportPipeFactory` connects a `createWsBackend` straight into an in-process router.
|
|
882
|
+
- **Socket.IO transport** — `@jgengine/ws/socketIoPipe` (`SocketIoLikeSocket` structural type, `socketIoPipe(socket)`, `createSocketIoBackend({ socket, userId, … })`) and `@jgengine/node/socketIoServer` (`attachGameSocketIoServer({ io, host, …router options }): { rewind, close }`, structural `SocketIoLikeServer`/`SocketIoLikeServerSocket`) ride the existing ws JSON protocol over socket.io's `send`/`message` frames — no socket.io dependency in either package's types. New `socketIo({ topology?, url? })` adapter in `@jgengine/core/runtime/adapter`.
|
|
883
|
+
- **WebRTC peer-to-peer** (`@jgengine/ws/peer`) — one browser tab hosts, authoritatively, with no server process. `createPeerHost({ userId, host?, runtimes?, persistence?, tickMs?, router?, rtc? })` runs a `GameHost` + `HostRouter` in the host tab (`backend` is the host player's own loopback connection, `accept(offerCode) → Promise<answerCode>` per guest); `createPeerGuest({ userId, token?, rtc? })` offers/connects from the joining side. `encodePeerSignal`/`decodePeerSignal` turn SDP into copy-pasteable base64url codes for manual cross-machine signaling; `broadcastChannelSignaling(room)` automates it for same-origin multi-tab play, with `announcePeerHost`/`joinPeerSession` wiring host/guest to a `PeerSignaling` in one call. New `p2p({ topology?, room? })` adapter (topology defaults `"private"`).
|
|
884
|
+
- **LAN adapter + Fly sugar** — `lan({ topology?, port?, path? })` in `@jgengine/core/runtime/adapter` resolves through `@jgengine/shell/multiplayer`'s `resolveShellMultiplayer` to `ws(s)://<page hostname>:<port ?? 8080><path ?? /ws>` derived from `window.location`, so any browser on the LAN auto-connects to whichever machine served the page — no URL configuration. `fly({ app, topology?, path? })` is `ws` sugar for a Fly.io deploy: resolves to `wss://<app>.fly.dev<path ?? "/ws">`. `apps/dev`'s Vite server now listens on the network (`server: { host: true }`) and exposes `?p2p=host` / `?p2p=join` query params wired through the new `resolvePeerShellMultiplayer({ gameId, role, room?, userId?, feedActions? })`.
|
|
885
|
+
- `ws()` (`@jgengine/core/runtime/adapter`) gained an optional `url` field, carried through by `resolveShellMultiplayer` (`args.url ?? adapter.url ?? default`).
|
|
886
|
+
|
|
887
|
+
### Removed
|
|
888
|
+
|
|
889
|
+
- **The `gameui` component kit** (`@jgengine/react/gameui`) — the themed HUD kit (bars, slots, feedback, meters, panels, screens, reticles, icons) and its `GameUiThemeProvider`/`useGameUiTheme` theming have been removed. **Breaking** for anyone importing `@jgengine/react/gameui` (or its subpaths/barrel). The components now ship as installable shadcn registry items at `https://jgengine.com/r/<name>.json` (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), styled with Tailwind + `--jg-*` CSS variables instead of a theme object. The icon catalog (`GameIcon`, `iconForAction`, `iconForItemId`, `isGameIconName`, `GameIconName`) moved to `@jgengine/react/gameIcons`. To theme, set the `--jg-*` variables on a wrapper element (the registry's `jg-theme` presets mirror the old `ember`/`synthwave`/`fieldkit` palettes) instead of wrapping in `GameUiThemeProvider`.
|
|
890
|
+
|
|
891
|
+
### Docs
|
|
892
|
+
|
|
893
|
+
- Added [CREDITS.md](CREDITS.md) crediting [achrefelouafi](https://github.com/achrefelouafi) for the MIT Three.js reference projects behind the procedural building, water, rain, and snow renderers, with links from the root, `@jgengine/core`, and `@jgengine/shell` READMEs.
|
|
894
|
+
|
|
895
|
+
## 0.7.0
|
|
896
|
+
|
|
897
|
+
The engine-gaps release — 22 system-level additions across turn/tactics, cards & boards,
|
|
898
|
+
crafting, survival, navigation & AI, camera rigs, physics & vehicles, traversal &
|
|
899
|
+
destruction, world items & building, map/HUD/ping, combat feel & abilities, audio,
|
|
900
|
+
interaction, sensors, embodiment, multiplayer depth, and objective/session machines.
|
|
901
|
+
**Every 0.6.0 API is unchanged** — the whole release is additive, so upgrading is a version bump.
|
|
902
|
+
|
|
903
|
+
### Migrate
|
|
904
|
+
|
|
905
|
+
- Bump every `@jgengine/*` dependency to `^0.7.0` (the eight packages version in lockstep).
|
|
906
|
+
- No code change is required — 0.7.0 only adds surface; no 0.6.0 API moved or was removed. Existing games keep the orbit/first-person camera, single-player-entity control, and every existing primitive exactly as before.
|
|
907
|
+
- Opt into any new system by importing it directly: a camera rig via `camera.rig` + its config block; a `sensor/*` probe with `@jgengine/shell/vision` renderers; an `ai/*` director over the `nav/` navmesh; `turn/*` + `tactics/*` for turn-based/grid games; `cards/*` + `board/*` for deckbuilders; `crafting/*` for recipes/production/farming; `survival/*` + `world/{envField,weather,realm}` for survival; `combat/{abilityKit,animationState,defensiveWindow,…}` for action feel; `physics/{vehicleBody,traversal,structure,ragdoll,…}` for vehicles/destruction; `session/*` for contested/round/downed/ring/extraction machines; and `multiplayer/*` for lag-comp/hidden-commit/matchmaking.
|
|
908
|
+
- `entityStore.update()` now also accepts `name`, so possession/form can retarget an instance's catalog id without despawn/respawn — no action needed unless you relied on `name` being immutable.
|
|
909
|
+
|
|
910
|
+
### Added
|
|
911
|
+
|
|
912
|
+
- **Turn-based & tactics stack** — six pure, renderer-free `@jgengine/core` primitives for turn-based, grid-tactics, and card games (XCOM, BG3, Into the Breach, Slay the Spire, Marvel Snap, Tactical Breach Wizards, Divinity surfaces).
|
|
913
|
+
- `@jgengine/core/turn/turnLoop` (`createTurnLoop`) — initiative machine with configurable `phases` and per-turn action-economy `pools` (`{ id, max, start? }`) that reset when a participant enters their turn; `advanceTurn`/`advancePhase`/`advanceRound`, `spend`/`canSpend`/`gain`/`refill`, and `setOrder`/`addParticipant`/`removeParticipant`. Covers both Slay-the-Spire single-energy resets and BG3's Action/Bonus/Movement/Reaction set.
|
|
914
|
+
- `@jgengine/core/turn/commit` (`createCommitController`, also `turnLoop.commit`) — three commit modes: `immediate`, `simultaneous` (sealed hidden submissions → `reveal()` on `allReady()`), and `rewind` (visible `pending()` → `rewind()`/`commit()`).
|
|
915
|
+
- `@jgengine/core/tactics/tacticalGrid` (`createTacticalGrid`) — tile occupancy, `reachable(from, budget)` flood-fill, `path`, and `push(id, dir, { distance, chain })` discrete knockback-to-tile with chained collisions (Into the Breach).
|
|
916
|
+
- `@jgengine/core/tactics/predictiveQuery` (`predictAreaEffect`/`predictArcEffect`/`predictTiles`) — a would-this-effect-hit query for pre-commit overlays and enemy-intent telegraphs, reusing the exact AoE/LoS targeting behind `ctx.scene.entity.effect` (new shared `resolveAreaTargets` in `combat/effects`) so predictions match what the effect actually drains.
|
|
917
|
+
- `@jgengine/core/tactics/snapshot` (`createSnapshotStore`, `deepClone`) — cheap, repeatable turn-undo over registered `capture()/restore()` slices (the grid, surfaces, and turn loop all qualify), with a `push()/pop()` undo stack.
|
|
918
|
+
- `@jgengine/core/tactics/surface` (`createSurfaceLayer`) — a stateful tile surface layer with its own `tick(dt)` and a data-driven combination matrix (`reactions: [{ when: [a, b], result }]`) — grease+fire, water+lightning — distinct from terrain/water.
|
|
919
|
+
- `@jgengine/core/combat/effects` now exports `resolveAreaTargets` (+ `AreaTarget`, `AreaTargetInput`), the shared in-radius→LoS→falloff→accept targeting that both `applyEffect` and the predictive query run, guaranteeing parity by construction. No behavior change to `effect`.
|
|
920
|
+
|
|
921
|
+
Pure, renderer-free primitives for card, board, and deckbuilder games — they sit **beside** the slot inventory, never replace it.
|
|
922
|
+
|
|
923
|
+
- **`@jgengine/core/cards/cardPile`** — a `cardPile` of named ordered zones (deck/hand/discard/exhaust) with seeded `shuffle`, `draw(n)` (hand limits + reshuffle-on-empty), `discard`/`exhaust`/`move`. Reuses the engine's seeded RNG (`pileRng`), so shuffles are deterministic under a seed. Slay the Spire, Balatro.
|
|
924
|
+
- **`@jgengine/core/cards/modifierPipeline`** — an ordered `{ source, apply(value) → value }` pipeline (`runPipeline` / `createModifierPipeline`) with an inspectable per-step `trace` (before/after/changed) for Balatro-style scoring readouts. Generic over the scored value.
|
|
925
|
+
- **`@jgengine/core/board/laneBoard`** — N lanes, per-side power aggregate + optional per-lane `LaneRule` modifier, with `laneOutcome`/`boardTotals`/`lanesWon`. Marvel Snap, Inscryption.
|
|
926
|
+
- **`@jgengine/core/board/timelineBoard`** — N slots each on an independent cooldown, `tick(dtMs)` resolving fires in expiry order (then slot index), multiple fires per slot per tick. The Bazaar auto-battlers.
|
|
927
|
+
- **`@jgengine/core/inventory/shapedGrid`** — a polyomino inventory variant: `Footprint` placement, `rotateFootprint`, `canPlace` overlap/bounds check, plus `gridAdjacencyQuery` (orthogonal/diagonal neighbors) feeding synergy effects, and `cellFromPoint` for pointer→cell snap. Backpack Hero, Tetris inventory.
|
|
928
|
+
- **`@jgengine/react/dragLayer`** — a 2-D UI-space drag/rotate/drop/snap gesture layer over the above: `useDragLayer`, headless `DraggableCard` (right-click rotate), `DropZone` (cell snap + active state), `DragGhost`.
|
|
929
|
+
|
|
930
|
+
- `@jgengine/core/crafting/recipe` — a recipe graph primitive: `RecipeDef { inputs, outputs, seconds?, station?, stationRange?, requires? }` turns inputs + an optional required-workstation-in-range + time into outputs. `craft` / `canCraft` consume and produce on an `InventoryState` atomically (rejecting `missing-inputs` / `no-station` / `locked` / `no-output-space`); `stationSatisfied` does the range check against placed `{ catalogId, position }` stations; `createRecipeGraph` indexes recipes by `producing` / `using` / `category`. For Valheim/Enshrouded-style workbench tiers, Tarkov hideout stations, Palworld base craft. (#71)
|
|
931
|
+
- `@jgengine/core/economy/techTree` — a prerequisite-gated tech tree that **generalizes flat `unlocks`** rather than duplicating it: `TechNodeDef extends UnlockDef` adds `requires` (prereq ids), a `recipe` payload, and `grants`. `createTechTree(defs)` wraps `createUnlocks` and gates `unlock(userId, id)` on prerequisites, exposing `available` (frontier) and `recipes` (payloads unlocked). Flat unlocks are just nodes with no `requires`. For Once Human Memetics / Abiotic Factor branching trees. (#72)
|
|
932
|
+
- `@jgengine/core/crafting/production` — `productionBuilding({ inputs, outputs, rate, power? })` plus `tickProduction`, which consumes buffered inputs and emits outputs continuously through game-time `dt` (so pause/fast-forward apply for free). `feedProduction` / `drainOutput` are the buffer I/O, `advanceTransport` slides items along a conveyor path, `resolvePowerGrid` powers demands greedily against a supply. For Palworld/Satisfactory/Factorio automation. (#74)
|
|
933
|
+
- `@jgengine/core/crafting/crop` — a `cropTile` soil-and-growth state machine (`tillTile` / `plantCrop` / `waterTile` / `advanceCropDay` / `harvestCrop`, with regrow and daily-water rules) that advances stages on the simClock day tick, plus `applyToolToTiles(tiles, center, pattern, apply)` with `squarePattern` / `diamondPattern` / `rectPattern` for watering-can/hoe AoE under the cursor. `createCropField` wraps a tile grid; `createDayTicker` reads day rollovers off `ctx.time.calendar()`. For Stardew/Coral Island/Palia farming. (#75)
|
|
934
|
+
- **Survival & environment layer.** Renderer-free `@jgengine/core` primitives for survival games (Project Zomboid moodles, The Long Dark / Green Hell survival, DayZ / Tarkov per-limb health, weather-driven games, Nightingale realm cards), plus the `@jgengine/shell` fire visuals.
|
|
935
|
+
- `@jgengine/core/survival/decayMeter` — named `decayMeter`s (`createDecayMeterSet`) that drain/recover on game-time `dt`, refill from consumables/actions, raise threshold moodles, and take environment-driven rate modifiers (cold speeds warmth loss, toxic biomes drop oxygen).
|
|
936
|
+
- `@jgengine/core/survival/moodle` — a stacking status-effect display distinct from numeric bars: `stackMoodles(...)` folds meter/ailment/buff moodles worst-first, and `createMoodleStack()` holds timed buffs (Valheim-style concurrent food buffs) that expire on `tick(dt)`.
|
|
937
|
+
- `@jgengine/core/survival/regionHealth` — a multi-region health component (`createMultiRegionHealth`): per-part pools with vulnerability + vital death, a stacking ailment queue (bleed/fracture) that drains over time, and per-injury treatment items (`treat("bandage")`). Shares the moodle display via `ailmentMoodles()`.
|
|
938
|
+
- `@jgengine/core/world/envField` — sampleable environment fields (`createEnvironmentField`): temperature, wetness, light-exposure, and ambient-light at any world position and time, with sky occluders, heat sources, and a day/night sun cycle. Meters and spawn gating read it renderer-free.
|
|
939
|
+
- `@jgengine/core/world/weather` — weather → gameplay modifiers (`resolveWeather` → grip/visibility/structure-damage/chill/ignition/spread) over a game-owned table, plus a **coarse cellular** fire-spread grid (`createFireGrid`, not a fluid solver): wind-biased propagation, fuel burn-through, firebreaks, and rain/wetness suppression.
|
|
940
|
+
- `@jgengine/core/world/realm` — runtime realm composition (`composeRealm`): assemble a played instance from a deck of modifier cards (major biome + minor weather/day-length/spawn cards) that override environment params and spawn tables, recomposing a sampleable environment field on top of the weather hooks.
|
|
941
|
+
- `@jgengine/shell/weather` `FireSpreadLayer` — renders a `FireGrid`'s burning flames + scorched cells; a `survival-demo` game wires the whole stack (moodle stack, per-part body panel, condition meters, weather readout, spreading fire).
|
|
942
|
+
- `@jgengine/core/audio/audioFalloff` — the audio contract + pure distance→gain math. `SoundDef`/`AudioBusDef` catalog types, `computeFalloffGain(distance, config)` (`"linear" | "inverse" | "none"` curves), and `resolveEmitterGain`/`distance3` helpers. No Web Audio in core — this is the tested math the shell plays from.
|
|
943
|
+
- `@jgengine/core/time/beatClock` — a BPM tick signal, separate from `simClock`. `createBeatClock({ bpm, beatsPerBar? }, onBeat?)` advances on **game-time** `dt` and fires `onBeat` per crossed beat; `createBeatInputBuffer` auto-corrects an off-beat press to fire on the next beat tick (`nextBeatTime` is the underlying pure quantization). For rhythm-quantized combat (Hi-Fi Rush–style).
|
|
944
|
+
- `@jgengine/shell` positional audio — `PlayableGame.audio = { sounds, buses? }` + `entitySounds`/`objectSounds` (kind/catalog-id → sound id, same convention as `entityModels`) declare looping positional emitters; the shell (`shell/audio/audioEngine`, `shell/audio/AudioComponents`) owns the `AudioContext`, attaches the listener to the camera every frame, and drives emitter gain from the core falloff curve. Music/SFX buses are plain per-bus gain nodes. No per-game Web Audio glue.
|
|
945
|
+
- `@jgengine/ws/voiceChannel` — a thin, coarse voice-channel router riding the same transport/presence model (channel/falloff model only, no WebRTC media). `createVoiceChannelRouter(channels?)`: `join`/`leave` any number of channels at once, `updatePosition` for proximity falloff (reusing the core audio falloff curve), `setMuted`, and `resolveRoutes(listenerUserId)` — one `{ fromUserId, channelId, gain }` per shared channel, so a positional proximity channel and a flat-gain walkie/crew channel resolve simultaneously and independently.
|
|
946
|
+
- `@jgengine/core/interaction/skillCheck` — a moving-target-zone, timed minigame (`evaluateSkillCheck`, `skillCheckMarkerPosition`, `skillCheckZoneAt`) for casting/reeling, active-reload, and production minigames from an `item.use` handler.
|
|
947
|
+
- `@jgengine/core/interaction/qte` — a timed-prompt QTE sequencer (`evaluateQteSequence`, `pendingQteStep`, `qteProgress`).
|
|
948
|
+
- `@jgengine/core/scene/captureCheck` — `captureChance`/`rollCapture`, an hp%+catchPower→probability formula for capture/tame mechanics.
|
|
949
|
+
- `@jgengine/core/scene/roster` — `createRoster()`, a persisted per-owner roster (capture/release/list/setEquipped) wired onto the runtime as `ctx.game.roster`, distinct from the ephemeral `game.social.party`.
|
|
950
|
+
- `@jgengine/core/stats/rollCheck` — `rollCheck`, a d20-style roll vs. DC with advantage/disadvantage and critical detection.
|
|
951
|
+
- Dialogue choices can now gate a branch behind a roll: `DialogueChoice.check` (+ `onSuccess`/`onFailure`) on the `@jgengine/react/components` `DialogueDef`/`DialogueChoice` types, resolved via `resolveDialogueInvoke`.
|
|
952
|
+
- `@jgengine/react` gained `SkillCheckBar`, `QteTrack`, and `CaptureOdds` headless minigame UI components, plus a `useRoster(userId?)` hook and extra `DialogueBox` slot classNames (`lineClassName`, `speakerClassName`, `choicesClassName`, `choiceClassName`, `checkClassName`).
|
|
953
|
+
- **Navigation & pointer-driven input.** A renderer-free foundation for click-to-move, RTS unit command, and pointer verbs.
|
|
954
|
+
- `@jgengine/core/nav/navGrid` — a walkable grid + A* pathfinding (`createNavGrid`, `findPath`, `smoothPath`); blocked start/goal snap to the nearest walkable cell, paths are string-pulled, and one graph feeds both click-to-move and AI routing.
|
|
955
|
+
- `@jgengine/core/nav/pathFollow` — an authored-polyline mover for tower-defense creeps needing no navmesh (`createPathFollow` + pure `advancePathFollow`); `pathFromNav` lifts an A* route so the same follower drives click-to-move.
|
|
956
|
+
- `@jgengine/core/input/pointer` — the renderer-free `PointerHit` contract (`{ point, normal, entity, object }`) plus `aimToPoint` / `moveTargetFromHit` / `groundOf` so gameplay consumes cursor hits (aim, move-to, routing) without three.js.
|
|
957
|
+
- `@jgengine/core/scene/selection` — pure box-select math (`createSelectionSet`, `screenRect`, `selectWithinRect`, `isMarquee`).
|
|
958
|
+
- `@jgengine/core/interaction/contextMenu` — `contextVerb` / `buildContextMenu` / `contextVerbInput`; catalog entities/objects carry `verbs` for right-click menus.
|
|
959
|
+
- `PlayableGame.pointer` config (`moveCommand`, `select`, `orderCommand`, `contextMenu`, `aim`) — the `@jgengine/shell` `GamePlayerShell` casts the cursor (`pointer.worldHit()`), renders a drag-marquee + right-click verb menu, routes primary-ability aim to the cursor, and remaps orbit to middle-drag so the left button drives verbs.
|
|
960
|
+
- **AI over the navmesh.** A renderer-free `ai/` domain for directors, aggro, jobs, and crowds — all ticking on game-time `dt` (the `ctx.time` simClock delta, so pause/fast-forward come free) and routing over the `nav/` navmesh.
|
|
961
|
+
- `@jgengine/core/ai/spawnDirector` — a budget/escalation spawn director for wave shooters and difficulty directors (`createSpawnDirectorState` + pure `advanceSpawnDirector`). Per-`WaveManifest` budgets spent on weighted `SpawnEntry`s (`cost`/`weight`/`minWave`) under a `maxAlive` cap; `duration` auto-advances waves (or `advanceWave` on clear); budget trickles, ramps with sim-time (`escalationPerSecond`), scales per player, and surges on `raiseAlert`. Seeded/deterministic; `pickSpawnPoint` biases placement toward players.
|
|
962
|
+
- `@jgengine/core/ai/threat` — an aggro/threat table (`createThreatTable`): accumulate/`decay` per source, `highest({ current, stickiness })` for sticky MMO target selection feeding `scene/targeting`, `ranked` for a threat meter.
|
|
963
|
+
- `@jgengine/core/scene/behaviors` gained `patrol({ waypoints, speed, loop? })` — a waypoint route as a `BehaviorDescriptor` on top of `wander`, driven by `pathFollow` over `findPath`.
|
|
964
|
+
- `@jgengine/core/ai/jobBoard` — a task/job queue NPCs pull from (`createJobBoard`): `post`/`claim`/`assign`/`release` plus a per-tick `advance(worker, dt, { distanceToStation })` state machine (`travelling → working → done`, `repeat` for production loops) that reports on completion. For colony/companion assignment (Palworld, Schedule I, Sons of the Forest).
|
|
965
|
+
- `@jgengine/core/ai/crowd` — a flow field with congestion + POI routing for management sims (`computeFlowField` Dijkstra steering without per-agent A*, `createCrowdField` per-cell occupancy whose `penalty()` reroutes flow around crowds, `selectPoi` weighting appeal/proximity/capacity with a `findPath`-length distance override).
|
|
966
|
+
- **Dropped-item world entities & loot filter.** Loot no longer has to teleport straight to inventory — it can lie on the ground as a rarity-coded, beam-and-labelled `worldItem` you walk up to and grab (Borderlands/Diablo loot beams, ARPG ground loot, Apex/Lethal-Company pick-ups).
|
|
967
|
+
- `@jgengine/core/game/worldItem` — the `worldItem` scene-entity model (position + item ref + rarity): `ctx.scene.worldItem.spawn / get / list / nearestInRadius / pickup` (a third scene bucket alongside object/entity, never merged into inventory). Pure helpers `createWorldItemStore`, `resolveDeathDrops` (splits rolled drops into scattered ground items vs direct grants), `scatterOffset` / `scatterPosition` (the on-death scatter impulse), `selectNearestWorldItem` (pickup-radius nearest selection), and `resolveWorldItemPresentation` (rarity baseline + filter overlay → render binding). Emits `worldItem.dropped` / `worldItem.picked_up`.
|
|
968
|
+
- `onDeath.dropMode: "world"` (+ optional `onDeath.scatter`) routes an entity's death drops through scattered ground items instead of granting straight to the killer; currency still grants directly. Item catalog entries carry `rarity` / `baseType` read by the render binding + filter.
|
|
969
|
+
- `@jgengine/core/game/lootFilter` — a PoE/Last-Epoch-style rule evaluator (`lootFilter`, `evaluateLootFilter`) keyed on rarity + base type + affix-tier that hides/recolors/beams/labels ground items. Rules are **data the game supplies**; first match wins, field-by-field over the rarity baseline. The render binding is engine-owned.
|
|
970
|
+
- `PlayableGame.worldItem` config (`rarityStyle`, `filter`, `pickupRadius`, `beamHeight`) + `pointer.grabWorldItems` — `@jgengine/shell`'s `WorldItems` draws the rarity beam/color/label per drop and `GamePlayerShell` grants + despawns on a click within pickup radius (using `pointer.worldHit()`). `@jgengine/react`'s `useWorldItems()` / `useNearestWorldItem(radius)` drive a HUD pickup prompt.
|
|
971
|
+
|
|
972
|
+
- **Interactive placement, building & terraform.** Data-only placement becomes the build tooling of Valheim/Enshrouded/The Sims/Fortnite/Dinkum/Palia, all pure `@jgengine/core/world` driven by `pointer.worldHit()`, with shell renderers for the ghost/tint/brush.
|
|
973
|
+
- `@jgengine/core/world/placementController` — `createPlacementController(footprint)` owns the placement ghost: `hover(hit)` → `PlacementPreview` (valid/invalid tint wrapping `validatePlacement`), `rotate`, grid/free/surface `snapMode`, `commit` → `PlacementCommit`.
|
|
974
|
+
- `@jgengine/core/world/connectors` — typed connector sockets with `snapToNearest` snap-to-nearest-compatible (`socketsCompatible`, `worldSockets`).
|
|
975
|
+
- `@jgengine/core/world/support` — `solveSupport` walks the connector graph to ground (`supported`/`unsupported`/hop-`distance` for white→red decay); `toDebrisBodies` sinks collapsed pieces into the `PhysicsWorld`.
|
|
976
|
+
- `@jgengine/core/world/walls` — `createWallDrawTool` (drag walls → auto-enclose → `footprintFromWalls` → `autoRoof` hip/gable/flat) plus `createSurfacePaint` for per-tile floor/wall surfaces.
|
|
977
|
+
- `@jgengine/core/world/placedStructureStore` — `createPlacedStructureStore` save/load/select/move/delete with a `snapshot`↔`load` round-trip that survives reload.
|
|
978
|
+
- `@jgengine/core/world/terraform` — `createEditableTerrain({ bounds, base, cellSize })` makes a `TerrainField` you can **write back to** via `apply(edit)` (raise/lower/flatten/paint), and `createTerraformBrush` is the cursor tool. This height-offset grid is the shared terrain-edit write-back pattern.
|
|
979
|
+
- `@jgengine/core/world/buildPermissions` — `createPlotPermissions` (per-plot/guild `BuildRole` edit authority) + `createContributionPool` (co-op pooled-resource contribution model).
|
|
980
|
+
- `@jgengine/shell` renderers: `structures/PlacementGhost` (tinted ghost), `terrain/EditableGround` (renders an `EditableTerrain` with surface paint), `terrain/TerraformBrushCursor` (brush ring); the `builder-sandbox` demo game wires them to `pointer.worldHit()`.
|
|
981
|
+
- **Map, fog of war & contextual ping.** Minimap/world-map/fog/compass + a squad ping verb, built on renderer-free core state, a shell terrain bake, and react HUD components. (Stacked on the pointer foundation above.)
|
|
982
|
+
- `@jgengine/core/world/markers` — a reactive `createMarkerSet()` of `MapMarker`s (objective/entity/loot/ping) with `add`/`remove`/`query`/`prune`/`subscribe`; `DEFAULT_MARKER_KINDS` supplies content-agnostic colors + glyphs.
|
|
983
|
+
- `@jgengine/core/world/fog` — reveal-on-event fog: `createFogField({ bounds, cellSize })` with `reveal` (dig/act) and `revealAlong` (walked trail); revealed cells stay revealed and render from a stable `cells()` snapshot.
|
|
984
|
+
- `@jgengine/core/world/minimap` — pure projection + bearings (`projectToMinimap`, `clampToMinimapEdge`, `compassBearing`, `headingToBearing`, `bearingToCardinal`, `relativeBearing`).
|
|
985
|
+
- `@jgengine/core/game/ping` — `classifyPing(hit, …)` (hostile → enemy, tagged object → its category, ground → location) + `createPingSystem` that classifies, drops a categorized marker, and broadcasts a `PingPayload` over the existing party feed under `PING_FEED_ACTION`. `PlayableGame.pointer.pingCommand` binds the `ping` action → `worldHit()` → your command.
|
|
986
|
+
- `@jgengine/react` — `useMarkers` / `useFog` hooks and `Minimap` / `Compass` / `WorldMap` headless components (bind a core `MarkerSet`/`FogField`, override the `kindStyles` palette).
|
|
987
|
+
- `@jgengine/shell/map` — `bakeTerrainMap` (top-down terrain image for the map background) and `MapMarkerBeacons` world-space beacons; the `extraction-map` demo game wires the whole loop.
|
|
988
|
+
- **Physics joints & constraints** — `PhysicsWorld` gains `hingeJoint`/`fixedJoint`/`distanceJoint`/`springJoint(JointOptions)` between two bodies or a body and a fixed world point, plus `removeJoint`, `setJointAnchor` (move a follow point), `setJointRest`, and `readJointSegments`. The sim is translational: `hinge`/`fixed` pin the shared anchor, `distance` holds a separation, `spring` drives toward `restLength` with stiffness/damping. Foundation under vehicle suspension, ragdolls, grapples, and carry. Tune the buffers with `jointCapacity` / `jointCorrection` in `PhysicsWorldConfig`.
|
|
989
|
+
- **Physics collision → gameplay-event hook** — `PhysicsWorld.onCollision(listener, minApproachSpeed?)` delivers each impacting contact as a reused `CollisionEvent { a, b, nx, ny, nz, approachSpeed, impulse }` during `step`. The seam crash-damage and destruction read; contacts otherwise stay inside the sim.
|
|
990
|
+
- **`@jgengine/core/physics/ragdoll`** — `createRagdoll(world, { bones, links, balance? })` builds a jointed multi-body character on the joint API: floppy by default, or active-ragdoll when a balance motor drives the root toward a target height (`Ragdoll.balance(dt, moveX?, moveZ?)`), with `centerOfMass`, `applyImpulse`, `remove`.
|
|
991
|
+
- **`@jgengine/core/physics/carryable`** — `Carryable` grabs a physics body to a moving follow point via a spring constraint (the raycast pick is the caller's), supports shared multi-owner carry (follow point = owner average), drop/throw, and `carrySpeedMultiplier(mass, capacity, owners)` encumbrance.
|
|
992
|
+
- **`@jgengine/core/physics/forceVolume`** — `ForceVolume` (impulse/velocity/accelerate trigger region, `once` for boost pads vs continuous fans) and `PlatformCarry` (carry bodies standing on a moving platform by its per-`step` delta).
|
|
993
|
+
- **`@jgengine/core/physics/spatialGrid`** — `SpatialGrid`, a broad-phase uniform grid over the x/z plane, separate from the rigid-body sim, for cheap same-tick proximity across hundreds–thousands of simple movers: `rebuild(count, xs, zs)` then `queryCircle` (swarm enemies hitting a player/AoE) or `forEachPair` (mutual separation).
|
|
994
|
+
- **`@jgengine/shell/world/InstancedJoints`** — debug LineSegments overlay drawing a `PhysicsWorld`'s active joints from `readJointSegments`.
|
|
995
|
+
- **`@jgengine/core/physics/traversal`** — `Grapple` (a fired-anchor rope on the joint API: `fire`/`reel`/`payOut`/`moveAnchor`/`release` for grapple, zipline, and swing traversal) and `Glide` (reduced-gravity, forward-thrust wingsuit/glider via `apply(dt, steerX, steerZ)` before `step`). Grapple/zipline/glide (Sekiro, Deep Rock, Enshrouded) over the shared physics sim.
|
|
996
|
+
- **`@jgengine/core/world/carve`** — runtime destructible terrain. `VoxelVolume` is an editable dense voxel grid with `carve`/`deposit` sphere ops honouring per-material `strength` against a tool (Deep Rock dig, Astroneer deposit); `CarvableField` / `carvableTerrain(base)` writes craters and mounds back into any `TerrainField`'s height so ground-snap, collision, and the terrain mesh all see the deformation (Helldivers 2 craters).
|
|
997
|
+
- **`@jgengine/core/physics/structure`** — `StructureGraph`, a structural-integrity graph over a building: nodes (pieces), load-bearing edges, anchored foundations. `damage`/`damageEdge`/`severEdge` recompute reachability to an anchor and return one `CollapseEvent` of every newly-disconnected piece; `toDebris(world, event)` sinks the fallen pieces into a `PhysicsWorld` as rigid bodies. Coarse by design — the collapse **event** replicates, not each fragment (The Finals, Rainbow Six).
|
|
998
|
+
- **`@jgengine/shell/terrain/CarvedTerrain`** — meshes any `TerrainField` (a `CarvableField` with its craters/mounds) into a vertex-coloured deformed ground; `createFieldGroundGeometry` is the underlying geometry builder.
|
|
999
|
+
- **Analog axis input** (`@jgengine/core/input/axisInput`) — `AxisInput { throttle, brake, steer, handbrake }` continuous channel, distinct from the digital action bindings. `AxisChannel` ramps held keys into pedal-like analog values (`sample`) or takes a raw gamepad axis (`setAnalog`); `DRIVE_AXIS_BINDINGS` is a ready WASD/arrow map.
|
|
1000
|
+
- **Vehicle controller** (`@jgengine/core/physics/vehicleBody`) — `createVehicleBody(world, config)`: chassis body + per-wheel suspension raycast + spring-damper (on `springJoint`) + a `GripCurve` (`sampleGripCurve`) that bleeds lateral velocity for cornering and, under handbrake, drift. Driven by an `AxisInput`; still a colliding body, so contact feeds crash damage. (Rocket League, Trackmania, Wreckfest.)
|
|
1001
|
+
- **Buoyant boat** (`@jgengine/core/physics/buoyancy`) — `createBuoyantBody(world, { body, water, … })` floats a body on a CPU `waterSurface` (Archimedes per hull point + water drag) and, given an `AxisInput`, drives it as a boat (thrust + yaw + keel).
|
|
1002
|
+
- **Mount / rideable controller** (`@jgengine/core/scene/mount`) — `createMountController()` transfers camera + input to a driven entity with its own movement kit: `register({ id, kit, seats })`, `mount`/`dismount`, `cameraTarget`/`driveTarget`, `driver`/`occupants`. Control seat drives, passenger seats ride (multi-seat shared vehicles). (Palworld, V Rising, Sea of Thieves.)
|
|
1003
|
+
- **Crash-damage stages** (`@jgengine/core/physics/damageZones`) — `createDamageModel({ zones, disableAt })` maps accumulated `onCollision` impulse to coarse discrete stages (`absorb`/`routeCollision`), with an optional `detachStage` (part → debris) and a `disabled` threshold. Coarse stages, not soft-body. (Wreckfest.)
|
|
1004
|
+
- **Race state machine** (`@jgengine/core/game/race`) — `raceTrack({ checkpoints, laps })` + `createRaceState({ track, win })` emit `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished` on game time, keep split times, resolve a pluggable win condition (`firstPastPost`, `topK`, `everyoneFinishes`, `lastStanding`), and `resetToCheckpoint`. (Trackmania, Mario Kart, Fall Guys.)
|
|
1005
|
+
- **Lag-compensated hit registration** (`@jgengine/core/multiplayer/lagCompensation`) — `createPositionHistory({ historyMs })` retains an N-sample position ring per entity; `rewindTimestamp(now, rtt, interpDelay)` and `resolveHitscan(history, targets, ray, atMs)` (ray-sphere) register a shot where the target *was* at the shooter's perceived time. Coarse server-side rewind, not full rollback. The `@jgengine/node` ws host records accepted presence poses and exposes `server.rewind({ serverId, atMs })` (`positionHistoryMs` option). (Valorant, Apex.)
|
|
1006
|
+
- **Simultaneous hidden-commit + reveal** (`@jgengine/core/multiplayer/simultaneousCommit`) — `createCommitRound({ participants })`: each player `seal`s a sealed action, nothing is readable until `allSealed()`, then `reveal()` returns commits in deterministic participant order (independent of network arrival) for `resolveCommits`. (Marvel Snap.)
|
|
1007
|
+
- **Combat-snapshot replay** (`@jgengine/core/multiplayer/combatSnapshot`) — `serializeBoard({ ownerId, units, stats, seed })` deep-freezes a build into a portable `BoardSnapshot`; `replayCombat(a, b, rules)` resolves it deterministically (seeded PRNG) against a live opponent's snapshot — an async-PvP primitive distinct from the live-sync adapters. (The Bazaar.)
|
|
1008
|
+
- **Shared-vehicle stations** (`@jgengine/core/scene/stationClaim`) — `createStationClaim(controller?)` layers facet stations (`steer`/`sails`/`cannon`) on `scene/mount`: `register({ id, kit, stations })`, `claim`/`release`, `controllerOf(vehicleId, facet)`, `facetOf`, `openFacets`, `crew`. One control station drives the hull; the rest ride but command their facet. (Sea of Thieves.)
|
|
1009
|
+
- **Shared / group wallet** (`@jgengine/core/economy/sharedWallet`) — `createWalletBook()` holds per-`WalletScope` balances (`userScope`/`groupScope`) beside the per-user `economy/wallet`: `grantTo`/`chargeFrom`/`balanceIn`, with a `contributionOf`/`contributorsOf` ledger tracking who funded a shared pool. (Schedule I company funds, Lethal Company quota.)
|
|
1010
|
+
- **Session matchmaking** (`@jgengine/core/multiplayer/matchmaking`) — data-driven browse/filter (`browseSessions`, `matchesFilter`, `quickMatch`) hiding private/closed lobbies, plus join-by-code (`findByJoinCode`, `normalizeJoinCode`, `generateJoinCode`). The node host carries generic `SessionAttributes` (`label`/`mode`/`visibility`/`joinCode`/`tags`) on `GameServerRecord`/`ServerListing` and gains `browseServers` + `joinByCode`; the ws backend exposes `browse` / `joinByCode` / `createSession`. (Fortnite island browse, Web Fishing code lobbies.)
|
|
1011
|
+
- **Camera rig library** (`@jgengine/shell`, config types in `@jgengine/core/game/playableGame`) — the single orbit camera is now one of eight rigs, selected and tuned through `PlayableGame.camera` (never by writing camera positions from `onTick`). Set `camera.rig`:
|
|
1012
|
+
- `topDown` — fixed height/pitch/yaw with decoupled follow for ARPG-iso and top-down (Diablo IV/PoE 2/Last Epoch, Hades II); `camera.topDown: { height, pitch, yaw, followSmoothing, zoom }`.
|
|
1013
|
+
- `rts` — free-pan / edge-scroll / rotate / zoom independent of any avatar (The Sims 4, Manor Lords, Two Point Museum); `camera.rts: { panSpeed, edgeScroll, rotateSpeed, bounds, start }`.
|
|
1014
|
+
- `shoulder` — over-the-shoulder with ADS transition and shoulder-swap, reticle decoupled from camera center (Remnant II, Helldivers 2, The First Descendant); `camera.shoulder: { shoulderOffset, distance, ads, side }`.
|
|
1015
|
+
- `lockOn` — yaw bound to the player→target vector with the move axis reinterpreted as strafe (Elden Ring, Sekiro); `camera.lockOn: { targetEntityId?, distance, framingBias, yawSmoothing }`.
|
|
1016
|
+
- `chase` — speed-reactive vehicle chase (speed→FOV curve, spring-arm damping, procedural shake) plus fixed `cockpit`/`hood`/`rear` views (Forza Horizon 5, Rocket League, Trackmania); `camera.chase: { distance, springDamping, fov, shakePerSpeed, view }`.
|
|
1017
|
+
- **Every rig accepts `followEntityId: null`** — avatar-less games (city-builders, card games, auto-battlers) can now mount a camera; the orbit rig no longer bails when there is no follow target.
|
|
1018
|
+
- **Camera-shake / trauma channel** — every rig reads a shared trauma channel; feed it from any system with `import { cameraShake } from "@jgengine/shell/camera"` (`cameraShake(amplitude, decayPerSecond?)`, amplitude 0..1) or from React via `useCameraShake()`. Tune with `camera.shake: { maxOffset, maxRoll, decayPerSecond, exponent, frequency }`. (Combat hitstop and other systems feed the same channel.)
|
|
1019
|
+
- **Cinematic camera + mode-swap cross-fade** — `camera.cinematic: { keyframes: [{ position, lookAt, fov?, duration?, ease? }], loop? }` plays a scripted keyframe path over the active rig, and `camera.transitionSeconds` cross-fades the camera when the rig changes so mode swaps no longer hard-cut.
|
|
1020
|
+
- Pure rig math (shake decay/trauma, spring-arm damping, speed→FOV, lerp/cross-fade, offset/strafe vectors, keyframe sampling) is exported from `@jgengine/shell/camera` as testable functions.
|
|
1021
|
+
- **Sensors, vision & observer tools** (`@jgengine/core/sensor/*`, renderers in `@jgengine/shell/vision` and `@jgengine/shell/replay`) — a coherent "query hidden/tagged/framed world state and surface it" family:
|
|
1022
|
+
- **Reveal vision** (`sensor/revealQuery`) — `createRevealQuery` is an occlusion-ignoring tagged-entity radius query (`inRadius` already never checks occlusion; this scopes it to catalog-declared tags for a vision readout) paired with a toggleable screen-space reveal effect (`shell/vision/RevealVision`: `RevealHighlights` for through-wall 3D highlights, `RevealScreenTint` for the full-screen tint) — Dark Sight / detective-vision / wallhack-style highlight (Hunt: Showdown).
|
|
1023
|
+
- **Hidden-state probe** (`sensor/hiddenStateProbe`) — `probeHiddenState`/`probeHiddenStateAll` read a hidden zone/entity state variable in range and surface a distance-weighted reading; `shell/vision/HiddenStateProbeHud`'s `SensorReadoutMeter` renders it as a handheld sensor needle (EMF reader/spirit box/thermometer/geiger, Phasmophobia).
|
|
1024
|
+
- **View-frustum sensor** (`sensor/frustumSensor`) — `projectToView`/`framingScore`/`createFrustumSensor` answer "what's in this held camera's view, how well framed, and for how long" (dwell time resets the instant a subject leaves frame); `shell/vision/FrustumSensorHud`'s `FrustumSensorReadout` drives it off the live render camera for a photo-mode HUD (Content Warning).
|
|
1025
|
+
- **Session-recording buffer** (`sensor/recordingBuffer`) — `createRecordingBuffer` appends timestamped snapshots on game-time and seeks/scrubs them for replay, photo mode, or kill-cam; `shell/replay/useSessionRecorder` records an entity's pose every frame.
|
|
1026
|
+
- **`observer` camera rig** (config in `@jgengine/core/game/playableGame`: `ObserverCameraConfig`) — a detached spectator/photo cam bound to any entity or fixed point that reads no player input at all (van CCTV spectate, Forza-style photo mode free cam, Trackmania ghost/kill-cam).
|
|
1027
|
+
|
|
1028
|
+
- **Possession** (`@jgengine/core/scene/possession`, `createPossession`) — a player can own N scene entities and switch which one is under active control, distinct from the social party. `ctx.player.possession.own/disown/owns/listOwned(userId, entityId)` tracks ownership; `possess(userId, entityId)` swaps active control (rejecting entities the user doesn't own), flips the previous/next entity's `EntityRole` between `"player"`/`"npc"` (reusing entity control, not forking it), and emits `possession.swapped`. `active(userId)` defaults to `userId` itself until a swap happens. `@jgengine/shell`'s `GamePlayerShell` rebinds WASD movement, targeting, hotbar `from`, and the camera rig's `followEntityId` to the active possessed entity on every swap — no per-game camera glue required.
|
|
1029
|
+
- **Form / shapeshift** (`@jgengine/core/scene/form`, `createForms`) — a `form` bundles movement params + an ability-id list + a mesh (reusing the entity's catalog name, so mesh, movement defaults, and receive/role all follow the swap through the existing name-keyed resolution — no parallel mesh system). `ctx.scene.entity.form.register(defs)` in `onInit`; `shapeshift(instanceId, formId, durationSeconds?)` applies the bundle and optionally reverts automatically after `durationSeconds` of **game time** (`ctx.time.after`, so it obeys pause/fast-forward); `revert(instanceId)` reverts early. Emits `form.changed`.
|
|
1030
|
+
- **Cosmetic loadouts + emote broadcast** (`@jgengine/core/game/cosmetics` `createCosmetics`; `@jgengine/core/game/social` `Social.emotes`) — `ctx.player.cosmetics.register(defs)` + `apply(userId, loadoutId)` / `equip(userId, slot, cosmeticId)` manage a per-player cosmetic slot map (skin/back/aura/…), emitting `cosmetics.changed`. `ctx.game.social.emotes.play(fromUserId, emoteId, radius?)` broadcasts to nearby **player**-role entities (reusing `scene.entity.inRadius`, not a parallel proximity system) and emits `emote.played` — bind it through the existing `ctx.game.feed` primitive (`feed.bind("emote.played")`) for a HUD feed, no new hook needed.
|
|
1031
|
+
- `entityStore`'s `update()` patch now also accepts `name`, so possession/form (and any future system) can retarget an instance's catalog id without despawn/respawn.
|
|
1032
|
+
|
|
1033
|
+
An additive layer over effects/projectiles/death that adds melee/action feel. Every model is a renderer-free `@jgengine/core` factory a game composes per entity; `@jgengine/shell` renders the world/HUD side. No existing API moved.
|
|
1034
|
+
|
|
1035
|
+
- **Animation state machine** — `@jgengine/core/combat/animationState`: `createAnimationState({ clips })` over `AnimationClip`s whose `FrameRange`s tag `windup | active | recovery | cancel` windows. The root contract combat/defense subscribe to (`inPhase`, `isActive`, `canCancel`, `activeWindowMs`).
|
|
1036
|
+
- **Shared accumulator meter** — `@jgengine/core/stats/accumulatorMeter`: `createAccumulatorMeter({ max, mode, decayPerSecond, decayDelayMs, tiers })`, the fill/decay/threshold-fire/tier primitive. `@jgengine/core/combat/breakMeters` builds `createStaggerMeter` (poise/posture break → riposte) and `createBuildupMeter` (bleed/frost/rot proc) on it.
|
|
1037
|
+
- **Defensive window + attack tags** — `@jgengine/core/combat/defensiveWindow` (`resolveDefense` / `createDefensiveWindow`, parry/block/dodge evaluated against the attacker's active frames) reading `@jgengine/core/combat/attackTags` (`attackMeta`, unblockable/thrust/sweep/grab; `counters` for Mikiri-style reads).
|
|
1038
|
+
- **Combo strings** — `@jgengine/core/combat/comboString`: `advanceCombo` / `createComboRunner` — ordered attacks with stance-conditioned cancel points over the animation SM.
|
|
1039
|
+
- **Dash / dodge** — `@jgengine/core/movement/dash`: `createDashState` — directional burst + i-frame window + stamina/cooldown.
|
|
1040
|
+
- **Hit reaction, telegraphs, typed damage numbers** — `@jgengine/core/combat/hitReaction` + `ctx.scene.entity.hitReaction(...)` (knockback impulse + hitstop + `combat.hitReaction` shake channel); `@jgengine/core/combat/telegraph` + `ctx.scene.entity.telegraph(...)` (windup→activation ground decal bound to an effect, drawn by the shell); `ctx.scene.entity.floatText({ crit, element, hitType, scale })` styled by `@jgengine/shell/world/floatTextStyle`.
|
|
1041
|
+
|
|
1042
|
+
Genre systems over the existing effects/projectiles/targeting/loot primitives. Every model is a renderer-free `@jgengine/core` factory the game ticks on game-time `dt`; `@jgengine/react` adds four-state slot binding hooks. No existing API moved. The ult/streak meters build on the `stats/accumulatorMeter` from the combat-feel layer above.
|
|
1043
|
+
|
|
1044
|
+
- **Ability kit** — `@jgengine/core/combat/abilityKit`: `createAbilityKit([{ id, cooldownMs, chargesMax?, resourceCost?, castType? }])` models an ability slot **separate from an inventory item**, exposing the four HUD states `ready | cooldown | no-resource | just-cast` plus charges + cooldown fraction. Resource-agnostic — reports `no-resource` against a supplied `resourceAvailable`, the game spends. (MOBA/ARPG action bars, hero-shooter kits.)
|
|
1045
|
+
- **Event-fed meters** — `@jgengine/core/stats/eventMeter`: `createEventMeter({ max, mode, gains, resets?, tiers? })` on the shared accumulator. Mode `"hold"` = ult/adrenaline charge (`feed` combat tags, `ready()`, `consume()`); mode `"reset"` = kill-streak/combo with tiered thresholds that resets on a break tag. (Overwatch/Marvel Rivals ult, Returnal/DMC streak.)
|
|
1046
|
+
- **Auto-target policy** — `@jgengine/core/scene/autoTarget`: `selectAutoTarget` / `createAutoTargeter` — zero-input per-tick target selection `nearest | farthest | random | strongest | weakest | first | last` (path-progress aware). (Vampire Survivors auto-fire, Bloons tower priority.)
|
|
1047
|
+
- **Resistance matrix** — `@jgengine/core/combat/resistance`: `resolveResistance` / `resistanceScale` — damage-category × target-property → `immune | resist | normal | vulnerable` multiplier over the `receive` gate. (Bloons pop-types, elemental RPG weaknesses.)
|
|
1048
|
+
- **Run draft** — `@jgengine/core/game/runDraft`: `createRunDraft` / `createRunModifierStack` — pause, present N weighted picks (`pickWeighted`), choose, stack the modifiers for the run (aggregated onto `stats/statModifiers`). (Vampire Survivors level-ups, Hades boons.)
|
|
1049
|
+
- **React** — `@jgengine/react` `useAbilitySlots` / `useAbilitySlot` (four-state snapshots) and `useEventMeter` (ult/streak bar view).
|
|
1050
|
+
|
|
1051
|
+
- `@jgengine/core/item/durability` — per-instance item durability + repair. A catalog `DurabilitySpec` (`max`, `wearPerUse`/`wearPerHit`, `disableAtZero`, `repair`); `createDurability`/`wear`/`isDisabled`/`durabilityFraction` for the wear loop, `repairQuote(spec, state, { station?, to? })` for a quote-then-apply repair (material cost scaled by points restored, optional `qualityLossPerRepair` shrinking `max`), and `createDurabilityTracker()` to hold state per instance id. For weapon/tool/armor degradation repaired at stations.
|
|
1052
|
+
- `@jgengine/core/item/affix` — rarity-weighted procgen roller. `createAffixRoller({ pools, rarities })` turns `base × rarity` into `{ rolled affixes, computed stats, name }`: draws `affixCount` distinct affixes without replacement (weighted via the engine `pickWeighted`), computes stats (base × rarity scale, then `add` then `mul` affixes), and composes a name from rarity + prefix/suffix parts. `seededRng(seed)` gives deterministic drops. For looter-shooter / ARPG generated weapons.
|
|
1053
|
+
- `@jgengine/core/item/modularItem` — parts-in-typed-slots assembly. `ModularItemDef` with category-constrained `MountSlotDef`s; `install`/`uninstall` validate slot + category + occupancy, `computeEffectiveStats` rolls part `stats` (additive) then `multipliers` over the frame's `baseStats`, `missingRequiredSlots`/`isComplete` gate a buildable whole; `createModularItem(def)` is the stateful wrapper. For piece-by-piece guns and mech loadouts.
|
|
1054
|
+
- `@jgengine/core/inventory/storageTier` — tiered extraction-economy inventory. A `tier: "carried" | "banked"` on inventory containers (`InventoryDeclaration.tier`); `partitionOnDeath` splits a death snapshot into kept (banked) vs lost (carried) with merged stacks, `createDeliveryQueue()` is the delayed-delivery (insurance) hook (`schedule`/`due`/`claimDue` on the game clock), `insureLost` filters the lost set to insured items with a delayed `deliverAt`, and `resolveConsolation` yields a baseline loadout id (apply via `applyLoadout`) for the post-death consolation grant. The inventory foundation the extraction session/round machines build on.
|
|
1055
|
+
- `InventoryDeclaration` (`@jgengine/core/game/defineGame`) gained an optional `tier?: StorageTier` flag so containers declare carried-vs-banked storage directly.
|
|
1056
|
+
- `@jgengine/core/session/contestedChannel` — the interrupt-on-damage progress objective (plant/defuse, cash-out, urn deposit, hold-to-extract). `createContestedChannel({ duration, interruptOnDamage?, resetOnInterrupt?, favorability?, ratePerOccupant?, contested?, decayRate? })`: `start(team)`, `tick(dt, occupants)` (per-team occupancy → `start`/`tick`/`contested`/`paused`/`complete` events), `damage()` interrupts. `favorability`/`ratePerOccupant` scale the fill rate; `contested: "pause" | "decay"` handles a contesting team.
|
|
1057
|
+
- `@jgengine/core/session/roundState` — the buy→live→end match machine. `createRoundState({ phases, teams, maxRounds?, winReward?, lossBonus? })`: `tick(dt)` runs phase timers and auto-advances rounds, `concludeRound(winner)` settles win + escalating loss-bonus economy (`lossBonusFor`), `onPhaseEnd(hook)` gates commerce/spawns, `match.end` fires at `maxRounds`.
|
|
1058
|
+
- `@jgengine/core/combat/downed` — the alive→downed→dead revive chain. `createDownedState({ bleedoutSeconds, reviveSeconds?, reviveHealthFraction?, banner? })`: `down`/`tick` (bleedout → `died` + optional banner), `revive(id, dt)` accumulates ally hold time, `finish` executes, `respawnFromBanner` beacons back. Sits in front of engine death resolution.
|
|
1059
|
+
- `@jgengine/core/session/ring` — the shrinking battle-royale safe zone. `RingConfig` = `{ center, phases }`; `ringSampleAt(config, t)`/`createRing` give the live `{ center, radius, damagePerSecond }` (interpolated on the game clock), `isOutside`/`distanceOutside`/`damageOutside(t, dt, positions)` for out-of-bounds DoT.
|
|
1060
|
+
- `@jgengine/core/session/extraction` — the raid-scoped extract-to-bank session, composed from the contested channel + `inventory/storageTier`. `createRaidSession({ extracts, insurance?, consolation? })`: `beginExtract`/`tickExtract`/`damage` drive hold-to-leave, `resolveExtraction` banks everything carried, `resolveDeath` partitions/insures/consoles via storage tiers, `claimDeliveries(now)` drains insured returns.
|
|
1061
|
+
- `@jgengine/core/runtime/persistenceScope` — run-vs-meta persistence split with reset boundaries. `partitionScopes`/`resetRun`/`mergeScopes` over flat records, `clearRunFields`/`applyRunReset` over player rows/profiles, `planScenarioReset(...)` normalizes a season/scenario wipe applied through the new optional `HostPersistence.resetScenario` — implemented by `@jgengine/sql` (deletes a server's chunks + session and run-resets each profile in one transaction, keeping account meta).
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
## 0.6.0
|
|
1065
|
+
|
|
1066
|
+
An additive release: **every 0.5.0 API is unchanged**, so upgrading is only a
|
|
1067
|
+
version bump. The headline is a whole outdoor-world layer — composable
|
|
1068
|
+
`environment(...)` descriptors, renderer-free query primitives so gameplay reads
|
|
1069
|
+
the same world the shell renders, a standalone rigid-body physics sim, and the
|
|
1070
|
+
`@jgengine/shell` renderers that draw it all.
|
|
1071
|
+
|
|
1072
|
+
### Migrate
|
|
1073
|
+
|
|
1074
|
+
- Bump every `@jgengine/*` dependency to `^0.6.0` (the eight packages version in lockstep).
|
|
1075
|
+
- No code change is required — 0.6.0 adds surface, it doesn't move or remove any.
|
|
1076
|
+
- Optional: describe an outdoor scene once with `environment({ terrain, weather, vegetation, water, structures })` from `@jgengine/core/world/features`. `@jgengine/shell`'s `EnvironmentScene` renders it (terrain, rain/snow, grass, ocean, buildings), and gameplay reads the same world through the renderer-free primitives — `resolveTerrainField(...)` for ground-snap/collision, `windField(...)` for sway/sailing, `waterSurface(...)` for buoyancy, `scatter(...)` for prop/spawn placement, `buildingIndex(...)` for placement avoidance — no three.js needed.
|
|
1077
|
+
|
|
1078
|
+
### Added
|
|
1079
|
+
|
|
1080
|
+
- `@jgengine/core/world` query primitives — pure, renderer-free world sampling so gameplay and rendering read one source of truth. `world/terrain` (`TerrainField`, `noiseField`, `resolveTerrainField`, `resolveGroundStep` for slope-limited movement), `world/wind` (`windField` — one wind source for weather sway, grass, sailing, fire spread), `world/water` (`waterSurface` / `waterSurfaceFromDescriptor` — CPU Gerstner matching the ocean shader, for buoyancy and shorelines), `world/scatter` (`scatter` — seeded, overlap-aware point distribution for vegetation/props/spawns), `world/buildings` (`generateBuilding`, `generateBuildingDistrict`) and `world/buildingIndex` (`buildingIndex` — `at`/`within`/`nearest`/`isInside`/`blockers` over a district).
|
|
1081
|
+
- `@jgengine/core/world/features` composable outdoor descriptors — `environment(...)` plus `terrain()`, `rain()`, `snow()`, `grass()`, `ocean()`, `building()`.
|
|
1082
|
+
- `@jgengine/core/world/regions` (`createRegionField` — blend content-agnostic biomes by nearest selector, adding `tint`/`water`/`fog`/`speedMultiplier`/opaque `data`; extends `TerrainField` so it ground-snaps too) and `@jgengine/core/world/scatterItems` (`scatterItems` — region-driven content scatter: density per region, grounded, above-water/slope-aware, with `pickWeighted` for weighted rolls).
|
|
1083
|
+
- `@jgengine/core/physics/physicsWorld` — standalone fixed-capacity rigid-body sim (SoA buffers, spatial-hash broadphase, sleeping) for many colliding dynamic bodies (piles, debris, stress scenes). Distinct from the `defineGame` `physics: { gravity }` character-controller config.
|
|
1084
|
+
- `ctx.time` simulation clock (`@jgengine/core/time/simClock`) — `onTick`'s `dt` is now **game time**, so `rate * dt` decay/regen/AI obeys pause and fast-forward for free without per-system wiring. Configure with `defineGame({ time: { scale?, speeds?, dayLength?, start?, startPaused? } })` (all optional; default is real-time 1:1). `ctx.time` exposes `pause`/`play`/`toggle`/`setSpeed`/`cycleSpeed` + `snapshot()`/`calendar()`; `useGameClock()` binds it in React.
|
|
1085
|
+
- `@jgengine/shell` environment/terrain/water/weather/structures renderers — `EnvironmentScene` mounts an `environment()` descriptor as R3F renderers; `GrassField`, `ProceduralGround`, `Ocean`, `RainField`, `SnowField`, `LightningStrike`, `GeneratedBuilding`, and `world/InstancedBodies` (renders `PhysicsWorld` bodies).
|
|
1086
|
+
- `@jgengine/react/liveBind` — `useFrameBind` drives a DOM/SVG element from a per-frame value without re-rendering React (HUDs bound to live engine state never re-render or lag).
|
|
1087
|
+
|
|
1088
|
+
## 0.5.0
|
|
1089
|
+
|
|
1090
|
+
An additive release: **every 0.4.0 API is unchanged**, so upgrading is only a
|
|
1091
|
+
version bump. New pure primitives across progression, inventory slots, world
|
|
1092
|
+
geometry, and React store bindings.
|
|
1093
|
+
|
|
1094
|
+
### Migrate
|
|
1095
|
+
|
|
1096
|
+
- Bump every `@jgengine/*` dependency to `^0.5.0` (the eight packages version in lockstep).
|
|
1097
|
+
- No code change is required — 0.5.0 adds surface, it doesn't move or remove any.
|
|
1098
|
+
- Optional: replace a game's hand-rolled `progression/curves.ts` with the new `leveling(...)` track. `leveling({ xpForLevel: { kind: "power", base, exponent, round: "floor" }, maxLevel })` returns `xpForLevel`, `resolve`, and `grantXp(ctx.scene.entity.stats, userId, amount, onLevelUp?)` — a drop-in for the old `xpRequiredForLevel` / `resolveLevelProgress` / `grantXp` exports. `ctx.scene.entity.stats` satisfies the primitive's `LevelingStatAccess` structurally, so no adapter is needed.
|
|
1099
|
+
|
|
1100
|
+
### Added
|
|
1101
|
+
|
|
1102
|
+
- `@jgengine/core/game/progression` — genre-agnostic progression primitive. `curve(spec)` / `evalCurve(spec, x)` evaluate declarative scalar curves (`const`, `linear`, `power`, `geometric`, `steps`, `piecewise`, each with optional `round`/`min`/`max`) for speed-by-level, difficulty-by-wave, loot drop-rate ramps, and similar scaling. `leveling(config)` builds the stateful XP→level track (threshold accumulation, multi-level grants, cap handling, `stat.levelUp` emit) on top of an `xpForLevel` curve.
|
|
1103
|
+
- `@jgengine/core/inventory/slotModel` — pure slot-grid primitives (`createSlots`, `placeAt`, `removeAt`, `moveSlot`).
|
|
1104
|
+
- `@jgengine/core/world/geometry`, `/world/interiors`, `/world/placement` — pure world primitives: grid snapping, footprint AABBs and overlap, interior/exterior spaces, and placement validation.
|
|
1105
|
+
- `@jgengine/react/engineStore` — raw-store React bindings (`useEngineState`, `useEngineStore`, `useEngineEvent`).
|
|
1106
|
+
- Pure/functional tiers for the `trade`, `unlocks`, `quest`, and `feed` verbs in `@jgengine/core/game`.
|
|
1107
|
+
|
|
1108
|
+
## 0.4.0
|
|
1109
|
+
|
|
1110
|
+
Baseline release: the eight `@jgengine/*` packages (core, ws, sql, react,
|
|
1111
|
+
convex, node, shell, assets) as the first tracked version. No migration —
|
|
1112
|
+
this is the floor changelog entries are measured against.
|