@jgengine/shell 0.17.0 → 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 +130 -2
- package/dist/GamePlayerShell.js +10 -4
- package/dist/JoinGate.d.ts +17 -0
- package/dist/JoinGate.js +14 -0
- package/dist/Shell3dPresentation.d.ts +8 -1
- package/dist/Shell3dPresentation.js +34 -5
- package/dist/ShellChrome.d.ts +2 -0
- package/dist/ShellChrome.js +6 -0
- package/dist/TerritoryOverlay.d.ts +21 -0
- package/dist/TerritoryOverlay.js +9 -0
- package/dist/audio/AudioComponents.js +8 -1
- package/dist/audio/audioEngine.d.ts +7 -1
- package/dist/audio/audioEngine.js +67 -5
- package/dist/camera/cameraRigs.js +7 -0
- package/dist/defineGame.js +21 -6
- package/dist/devtools/KeysPanel.js +3 -3
- package/dist/drivers/FrameDriver.d.ts +5 -1
- package/dist/drivers/FrameDriver.js +61 -24
- package/dist/drivers/HudOnlyDriver.js +9 -4
- package/dist/environment/EnvironmentScene.js +1 -1
- package/dist/input/gamepadMerge.d.ts +10 -0
- package/dist/input/gamepadMerge.js +12 -0
- package/dist/input/gamepadSource.d.ts +12 -0
- package/dist/input/gamepadSource.js +107 -0
- package/dist/materialOverride.d.ts +4 -1
- package/dist/materialOverride.js +10 -1
- package/dist/postfx/PostProcessing.d.ts +10 -7
- package/dist/postfx/PostProcessing.js +33 -15
- package/dist/postfx/gradeShader.js +13 -0
- package/dist/render/CascadedShadows.d.ts +11 -0
- package/dist/render/CascadedShadows.js +19 -8
- package/dist/render/EnvironmentLighting.d.ts +4 -1
- package/dist/render/EnvironmentLighting.js +45 -9
- package/dist/render/SceneLighting.d.ts +16 -2
- package/dist/render/SceneLighting.js +56 -6
- package/dist/render/SceneModels.js +45 -9
- package/dist/render/modelLoad.d.ts +10 -0
- package/dist/render/modelLoad.js +28 -0
- package/dist/render/modelRender.d.ts +3 -0
- package/dist/render/modelRender.js +6 -2
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +15 -0
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +30 -0
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +13 -0
- package/dist/render/testFixtures/proceduralTileLayer.js +30 -0
- package/dist/render/useFootIk.d.ts +9 -0
- package/dist/render/useFootIk.js +89 -0
- package/dist/render/useModelAnimation.d.ts +2 -0
- package/dist/render/useModelAnimation.js +163 -107
- package/dist/scene/CityRenderer.js +661 -236
- package/dist/scene/cityWindowMaterial.d.ts +8 -6
- package/dist/scene/cityWindowMaterial.js +80 -21
- package/dist/settings/appliedSettings.d.ts +3 -1
- package/dist/settings/appliedSettings.js +5 -2
- package/dist/terrain/terrainDetailMaterial.js +7 -3
- package/dist/useShellMultiplayerSync.d.ts +8 -1
- package/dist/useShellMultiplayerSync.js +25 -5
- package/dist/visibility/CullingProvider.d.ts +2 -1
- package/dist/visibility/CullingProvider.js +7 -2
- package/dist/world/GridWorldScene.js +4 -0
- package/dist/world/TileLayerRenderer.d.ts +9 -0
- package/dist/world/TileLayerRenderer.js +64 -0
- package/dist/world/WorldParticles.d.ts +31 -0
- package/dist/world/WorldParticles.js +128 -0
- package/dist/world/WorldScene.d.ts +2 -1
- package/dist/world/WorldScene.js +46 -4
- package/dist/world/entityPose.d.ts +4 -0
- package/dist/world/entityPose.js +9 -0
- package/dist/world/tileLayerInstances.d.ts +11 -0
- package/dist/world/tileLayerInstances.js +28 -0
- package/dist/worldSync.d.ts +5 -1
- package/dist/worldSync.js +25 -4
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +2 -0
- package/skills/jgengine/api.md +98 -22
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine-ui/SKILL.md +8 -0
- package/skills/jgengine-ui/api.md +127 -16
- package/skills/jgengine-ui/capabilities.md +21 -0
- package/skills/jgengine-ui/reference.md +3 -0
- package/skills/jgengine-world/SKILL.md +6 -0
- package/skills/jgengine-world/api.md +312 -12
- package/skills/jgengine-world/capabilities.md +102 -0
- package/skills/level-design/SKILL.md +1 -1
- package/skills/jgengine-ui/references/visual-scorecard.md +0 -73
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
The lockstep game SDK set is `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`
|
|
3
|
+
The lockstep game SDK set is `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets}`
|
|
4
4
|
(this file’s 0.x.y releases). The CLI package `jgengine` and `@jgengine/github` use
|
|
5
5
|
**separate** version lines and may lag. Format follows [Keep a Changelog](https://keepachangelog.com);
|
|
6
6
|
each release **leads with a Migrate block** — the concrete steps to move a game
|
|
@@ -17,6 +17,13 @@ between (`--json` for structured output).
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
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
|
+
|
|
20
27
|
<!--
|
|
21
28
|
Every PR that changes `packages/*/src` records its consumer-facing change here, so
|
|
22
29
|
the next release's notes are complete by construction. Add a bullet under the right
|
|
@@ -26,14 +33,130 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
33
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
34
|
-->
|
|
28
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
|
+
|
|
29
88
|
### Migrate
|
|
30
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
|
+
|
|
31
98
|
### Changed
|
|
32
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
|
+
|
|
33
122
|
### Added
|
|
34
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
|
+
|
|
35
156
|
### Fixed
|
|
36
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
|
+
|
|
37
160
|
## 0.17.0
|
|
38
161
|
|
|
39
162
|
### Migrate
|
|
@@ -149,13 +272,18 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
149
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.
|
|
150
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.
|
|
151
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.
|
|
152
|
-
|
|
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`.
|
|
153
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).
|
|
154
278
|
|
|
155
279
|
### Fixed
|
|
156
280
|
|
|
157
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.
|
|
158
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.
|
|
159
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.
|
|
160
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.
|
|
161
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.
|
package/dist/GamePlayerShell.js
CHANGED
|
@@ -6,6 +6,7 @@ import { deriveTouchScheme, withTouchCodes, DEFAULT_TOUCH_STYLE } from "@jgengin
|
|
|
6
6
|
import { activeTouchControlsMode } from "@jgengine/core/input/touchControlsMode";
|
|
7
7
|
import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
|
|
8
8
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
9
|
+
import { activeActionCodes } from "@jgengine/core/game/controlGate";
|
|
9
10
|
import { useDisplayProfile } from "@jgengine/react/display";
|
|
10
11
|
import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
|
|
11
12
|
import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
@@ -24,6 +25,7 @@ import { OrientationHint } from "./touch/OrientationHint.js";
|
|
|
24
25
|
import { useTouchStyle, useTouchJoystickVariant, useGraphicsSettings } from "./settings/appliedSettings.js";
|
|
25
26
|
import { logRuntimeError, } from "./diagnostics/RuntimeDiagnostics.js";
|
|
26
27
|
import { EMPTY_RESERVED } from "./shellConstants.js";
|
|
28
|
+
import { JoinGate } from "./JoinGate.js";
|
|
27
29
|
import { useShellMultiplayerSync } from "./useShellMultiplayerSync.js";
|
|
28
30
|
import { ShellHudPresentation } from "./ShellHudPresentation.js";
|
|
29
31
|
import { Shell3dPresentation } from "./Shell3dPresentation.js";
|
|
@@ -88,8 +90,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
88
90
|
const effectiveInput = useMemo(() => applyBindingOverrides(playable.game.input ?? {}, bindingOverrides), [playable, bindingOverrides]);
|
|
89
91
|
const rebindAction = useCallback((action, code) => setBindingOverrides(saveBindingOverride(playable.game.name, action, [code])), [playable]);
|
|
90
92
|
const resetActionBinding = useCallback((action) => setBindingOverrides(clearBindingOverride(playable.game.name, action)), [playable]);
|
|
91
|
-
const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(withTouchCodes(effectiveInput))), [effectiveInput]);
|
|
92
|
-
const graphics = useGraphicsSettings(settingsStore, playable.shadows ?? true);
|
|
93
|
+
const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(withTouchCodes(ctx === null ? effectiveInput : activeActionCodes(ctx, effectiveInput)))), [ctx, effectiveInput]);
|
|
94
|
+
const graphics = useGraphicsSettings(settingsStore, playable.shadows ?? true, playable.graphics);
|
|
93
95
|
const trackPointerAxis = (event) => {
|
|
94
96
|
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
95
97
|
if (rect === undefined)
|
|
@@ -193,13 +195,16 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
193
195
|
setCtx(null);
|
|
194
196
|
};
|
|
195
197
|
}, [playable, userId]);
|
|
196
|
-
|
|
198
|
+
const authoritativeFrameRef = useRef(null);
|
|
199
|
+
const join = useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef, setRemotePlayers, authoritativeFrameRef);
|
|
197
200
|
useEffect(() => {
|
|
198
201
|
wrapperRef.current?.focus();
|
|
199
202
|
}, [ctx]);
|
|
200
203
|
useSyncExternalStore(ctx?.subscribe ?? (() => () => undefined), ctx?.version ?? (() => 0), ctx?.version ?? (() => 0));
|
|
201
204
|
if (ctx === null)
|
|
202
205
|
return _jsx("div", { className: "h-full w-full bg-neutral-950" });
|
|
206
|
+
if (join.status !== "joined")
|
|
207
|
+
return _jsx(JoinGate, { ...join });
|
|
203
208
|
const cameraConfig = playable.camera?.followEntityId !== undefined
|
|
204
209
|
? playable.camera
|
|
205
210
|
: { ...playable.camera, followEntityId: ctx.player.possession.active(userId) };
|
|
@@ -219,6 +224,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
219
224
|
touchSink,
|
|
220
225
|
touchJoystickVariant,
|
|
221
226
|
analogRef,
|
|
227
|
+
inputBindings: effectiveInput,
|
|
222
228
|
orientationGate,
|
|
223
229
|
orientationGateEl,
|
|
224
230
|
coarsePointer,
|
|
@@ -238,5 +244,5 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
238
244
|
return;
|
|
239
245
|
posterSettledRef.current = true;
|
|
240
246
|
setPosterFrozen(true);
|
|
241
|
-
} }));
|
|
247
|
+
}, authoritativeFrameRef: authoritativeFrameRef }));
|
|
242
248
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Blocking join feedback; callers may supply their own copy and classes.
|
|
4
|
+
* @capability join-gate block gameplay until joined and show failures with retry
|
|
5
|
+
*/
|
|
6
|
+
export declare function JoinGate({ status, failureReason, retry, joiningLabel, failedLabel, retryLabel, className, children, renderJoining, renderFailure }: {
|
|
7
|
+
status: "joining" | "joined" | "failed";
|
|
8
|
+
failureReason?: string | null;
|
|
9
|
+
retry: () => void;
|
|
10
|
+
joiningLabel?: ReactNode;
|
|
11
|
+
failedLabel?: ReactNode;
|
|
12
|
+
retryLabel?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
renderJoining?: () => ReactNode;
|
|
16
|
+
renderFailure?: (reason: string | null, retry: () => void) => ReactNode;
|
|
17
|
+
}): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
|
package/dist/JoinGate.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Blocking join feedback; callers may supply their own copy and classes.
|
|
4
|
+
* @capability join-gate block gameplay until joined and show failures with retry
|
|
5
|
+
*/
|
|
6
|
+
export function JoinGate({ status, failureReason, retry, joiningLabel = "Joining world…", failedLabel = "Unable to join", retryLabel = "Retry", className, children, renderJoining, renderFailure }) {
|
|
7
|
+
if (status === "joined")
|
|
8
|
+
return children;
|
|
9
|
+
if (status === "joining" && renderJoining)
|
|
10
|
+
return renderJoining();
|
|
11
|
+
if (status === "failed" && renderFailure)
|
|
12
|
+
return renderFailure(failureReason ?? null, retry);
|
|
13
|
+
return _jsxs("div", { className: className, "data-join-gate": status, style: { display: "grid", placeContent: "center", textAlign: "center", width: "100%", height: "100%", background: "var(--jg-surface, #171717)", color: "var(--jg-text, #fafafa)" }, children: [_jsx("p", { role: status === "failed" ? "alert" : "status", children: status === "failed" ? failedLabel : joiningLabel }), status === "failed" && _jsxs(_Fragment, { children: [_jsx("p", { children: failureReason }), _jsx("button", { type: "button", onClick: retry, children: retryLabel })] })] });
|
|
14
|
+
}
|
|
@@ -3,16 +3,18 @@ import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
|
|
|
3
3
|
import type { ActionStateTracker } from "@jgengine/core/input/actionBindings";
|
|
4
4
|
import type { BindingOverrides } from "@jgengine/core/input/bindingOverrides";
|
|
5
5
|
import type { TouchJoystickVariant, TouchScheme, TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
6
|
+
import type { GraphicsProfile } from "@jgengine/core/settings/graphicsProfile";
|
|
6
7
|
import { type GraphicsQuality, type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
7
8
|
import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
|
|
8
9
|
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
9
10
|
import type { AudioEngine } from "./audio/audioEngine.js";
|
|
10
11
|
import type { ShellMultiplayer } from "./multiplayer.js";
|
|
11
12
|
import type { PlayableGame } from "./registry.js";
|
|
13
|
+
import type { AuthoritativeFrameHandler } from "./worldSync.js";
|
|
12
14
|
import { type TouchCodeSink } from "./touch/TouchControlsOverlay.js";
|
|
13
15
|
import type { RuntimeDiagnostic } from "./diagnostics/RuntimeDiagnostics.js";
|
|
14
16
|
/** 3D play surface: canvas, world overlays, and shared chrome. @internal */
|
|
15
|
-
export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }: {
|
|
17
|
+
export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, inputBindings, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, authoritativeFrameRef, }: {
|
|
16
18
|
playable: PlayableGame;
|
|
17
19
|
ctx: GameContext;
|
|
18
20
|
multiplayer: ShellMultiplayer | null;
|
|
@@ -32,6 +34,7 @@ export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracke
|
|
|
32
34
|
touchStyle: TouchStyle;
|
|
33
35
|
touchJoystickVariant: TouchJoystickVariant;
|
|
34
36
|
analogRef: MutableRefObject<Readonly<Record<string, number>> | null>;
|
|
37
|
+
inputBindings: typeof playable.game.input;
|
|
35
38
|
orientationGate: boolean;
|
|
36
39
|
orientationGateEl: React.ReactNode;
|
|
37
40
|
coarsePointer: boolean;
|
|
@@ -41,6 +44,7 @@ export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracke
|
|
|
41
44
|
dpr: number;
|
|
42
45
|
uiScale: number;
|
|
43
46
|
quality: GraphicsQuality;
|
|
47
|
+
profile: GraphicsProfile;
|
|
44
48
|
};
|
|
45
49
|
settingsStore: SettingsStore;
|
|
46
50
|
bindingOverrides: BindingOverrides;
|
|
@@ -60,4 +64,7 @@ export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracke
|
|
|
60
64
|
poster: boolean;
|
|
61
65
|
posterFrozen: boolean;
|
|
62
66
|
onPosterSettled: () => void;
|
|
67
|
+
authoritativeFrameRef: {
|
|
68
|
+
current: AuthoritativeFrameHandler | null;
|
|
69
|
+
};
|
|
63
70
|
}): import("react").JSX.Element;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Canvas } from "@react-three/fiber";
|
|
3
3
|
import { useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
5
|
import { contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
|
|
5
6
|
import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
|
|
6
7
|
import { DEFAULT_PICKUP_RADIUS } from "@jgengine/core/game/worldItem";
|
|
7
8
|
import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
|
|
9
|
+
import { pixelPerfectFrustum, snapPixelPerfectPosition } from "@jgengine/core/game/pixelPerfect";
|
|
8
10
|
import { BUILT_IN_SETTING_CATEGORIES, } from "@jgengine/core/settings/settingsModel";
|
|
9
11
|
import { playControlsActive } from "@jgengine/core/game/controlGate";
|
|
10
12
|
import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
|
|
@@ -38,14 +40,36 @@ import { AudioSettingsBridge } from "./settings/appliedSettings.js";
|
|
|
38
40
|
import { ConfiguredLighting, BackdropFog } from "./render/SceneLighting.js";
|
|
39
41
|
import { WorldView, RemotePlayers } from "./world/WorldScene.js";
|
|
40
42
|
import { FrameDriver } from "./drivers/FrameDriver.js";
|
|
43
|
+
import { GamepadSource } from "./input/gamepadSource.js";
|
|
41
44
|
import { createShellKeyHandlers, ShellDebugOverlays, ShellGameUiChrome } from "./ShellChrome.js";
|
|
42
45
|
import { CombatPresentation } from "./CombatPresentation.js";
|
|
46
|
+
import { WorldParticles } from "./world/WorldParticles.js";
|
|
47
|
+
import { detectKtx2Support } from "./render/modelLoad.js";
|
|
43
48
|
import { resolvePresentationEffects, resolveWorldOverlayBars, } from "./presentationResolve.js";
|
|
44
49
|
const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
|
|
45
50
|
const DEFAULT_BACKGROUND_COLOR = "#14161b";
|
|
51
|
+
function PixelPerfectCamera({ pixelsPerUnit, integerScale }) {
|
|
52
|
+
const camera = useThree((state) => state.camera);
|
|
53
|
+
const size = useThree((state) => state.size);
|
|
54
|
+
useFrame(() => {
|
|
55
|
+
if (camera.isOrthographicCamera !== true)
|
|
56
|
+
return;
|
|
57
|
+
const frustum = pixelPerfectFrustum(size, pixelsPerUnit, integerScale === true);
|
|
58
|
+
camera.left = frustum.left;
|
|
59
|
+
camera.right = frustum.right;
|
|
60
|
+
camera.top = frustum.top;
|
|
61
|
+
camera.bottom = frustum.bottom;
|
|
62
|
+
camera.zoom = 1;
|
|
63
|
+
const snapped = snapPixelPerfectPosition(camera.position, pixelsPerUnit);
|
|
64
|
+
camera.position.set(snapped.x, snapped.y, snapped.z);
|
|
65
|
+
camera.updateProjectionMatrix();
|
|
66
|
+
}, 1);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
46
69
|
/** 3D play surface: canvas, world overlays, and shared chrome. @internal */
|
|
47
|
-
export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }) {
|
|
70
|
+
export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, inputBindings, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, authoritativeFrameRef, }) {
|
|
48
71
|
const GameUI = playable.GameUI;
|
|
72
|
+
const ktx2SupportDetected = useRef(false);
|
|
49
73
|
const WorldOverlay = playable.WorldOverlay;
|
|
50
74
|
const pointerService = useMemo(() => createPointerService(), []);
|
|
51
75
|
const selection = useMemo(() => createSelectionSet(), [playable]);
|
|
@@ -67,7 +91,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
67
91
|
const worldSky = resolveWorldSky(playable.game.world);
|
|
68
92
|
const world = playable.game.world;
|
|
69
93
|
const biomeBands = world?.kind === "environment" ? world.terrain?.biomeBands : undefined;
|
|
70
|
-
const AutoEnvironment = playable.environment ??
|
|
94
|
+
const AutoEnvironment = (typeof playable.environment === "function" ? playable.environment : undefined) ??
|
|
71
95
|
(world?.kind === "environment"
|
|
72
96
|
? () => _jsx(EnvironmentScene, { feature: world })
|
|
73
97
|
: world?.kind === "place"
|
|
@@ -279,7 +303,12 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
279
303
|
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
280
304
|
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
281
305
|
}
|
|
282
|
-
}, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsxs(Canvas, {
|
|
306
|
+
}, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsxs(Canvas, { onCreated: ({ gl }) => {
|
|
307
|
+
if (ktx2SupportDetected.current)
|
|
308
|
+
return;
|
|
309
|
+
ktx2SupportDetected.current = true;
|
|
310
|
+
detectKtx2Support(gl);
|
|
311
|
+
}, frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
|
|
283
312
|
? {
|
|
284
313
|
zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
|
|
285
314
|
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
@@ -289,9 +318,9 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
289
318
|
fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
|
|
290
319
|
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
291
320
|
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
292
|
-
}, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? (_jsx(EnvironmentLighting, { skyColor: lighting?.hemisphere?.skyColor ?? "#87b5e0", groundColor: lighting?.hemisphere?.groundColor ?? "#3d4a38", sunDirection: lighting?.directional?.[0]?.position, sunColor: lighting?.directional?.[0]?.color ?? "#fff2d6" })) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) }))) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(CullingProvider, { config: playable.visibility, children: _jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? (_jsx("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null, bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }), ...(bars.occlude === undefined ? {} : { occlude: bars.occlude }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
|
|
321
|
+
}, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [orthographic && playable.camera?.pixelPerfect !== undefined ? (_jsx(PixelPerfectCamera, { ...playable.camera.pixelPerfect })) : null, backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? (_jsx(EnvironmentLighting, { source: playable.environmentSource, skyColor: lighting?.hemisphere?.skyColor ?? "#87b5e0", groundColor: lighting?.hemisphere?.groundColor ?? "#3d4a38", sunDirection: lighting?.directional?.[0]?.position, sunColor: lighting?.directional?.[0]?.color ?? "#fff2d6" })) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting, profile: graphics.profile })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) }))) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(CullingProvider, { config: playable.visibility, drawDistance: graphics.profile.drawDistance, children: _jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? (_jsx("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null, bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }), ...(bars.occlude === undefined ? {} : { occlude: bars.occlude }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldParticles, { quality: graphics.quality, particleCap: graphics.profile.particleCap }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
|
|
293
322
|
cameraDraggingRef.current = dragging;
|
|
294
|
-
} }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, analogRef: analogRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: onPosterSettled }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality })) : null] }), !poster &&
|
|
323
|
+
} }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, analogRef: analogRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: onPosterSettled, authoritativeFrameRef: authoritativeFrameRef }), _jsx(GamepadSource, { tracker: tracker, bindings: inputBindings ?? {}, analogRef: analogRef, input: ctx.input }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality, stages: graphics.profile.postStages })) : null] }), !poster &&
|
|
295
324
|
!orientationGate &&
|
|
296
325
|
coarsePointer &&
|
|
297
326
|
controlsActive &&
|
package/dist/ShellChrome.d.ts
CHANGED
|
@@ -26,10 +26,12 @@ export declare function ShellDebugOverlays({ ctx, playable, multiplayer, diagnos
|
|
|
26
26
|
export type ShellKeyHandlers = {
|
|
27
27
|
onKeyDown: (event: {
|
|
28
28
|
code: string;
|
|
29
|
+
target?: EventTarget | null;
|
|
29
30
|
preventDefault: () => void;
|
|
30
31
|
}) => void;
|
|
31
32
|
onKeyUp: (event: {
|
|
32
33
|
code: string;
|
|
34
|
+
target?: EventTarget | null;
|
|
33
35
|
}) => void;
|
|
34
36
|
onBlur: () => void;
|
|
35
37
|
};
|
package/dist/ShellChrome.js
CHANGED
|
@@ -16,6 +16,12 @@ export function ShellDebugOverlays({ ctx, playable, multiplayer, diagnostics, de
|
|
|
16
16
|
export function createShellKeyHandlers({ f2HeldRef, tracker, devtoolsEnabled, setDevtoolsOpen, controlsActive, exitPointerLockOnDevtools = false, }) {
|
|
17
17
|
return {
|
|
18
18
|
onKeyDown: (event) => {
|
|
19
|
+
const target = event.target;
|
|
20
|
+
if (target?.closest?.("input, textarea, select, [contenteditable]:not([contenteditable=false])")) {
|
|
21
|
+
f2HeldRef.current = false;
|
|
22
|
+
tracker.reset();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
19
25
|
if (event.code === "F2") {
|
|
20
26
|
event.preventDefault();
|
|
21
27
|
f2HeldRef.current = true;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
/** A placement footprint cell and its authoritative preview status. */
|
|
3
|
+
export type TerritoryPreviewCell = {
|
|
4
|
+
key: string;
|
|
5
|
+
x: number;
|
|
6
|
+
z: number;
|
|
7
|
+
status: "owned" | "claimable" | "blocked";
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Placement footprint feedback with inline claim cost and affordability.
|
|
11
|
+
* @capability territory-overlay show footprint ownership, claim cost, and affordability
|
|
12
|
+
*/
|
|
13
|
+
export declare function TerritoryOverlay({ cells, cost, affordable, formatCost, renderCell, className, style }: {
|
|
14
|
+
cells: readonly TerritoryPreviewCell[];
|
|
15
|
+
cost: number;
|
|
16
|
+
affordable: boolean;
|
|
17
|
+
formatCost?: (cost: number) => ReactNode;
|
|
18
|
+
renderCell?: (cell: TerritoryPreviewCell) => ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
style?: CSSProperties;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Placement footprint feedback with inline claim cost and affordability.
|
|
4
|
+
* @capability territory-overlay show footprint ownership, claim cost, and affordability
|
|
5
|
+
*/
|
|
6
|
+
export function TerritoryOverlay({ cells, cost, affordable, formatCost = String, renderCell, className, style }) {
|
|
7
|
+
const blocked = cells.some((cell) => cell.status === "blocked");
|
|
8
|
+
return _jsxs("div", { className: className, style: { pointerEvents: "none", ...style }, "data-territory-overlay": true, children: [_jsx("div", { style: { display: "flex", gap: 3 }, children: cells.map((cell) => _jsx("span", { "data-territory-cell": cell.status, title: `${cell.x}, ${cell.z}: ${cell.status}`, children: renderCell ? renderCell(cell) : _jsx("span", { "aria-label": cell.status, style: { display: "inline-block", width: 8, height: 8, borderRadius: 2, background: cell.status === "owned" ? "var(--jg-accent, #5fc2a8)" : cell.status === "claimable" ? "#d6a548" : "#d96969" } }) }, cell.key)) }), _jsx("output", { "aria-live": "polite", "data-affordable": affordable, "data-blocked": blocked, children: blocked ? "Land unavailable" : _jsxs(_Fragment, { children: ["Land: ", formatCost(cost), affordable ? "" : " — Insufficient funds"] }) })] });
|
|
9
|
+
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
|
+
import { Quaternion, Vector3 } from "three";
|
|
3
4
|
import { useGameContext } from "@jgengine/react/provider";
|
|
4
5
|
export function AudioListener({ engine }) {
|
|
5
6
|
const camera = useThree((state) => state.camera);
|
|
6
7
|
useFrame(() => {
|
|
7
|
-
|
|
8
|
+
const direction = camera.getWorldDirection(new Vector3());
|
|
9
|
+
const up = camera.up.clone().applyQuaternion(camera.getWorldQuaternion(new Quaternion()));
|
|
10
|
+
engine.setListenerPose({
|
|
11
|
+
position: { x: camera.position.x, y: camera.position.y, z: camera.position.z },
|
|
12
|
+
forward: { x: direction.x, y: direction.y, z: direction.z },
|
|
13
|
+
up: { x: up.x, y: up.y, z: up.z },
|
|
14
|
+
});
|
|
8
15
|
});
|
|
9
16
|
return null;
|
|
10
17
|
}
|
|
@@ -6,6 +6,12 @@ export interface Vec3 {
|
|
|
6
6
|
y: number;
|
|
7
7
|
z: number;
|
|
8
8
|
}
|
|
9
|
+
/** Position and orientation for a spatial-audio listener. */
|
|
10
|
+
export interface ListenerPose {
|
|
11
|
+
position: Vec3;
|
|
12
|
+
forward: Vec3;
|
|
13
|
+
up: Vec3;
|
|
14
|
+
}
|
|
9
15
|
export interface AudioSceneConfig {
|
|
10
16
|
sounds?: Record<string, SoundDef>;
|
|
11
17
|
buses?: Record<string, AudioBusDef>;
|
|
@@ -23,7 +29,7 @@ export interface AudioEmitterHandle {
|
|
|
23
29
|
stop(): void;
|
|
24
30
|
}
|
|
25
31
|
export interface AudioEngine {
|
|
26
|
-
setListenerPose(
|
|
32
|
+
setListenerPose(pose: ListenerPose | Vec3): void;
|
|
27
33
|
playOneShot(soundId: string, position?: Vec3): void;
|
|
28
34
|
playLoop(soundId: string, position?: Vec3): AudioEmitterHandle | null;
|
|
29
35
|
/** Crossfade the procedural soundtrack to `themeId` (null fades out). No-op when no `music` catalog is configured. */
|