@jgengine/react 0.18.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 +114 -2
- package/dist/bars.js +2 -1
- package/dist/chat.d.ts +10 -2
- package/dist/chat.js +45 -5
- package/dist/hudFrame.js +2 -0
- package/dist/hudSkin.d.ts +29 -0
- package/dist/hudSkin.js +50 -0
- package/dist/hudSkinPreview.d.ts +4 -0
- package/dist/hudSkinPreview.js +31 -0
- package/dist/hudTheme.d.ts +4 -0
- package/dist/hudTheme.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/previewFixtures.js +14 -0
- package/dist/spriteClipPreview.d.ts +6 -0
- package/dist/spriteClipPreview.js +72 -0
- package/dist/standaloneChatPanel.d.ts +38 -0
- package/dist/standaloneChatPanel.js +55 -0
- package/dist/tileLayerPreview.d.ts +4 -0
- package/dist/tileLayerPreview.js +37 -0
- package/dist/useServerSession.d.ts +11 -0
- package/dist/useServerSession.js +37 -0
- package/package.json +2 -2
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/jgengine/SKILL.md +2 -0
- package/skills/jgengine/api.md +94 -19
- 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-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,6 +33,106 @@ 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
|
+
|
|
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
|
+
|
|
29
136
|
## 0.18.0
|
|
30
137
|
|
|
31
138
|
### Migrate
|
|
@@ -165,13 +272,18 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
165
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.
|
|
166
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.
|
|
167
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.
|
|
168
|
-
|
|
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`.
|
|
169
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).
|
|
170
278
|
|
|
171
279
|
### Fixed
|
|
172
280
|
|
|
173
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.
|
|
174
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.
|
|
175
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.
|
|
176
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.
|
|
177
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/bars.js
CHANGED
|
@@ -87,6 +87,7 @@ function BarView({ fraction, valueText, config, label, width = 200, segments, en
|
|
|
87
87
|
height: cssVar("height"),
|
|
88
88
|
border: `${cssVar("frameWidth")} solid ${cssVar("frame")}`,
|
|
89
89
|
background: cssVar("track"),
|
|
90
|
+
backgroundImage: "var(--jg-bar-skin-track, none)",
|
|
90
91
|
boxShadow: cssVar("bevel"),
|
|
91
92
|
overflow: "hidden",
|
|
92
93
|
boxSizing: "border-box",
|
|
@@ -95,7 +96,7 @@ function BarView({ fraction, valueText, config, label, width = 200, segments, en
|
|
|
95
96
|
position: "absolute",
|
|
96
97
|
inset: 0,
|
|
97
98
|
width: `${fraction * 100}%`,
|
|
98
|
-
background: fill,
|
|
99
|
+
background: "var(--jg-bar-skin-fill, " + fill + ")",
|
|
99
100
|
transition: "width 160ms ease",
|
|
100
101
|
} }), _jsx("div", { style: {
|
|
101
102
|
position: "absolute",
|
package/dist/chat.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type StandaloneChatPanelProps } from "./standaloneChatPanel.js";
|
|
1
2
|
import { type ReactNode } from "react";
|
|
2
3
|
import type { ChatMessage } from "@jgengine/core/game/chat";
|
|
3
4
|
import type { ChatSync, ChatTransport } from "@jgengine/core/multiplayer/chatContract";
|
|
@@ -7,11 +8,12 @@ import type { ChatSync, ChatTransport } from "@jgengine/core/multiplayer/chatCon
|
|
|
7
8
|
* render or inside useMemo — so subscriptions survive re-renders.
|
|
8
9
|
*/
|
|
9
10
|
export declare function chatTransportFromSync(sync: ChatSync): ChatTransport;
|
|
10
|
-
export declare function ChatLog({ channelId, limit, className, messageClassName, renderMessage, }: {
|
|
11
|
+
export declare function ChatLog({ channelId, limit, className, messageClassName, ownMessageClassName, renderMessage, }: {
|
|
11
12
|
channelId: string;
|
|
12
13
|
limit?: number;
|
|
13
14
|
className?: string;
|
|
14
15
|
messageClassName?: string;
|
|
16
|
+
ownMessageClassName?: string;
|
|
15
17
|
renderMessage?: (message: ChatMessage) => ReactNode;
|
|
16
18
|
}): import("react").JSX.Element;
|
|
17
19
|
export declare function ChatInput({ channelId, className, inputClassName, buttonClassName, placeholder, sendLabel, onSent, onRejected, }: {
|
|
@@ -33,16 +35,19 @@ export declare function ChannelTabs({ channels, active, onSelect, className, tab
|
|
|
33
35
|
activeTabClassName?: string;
|
|
34
36
|
renderTab?: (channelId: string, isActive: boolean) => ReactNode;
|
|
35
37
|
}): import("react").JSX.Element | null;
|
|
36
|
-
|
|
38
|
+
declare function ContextChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, ownMessageClassName, defaultExpanded, toggleLabel, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, renderTab, onRejected, }: {
|
|
37
39
|
channels?: readonly string[];
|
|
38
40
|
initialChannel?: string;
|
|
39
41
|
limit?: number;
|
|
40
42
|
className?: string;
|
|
43
|
+
defaultExpanded?: boolean;
|
|
44
|
+
toggleLabel?: ReactNode;
|
|
41
45
|
tabsClassName?: string;
|
|
42
46
|
tabClassName?: string;
|
|
43
47
|
activeTabClassName?: string;
|
|
44
48
|
logClassName?: string;
|
|
45
49
|
messageClassName?: string;
|
|
50
|
+
ownMessageClassName?: string;
|
|
46
51
|
inputClassName?: string;
|
|
47
52
|
inputFieldClassName?: string;
|
|
48
53
|
sendButtonClassName?: string;
|
|
@@ -52,3 +57,6 @@ export declare function ChatPanel({ channels, initialChannel, limit, className,
|
|
|
52
57
|
renderTab?: (channelId: string, isActive: boolean) => ReactNode;
|
|
53
58
|
onRejected?: (reason: string) => void;
|
|
54
59
|
}): import("react").JSX.Element | null;
|
|
60
|
+
/** Chat behavior over a game context or externally supplied server messages. */
|
|
61
|
+
export declare function ChatPanel<T extends ChatMessage = ChatMessage>(props: Parameters<typeof ContextChatPanel>[0] | StandaloneChatPanelProps<T>): import("react").JSX.Element;
|
|
62
|
+
export {};
|
package/dist/chat.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { StandaloneChatPanel } from "./standaloneChatPanel.js";
|
|
2
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
4
|
import { useGameContext } from "./provider.js";
|
|
4
5
|
import { useChat } from "./hooks.js";
|
|
@@ -24,7 +25,8 @@ export function chatTransportFromSync(sync) {
|
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
|
-
export function ChatLog({ channelId, limit, className, messageClassName, renderMessage, }) {
|
|
28
|
+
export function ChatLog({ channelId, limit, className, messageClassName, ownMessageClassName, renderMessage, }) {
|
|
29
|
+
const ctx = useGameContext();
|
|
28
30
|
const messages = useChat(channelId, limit === undefined ? undefined : { limit });
|
|
29
31
|
const scrollRef = useRef(null);
|
|
30
32
|
useEffect(() => {
|
|
@@ -32,11 +34,12 @@ export function ChatLog({ channelId, limit, className, messageClassName, renderM
|
|
|
32
34
|
if (node !== null)
|
|
33
35
|
node.scrollTop = node.scrollHeight;
|
|
34
36
|
}, [messages.length]);
|
|
35
|
-
return (_jsx("div", { ref: scrollRef, className: className, "data-chat-log": channelId, children: messages.map((message) => (_jsx("div", { className: messageClassName, "data-chat-message": true, "data-from": message.fromUserId, children: renderMessage !== undefined ? (renderMessage(message)) : (_jsxs(_Fragment, { children: [_jsx("span", { "data-chat-from": true, children: message.fromUserId }), _jsx("span", { "data-chat-body": true, children: message.body })] })) }, message.id))) }));
|
|
37
|
+
return (_jsx("div", { ref: scrollRef, className: className, "data-chat-log": channelId, children: messages.map((message) => (_jsx("div", { className: [messageClassName, message.fromUserId === ctx.player.userId ? ownMessageClassName : undefined].filter(Boolean).join(" "), "data-own-message": message.fromUserId === ctx.player.userId || undefined, "data-chat-message": true, "data-from": message.fromUserId, children: renderMessage !== undefined ? (renderMessage(message)) : (_jsxs(_Fragment, { children: [_jsx("span", { "data-chat-from": true, children: message.fromUserId }), _jsx("span", { "data-chat-body": true, children: message.body })] })) }, message.id))) }));
|
|
36
38
|
}
|
|
37
39
|
export function ChatInput({ channelId, className, inputClassName, buttonClassName, placeholder, sendLabel, onSent, onRejected, }) {
|
|
38
40
|
const ctx = useGameContext();
|
|
39
41
|
const [value, setValue] = useState("");
|
|
42
|
+
const [failure, setFailure] = useState(null);
|
|
40
43
|
const chat = ctx.game.chat;
|
|
41
44
|
if (chat === undefined)
|
|
42
45
|
return null;
|
|
@@ -44,13 +47,15 @@ export function ChatInput({ channelId, className, inputClassName, buttonClassNam
|
|
|
44
47
|
event.preventDefault();
|
|
45
48
|
const result = chat.send(ctx.player.userId, channelId, value);
|
|
46
49
|
if ("reason" in result) {
|
|
50
|
+
setFailure(result.reason);
|
|
47
51
|
onRejected?.(result.reason);
|
|
48
52
|
return;
|
|
49
53
|
}
|
|
54
|
+
setFailure(null);
|
|
50
55
|
setValue("");
|
|
51
56
|
onSent?.(result.message);
|
|
52
57
|
};
|
|
53
|
-
return (_jsxs("form", { className: className, "data-chat-input": channelId, onSubmit: submit, children: [_jsx("input", { className: inputClassName, type: "text", value: value, placeholder: placeholder, onChange: (event) => setValue(event.target.value) }), _jsx("button", { type: "submit", className: buttonClassName, "data-chat-send": true, children: sendLabel ?? "Send" })] }));
|
|
58
|
+
return (_jsxs("form", { className: className, "data-chat-input": channelId, onSubmit: submit, children: [_jsx("input", { className: inputClassName, type: "text", "aria-label": "Chat message", maxLength: 240, value: value, placeholder: placeholder, onChange: (event) => setValue(event.target.value) }), failure !== null && _jsx("p", { role: "alert", children: failure }), _jsx("button", { type: "submit", className: buttonClassName, "data-chat-send": true, children: sendLabel ?? "Send" })] }));
|
|
54
59
|
}
|
|
55
60
|
export function ChannelTabs({ channels, active, onSelect, className, tabClassName, activeTabClassName, renderTab, }) {
|
|
56
61
|
const ctx = useGameContext();
|
|
@@ -66,12 +71,47 @@ export function ChannelTabs({ channels, active, onSelect, className, tabClassNam
|
|
|
66
71
|
return (_jsx("button", { type: "button", role: "tab", "aria-selected": isActive, className: classes.length > 0 ? classes : undefined, "data-channel": channelId, onClick: () => onSelect(channelId), children: renderTab !== undefined ? renderTab(channelId, isActive) : channelId }, channelId));
|
|
67
72
|
}) }));
|
|
68
73
|
}
|
|
69
|
-
|
|
74
|
+
function ContextChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, ownMessageClassName, defaultExpanded = true, toggleLabel = "Chat", inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, renderTab, onRejected, }) {
|
|
70
75
|
const ctx = useGameContext();
|
|
71
76
|
const chat = ctx.game.chat;
|
|
72
77
|
const ids = channels ?? chat?.channels().map((def) => def.id) ?? [];
|
|
73
78
|
const [active, setActive] = useState(initialChannel ?? ids[0] ?? "global");
|
|
79
|
+
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
80
|
+
const panelRef = useRef(null);
|
|
81
|
+
const focusPending = useRef(false);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (chat === undefined)
|
|
84
|
+
return;
|
|
85
|
+
const onKey = (event) => {
|
|
86
|
+
if (event.defaultPrevented || event.isComposing || event.key !== "Enter")
|
|
87
|
+
return;
|
|
88
|
+
const target = event.target;
|
|
89
|
+
if (target?.closest("input, textarea, select, button, [contenteditable]:not([contenteditable=false])"))
|
|
90
|
+
return;
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
focusPending.current = true;
|
|
93
|
+
setExpanded(true);
|
|
94
|
+
panelRef.current?.querySelector("input")?.focus();
|
|
95
|
+
};
|
|
96
|
+
document.addEventListener("keydown", onKey);
|
|
97
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
98
|
+
}, [chat]);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (expanded && focusPending.current) {
|
|
101
|
+
panelRef.current?.querySelector("input")?.focus();
|
|
102
|
+
focusPending.current = false;
|
|
103
|
+
}
|
|
104
|
+
}, [expanded]);
|
|
74
105
|
if (chat === undefined)
|
|
75
106
|
return null;
|
|
76
|
-
return (_jsxs("section", { className: className, "data-chat-panel": true,
|
|
107
|
+
return (_jsxs("section", { ref: panelRef, className: className, "data-chat-panel": true, "data-expanded": expanded, onKeyDown: (event) => {
|
|
108
|
+
if (event.key === "Escape") {
|
|
109
|
+
event.target.blur();
|
|
110
|
+
event.stopPropagation();
|
|
111
|
+
}
|
|
112
|
+
}, children: [_jsx("button", { type: "button", "aria-expanded": expanded, onClick: () => setExpanded((value) => !value), children: toggleLabel }), expanded && _jsxs(_Fragment, { children: [_jsx(ChannelTabs, { channels: ids, active: active, onSelect: setActive, className: tabsClassName, tabClassName: tabClassName, activeTabClassName: activeTabClassName, renderTab: renderTab }), _jsx(ChatLog, { channelId: active, limit: limit, className: logClassName, messageClassName: messageClassName, ownMessageClassName: ownMessageClassName, renderMessage: renderMessage }), _jsx(ChatInput, { channelId: active, className: inputClassName, inputClassName: inputFieldClassName, buttonClassName: sendButtonClassName, placeholder: placeholder, onRejected: onRejected })] })] }));
|
|
113
|
+
}
|
|
114
|
+
/** Chat behavior over a game context or externally supplied server messages. */
|
|
115
|
+
export function ChatPanel(props) {
|
|
116
|
+
return "messages" in props ? _jsx(StandaloneChatPanel, { ...props }) : _jsx(ContextChatPanel, { ...props });
|
|
77
117
|
}
|
package/dist/hudFrame.js
CHANGED
|
@@ -15,6 +15,8 @@ export function hudFrameStyle(variation = "glass", shape = "rounded") {
|
|
|
15
15
|
border: "var(--jg-frame-border, 1px solid rgba(255,255,255,0.10))",
|
|
16
16
|
borderRadius: "var(--jg-frame-radius, 10px)",
|
|
17
17
|
boxShadow: "var(--jg-frame-glow, 0 4px 16px rgba(0,0,0,0.35))",
|
|
18
|
+
borderImage: "var(--jg-frame-image, none) var(--jg-frame-slice, 0) / var(--jg-frame-scale, 1) var(--jg-frame-fill, stretch)",
|
|
19
|
+
cursor: "var(--jg-skin-cursor, auto)",
|
|
18
20
|
color: "var(--jg-bar-text, #f4f6fb)",
|
|
19
21
|
fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
20
22
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type CSSProperties } from "react";
|
|
2
|
+
/** Optional raster-backed materials for shared HUD chrome. */
|
|
3
|
+
export interface HudSkinMaterial {
|
|
4
|
+
image: string;
|
|
5
|
+
slice: [number, number, number, number];
|
|
6
|
+
scale?: number;
|
|
7
|
+
fill?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** A game-authored visual skin layered over a {@link HudTheme}. */
|
|
10
|
+
export interface HudSkin {
|
|
11
|
+
frame?: HudSkinMaterial;
|
|
12
|
+
slot?: HudSkinMaterial;
|
|
13
|
+
bar?: {
|
|
14
|
+
fill: string;
|
|
15
|
+
track: string;
|
|
16
|
+
cap?: string;
|
|
17
|
+
};
|
|
18
|
+
font?: {
|
|
19
|
+
family: string;
|
|
20
|
+
src: string;
|
|
21
|
+
weight?: number;
|
|
22
|
+
pixel?: boolean;
|
|
23
|
+
};
|
|
24
|
+
cursor?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Converts a skin into the CSS variables consumed by shared HUD components. */
|
|
27
|
+
export declare function hudSkinVars(skin?: HudSkin): CSSProperties;
|
|
28
|
+
/** Loads a skin font in browsers and reports when it is available. */
|
|
29
|
+
export declare function useHudFont(skin?: HudSkin): boolean;
|
package/dist/hudSkin.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
/** Converts a skin into the CSS variables consumed by shared HUD components. */
|
|
3
|
+
export function hudSkinVars(skin) {
|
|
4
|
+
if (!skin)
|
|
5
|
+
return {};
|
|
6
|
+
const vars = {};
|
|
7
|
+
const material = (prefix, value) => {
|
|
8
|
+
if (!value)
|
|
9
|
+
return;
|
|
10
|
+
vars[`--jg-${prefix}-image`] = `url(${value.image})`;
|
|
11
|
+
vars[`--jg-${prefix}-slice`] = value.slice.join(" ");
|
|
12
|
+
if (value.scale !== undefined)
|
|
13
|
+
vars[`--jg-${prefix}-scale`] = String(value.scale);
|
|
14
|
+
if (value.fill !== undefined)
|
|
15
|
+
vars[`--jg-${prefix}-fill`] = value.fill ? "fill" : "";
|
|
16
|
+
};
|
|
17
|
+
material("frame", skin.frame);
|
|
18
|
+
material("slot", skin.slot);
|
|
19
|
+
if (skin.bar) {
|
|
20
|
+
vars["--jg-bar-skin-fill"] = skin.bar.fill;
|
|
21
|
+
vars["--jg-bar-skin-track"] = skin.bar.track;
|
|
22
|
+
if (skin.bar.cap)
|
|
23
|
+
vars["--jg-bar-skin-cap"] = `url(${skin.bar.cap})`;
|
|
24
|
+
}
|
|
25
|
+
if (skin.font) {
|
|
26
|
+
vars["--jg-skin-font"] = skin.font.family;
|
|
27
|
+
if (skin.font.pixel)
|
|
28
|
+
vars["--jg-skin-font-rendering"] = "pixelated";
|
|
29
|
+
}
|
|
30
|
+
if (skin.cursor)
|
|
31
|
+
vars["--jg-skin-cursor"] = `url(${skin.cursor}), auto`;
|
|
32
|
+
return vars;
|
|
33
|
+
}
|
|
34
|
+
/** Loads a skin font in browsers and reports when it is available. */
|
|
35
|
+
export function useHudFont(skin) {
|
|
36
|
+
const [loaded, setLoaded] = useState(false);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!skin?.font || typeof FontFace === "undefined") {
|
|
39
|
+
setLoaded(!skin?.font);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const font = new FontFace(skin.font.family, `url(${skin.font.src})`, { weight: String(skin.font.weight ?? 400) });
|
|
43
|
+
let active = true;
|
|
44
|
+
font.load().then((face) => { document.fonts.add(face); if (active)
|
|
45
|
+
setLoaded(true); }).catch(() => { if (active)
|
|
46
|
+
setLoaded(false); });
|
|
47
|
+
return () => { active = false; };
|
|
48
|
+
}, [skin?.font?.family, skin?.font?.src, skin?.font?.weight]);
|
|
49
|
+
return loaded;
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { HealthBar, ShieldBar } from "./bars.js";
|
|
4
|
+
import { HudFrame } from "./hudFrame.js";
|
|
5
|
+
import { hudTheme, hudThemeVars } from "./hudTheme.js";
|
|
6
|
+
import { hudSkinVars } from "./hudSkin.js";
|
|
7
|
+
function painted(color, dark) {
|
|
8
|
+
if (typeof document === "undefined")
|
|
9
|
+
return "";
|
|
10
|
+
const canvas = document.createElement("canvas");
|
|
11
|
+
canvas.width = 32;
|
|
12
|
+
canvas.height = 32;
|
|
13
|
+
const ctx = canvas.getContext("2d");
|
|
14
|
+
if (!ctx)
|
|
15
|
+
return "";
|
|
16
|
+
ctx.fillStyle = dark;
|
|
17
|
+
ctx.fillRect(0, 0, 32, 32);
|
|
18
|
+
ctx.strokeStyle = color;
|
|
19
|
+
ctx.lineWidth = 5;
|
|
20
|
+
ctx.strokeRect(3, 3, 26, 26);
|
|
21
|
+
return canvas.toDataURL("image/png");
|
|
22
|
+
}
|
|
23
|
+
function SkinCell({ title, skin }) {
|
|
24
|
+
const vars = { ...hudThemeVars(hudTheme({ skin })), ...hudSkinVars(skin) };
|
|
25
|
+
return _jsx("div", { style: { ...vars, width: 300 }, children: _jsx(HudFrame, { variation: "themed", title: title, padding: 14, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 10 }, children: [_jsx(HealthBar, { value: 78, max: 100, label: "HP", width: "100%" }), _jsx(ShieldBar, { value: 42, max: 60, label: "SH", width: "100%" }), _jsx("div", { style: { display: "flex", gap: 6 }, children: ["A", "B", "C", "D", "E"].map((label) => _jsx("div", { style: { width: 40, height: 40, display: "grid", placeItems: "center", color: "var(--jg-text)", border: "var(--jg-slot-border)", borderImage: "var(--jg-slot-image, none) var(--jg-slot-slice, 0) / var(--jg-slot-scale, 1)", background: "var(--jg-slot-bg)", borderRadius: "var(--jg-slot-radius)" }, children: label }, label)) })] }) }) });
|
|
26
|
+
}
|
|
27
|
+
/** Procedural two-skin fixture for visual regression captures. */
|
|
28
|
+
export function HudSkinPreview({ className }) {
|
|
29
|
+
const skins = useMemo(() => [{ title: "painted cartoon", skin: { frame: { image: painted("#ffd166", "#7b3f00"), slice: [8, 8, 8, 8], fill: true }, slot: { image: painted("#66e3ff", "#12354a"), slice: [8, 8, 8, 8] }, bar: { fill: "linear-gradient(90deg,#ff6b6b,#ffd166)", track: "#24152a" } } }, { title: "thin dark overlay", skin: { frame: { image: painted("#8d99ae", "#11151c"), slice: [4, 4, 4, 4] }, slot: { image: painted("#566070", "#090b10"), slice: [4, 4, 4, 4] }, bar: { fill: "#8d99ae", track: "#090b10" } } }], []);
|
|
30
|
+
return _jsx("div", { className: className, "data-hud-skin-preview": true, style: { display: "flex", gap: 18, flexWrap: "wrap", padding: 22, background: "#0e1216", color: "#f5f7fa" }, children: skins.map((item) => _jsx(SkinCell, { ...item }, item.title)) });
|
|
31
|
+
}
|
package/dist/hudTheme.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type CSSProperties } from "react";
|
|
2
|
+
import { type HudSkin } from "./hudSkin.js";
|
|
3
|
+
export { hudSkinVars, type HudSkin } from "./hudSkin.js";
|
|
2
4
|
/**
|
|
3
5
|
* `HudTheme` (#1034, #1573): one token object that restyles every shared token-driven building
|
|
4
6
|
* block at once — the atomic bars (#1033), frames, slots, the minimap ring, and the chrome the
|
|
@@ -106,6 +108,7 @@ export interface HudTheme {
|
|
|
106
108
|
slot: HudThemeSlot;
|
|
107
109
|
/** Minimap chrome ring color. */
|
|
108
110
|
ring: string;
|
|
111
|
+
skin?: HudSkin;
|
|
109
112
|
}
|
|
110
113
|
/** Emits the CSS custom properties for a theme — spread onto any HUD ancestor to re-skin the subtree. */
|
|
111
114
|
export declare function hudThemeVars(theme: HudTheme): CSSProperties;
|
|
@@ -122,6 +125,7 @@ export interface HudThemeOverrides {
|
|
|
122
125
|
bar?: Partial<HudThemeBar>;
|
|
123
126
|
slot?: Partial<HudThemeSlot>;
|
|
124
127
|
ring?: string;
|
|
128
|
+
skin?: HudSkin;
|
|
125
129
|
}
|
|
126
130
|
/**
|
|
127
131
|
* Builds a full {@link HudTheme} from per-group partials, filling the rest from
|
package/dist/hudTheme.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import {} from "react";
|
|
2
|
+
import { hudSkinVars } from "./hudSkin.js";
|
|
3
|
+
export { hudSkinVars } from "./hudSkin.js";
|
|
2
4
|
/** Emits the CSS custom properties for a theme — spread onto any HUD ancestor to re-skin the subtree. */
|
|
3
5
|
export function hudThemeVars(theme) {
|
|
4
6
|
const { palette, surface, status, font, rarity, frame, bar, slot } = theme;
|
|
5
7
|
return {
|
|
8
|
+
"--jg-frame-image": "none",
|
|
9
|
+
"--jg-frame-slice": "0",
|
|
10
|
+
"--jg-frame-scale": "1",
|
|
11
|
+
"--jg-frame-fill": "stretch",
|
|
12
|
+
"--jg-slot-image": "none",
|
|
13
|
+
"--jg-slot-slice": "0",
|
|
14
|
+
"--jg-slot-scale": "1",
|
|
15
|
+
"--jg-bar-skin-track": "none",
|
|
16
|
+
"--jg-bar-skin-fill": "none",
|
|
17
|
+
"--jg-skin-cursor": "auto",
|
|
18
|
+
...hudSkinVars(theme.skin),
|
|
6
19
|
// atomic-bar tokens (same names the bars read — #1033)
|
|
7
20
|
"--jg-health": palette.health,
|
|
8
21
|
"--jg-health-low": palette.healthLow,
|
|
@@ -146,6 +159,7 @@ export function hudTheme(overrides) {
|
|
|
146
159
|
bar: { ...defaultHudTheme.bar, ...overrides.bar },
|
|
147
160
|
slot: { ...defaultHudTheme.slot, ...overrides.slot },
|
|
148
161
|
ring: overrides.ring ?? defaultHudTheme.ring,
|
|
162
|
+
skin: overrides.skin,
|
|
149
163
|
};
|
|
150
164
|
}
|
|
151
165
|
const theme = hudTheme;
|
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export * from "./panels.js";
|
|
|
48
48
|
export * from "./characterSheet.js";
|
|
49
49
|
export * from "./talentTree.js";
|
|
50
50
|
export * from "./hudTheme.js";
|
|
51
|
+
export * from "./hudSkin.js";
|
|
52
|
+
export * from "./hudSkinPreview.js";
|
|
51
53
|
export * from "./hudThemePreview.js";
|
|
52
54
|
export * from "./hudFrame.js";
|
|
53
55
|
export * from "./keyHint.js";
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,8 @@ export * from "./panels.js";
|
|
|
48
48
|
export * from "./characterSheet.js";
|
|
49
49
|
export * from "./talentTree.js";
|
|
50
50
|
export * from "./hudTheme.js";
|
|
51
|
+
export * from "./hudSkin.js";
|
|
52
|
+
export * from "./hudSkinPreview.js";
|
|
51
53
|
export * from "./hudThemePreview.js";
|
|
52
54
|
export * from "./hudFrame.js";
|
|
53
55
|
export * from "./keyHint.js";
|
package/dist/previewFixtures.js
CHANGED
|
@@ -2,6 +2,9 @@ import { BarsPreview } from "./barsPreview.js";
|
|
|
2
2
|
import { HudThemePreview } from "./hudThemePreview.js";
|
|
3
3
|
import { IconsPreview } from "./iconsPreview.js";
|
|
4
4
|
import { StreetGeometryPreview } from "./streetGeometryPreview.js";
|
|
5
|
+
import { SpriteClipPreview } from "./spriteClipPreview.js";
|
|
6
|
+
import { TileLayerPreview } from "./tileLayerPreview.js";
|
|
7
|
+
import { HudSkinPreview } from "./hudSkinPreview.js";
|
|
5
8
|
/**
|
|
6
9
|
* Registry of deterministic, engine-level preview fixtures — the *real* exported
|
|
7
10
|
* `@jgengine/react` components (`HudThemePreview`, `BarsPreview`, `IconsPreview`, …) that render
|
|
@@ -35,6 +38,17 @@ export const PREVIEW_FIXTURES = {
|
|
|
35
38
|
description: "Close-up welded road meshes for turns, unequal crossings, and five-way junctions.",
|
|
36
39
|
component: StreetGeometryPreview,
|
|
37
40
|
},
|
|
41
|
+
SpriteClipPreview: {
|
|
42
|
+
name: "SpriteClipPreview",
|
|
43
|
+
description: "A procedurally drawn two-frame atlas driven by the core sprite clip player.",
|
|
44
|
+
component: SpriteClipPreview,
|
|
45
|
+
},
|
|
46
|
+
TileLayerPreview: {
|
|
47
|
+
name: "TileLayerPreview",
|
|
48
|
+
description: "A procedural 3x3 atlas-backed tile layer preview.",
|
|
49
|
+
component: TileLayerPreview,
|
|
50
|
+
},
|
|
51
|
+
HudSkinPreview: { name: "HudSkinPreview", description: "Two procedural HUD skins applied to frames, bars, and inventory slots.", component: HudSkinPreview },
|
|
38
52
|
};
|
|
39
53
|
/** Sorted list of registered fixture names, for discovery/listing. */
|
|
40
54
|
export function previewFixtureNames() {
|