@jgengine/shell 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +158 -7
- package/dist/GamePlayerShell.js +20 -3
- package/dist/Shell3dPresentation.d.ts +3 -9
- package/dist/Shell3dPresentation.js +26 -6
- package/dist/ShellHudPresentation.d.ts +9 -1
- package/dist/ShellHudPresentation.js +21 -0
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +8 -0
- package/dist/audio/audioEngine.js +65 -6
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +7 -1
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +4 -2
- package/dist/camera/GameFirstPersonCamera.js +52 -15
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +70 -62
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/defineGame.d.ts +9 -0
- package/dist/defineGame.js +20 -3
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +15 -1
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +13 -2
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.js +1 -1
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/postfx/PostProcessing.d.ts +1 -1
- package/dist/postfx/PostProcessing.js +5 -1
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/render/SceneModels.js +1 -1
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/modelLoad.d.ts +29 -0
- package/dist/render/modelLoad.js +61 -1
- package/dist/render/resolveModel.js +4 -0
- package/dist/render/useFootIk.d.ts +53 -4
- package/dist/render/useFootIk.js +197 -70
- package/dist/render/useModelAnimation.d.ts +24 -0
- package/dist/render/useModelAnimation.js +61 -40
- package/dist/scene/AuthoredScene.d.ts +9 -0
- package/dist/scene/AuthoredScene.js +17 -1
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +1 -1
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/structures/BuildingKitBatch.js +26 -22
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +21 -15
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +5 -3
- package/dist/world/WorldParticles.js +1 -1
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +7 -5
- package/skills/jgengine/api.md +118 -89
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-ui/SKILL.md +13 -16
- package/skills/jgengine-ui/api.md +418 -317
- package/skills/jgengine-ui/capabilities.md +16 -0
- package/skills/jgengine-ui/reference-primitives.md +89 -0
- package/skills/jgengine-ui/reference.md +2 -81
- package/skills/jgengine-world/SKILL.md +15 -3
- package/skills/jgengine-world/api.md +873 -626
- package/skills/jgengine-world/capabilities.md +130 -6
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
|
@@ -44,6 +44,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
44
44
|
|
|
45
45
|
- `ActionTooltip` (function) · `import { ActionTooltip } from "@jgengine/react"`
|
|
46
46
|
|
|
47
|
+
## authored-solids — collision for studio-authored world content such as city volumes
|
|
48
|
+
|
|
49
|
+
- `AuthoredSolids` (function) · `import { AuthoredSolids } from "@jgengine/shell/scene"`
|
|
50
|
+
|
|
47
51
|
## auto-scroll — pin a log/chat/console panel to its newest line as entries arrive — no hand-rolled scrollTop effects
|
|
48
52
|
|
|
49
53
|
- `useAutoScroll` (function) · `import { useAutoScroll } from "@jgengine/react"`
|
|
@@ -117,6 +121,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
117
121
|
|
|
118
122
|
- `CreditsScreen` (function) · `import { CreditsScreen } from "@jgengine/react/creditsScreen"`
|
|
119
123
|
|
|
124
|
+
## current-target — read the player's selected target (click or tab target) to drive a target frame
|
|
125
|
+
|
|
126
|
+
- `useTarget` (function) · `import { useTarget } from "@jgengine/react"`
|
|
127
|
+
|
|
120
128
|
## cutscene-letterbox — reskinnable cinematic letterbox + skip overlay for a cutscene — animated bars, caption/dialogue slot, progress line, Skip button
|
|
121
129
|
|
|
122
130
|
- `CutsceneLetterbox` (function) · `import { CutsceneLetterbox } from "@jgengine/react"`
|
|
@@ -205,6 +213,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
205
213
|
|
|
206
214
|
- `GameContextBridge` (function) · `import { GameContextBridge } from "@jgengine/react"`
|
|
207
215
|
|
|
216
|
+
## heavy-models — models over the per-model triangle budget, for perf probes and smoke assertions
|
|
217
|
+
|
|
218
|
+
- `heavyModels` (function) · `import { heavyModels } from "@jgengine/shell/render/modelLoad"`
|
|
219
|
+
|
|
208
220
|
## heightfield-mesh-update — in-place partial update of a displaced ground plane mesh
|
|
209
221
|
|
|
210
222
|
- `displaceHeightfieldGeometry` (function) · `import { displaceHeightfieldGeometry } from "@jgengine/shell/terrain"`
|
|
@@ -515,6 +527,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
515
527
|
|
|
516
528
|
- `formatSpeed` (function) · `import { formatSpeed } from "@jgengine/core/ui"`
|
|
517
529
|
|
|
530
|
+
## split-screen-hud — Per-seat HUD roots positioned over each split-screen viewport.
|
|
531
|
+
|
|
532
|
+
- `ViewportHuds` (function) · `import { ViewportHuds } from "@jgengine/shell/camera/Viewports"`
|
|
533
|
+
|
|
518
534
|
## sprite-2d — animate a sprite atlas clip from headless or rendered game code
|
|
519
535
|
|
|
520
536
|
- `createSpriteClipPlayer` (function) · `import { createSpriteClipPlayer } from "@jgengine/core/render/sprite2d"`
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# JGengine UI — primitives and rendering reference
|
|
2
|
+
|
|
3
|
+
On-demand detail behind [reference.md](reference.md) §6. Search [capabilities.md](capabilities.md) by intent first; open this file when you need how a shipped block composes.
|
|
4
|
+
|
|
5
|
+
## 6. Game UI primitives
|
|
6
|
+
|
|
7
|
+
Prefer small headless or lightly styled primitives over a giant universal design system. Useful concepts include:
|
|
8
|
+
|
|
9
|
+
- `HudAnchor`
|
|
10
|
+
- `StatReadout`
|
|
11
|
+
- `Meter`
|
|
12
|
+
- `ObjectiveTracker`
|
|
13
|
+
- `ActionPrompt`
|
|
14
|
+
- `Reticle`
|
|
15
|
+
- `MinimapFrame`
|
|
16
|
+
- `DialoguePlate`
|
|
17
|
+
- `Countdown`
|
|
18
|
+
- `BossBar`
|
|
19
|
+
- `ItemPickup`
|
|
20
|
+
- `DamageIndicator`
|
|
21
|
+
- `PauseScreen`
|
|
22
|
+
- `ResultsScreen`
|
|
23
|
+
- `VirtualControlZone`
|
|
24
|
+
- `ScreenTransition`
|
|
25
|
+
|
|
26
|
+
A primitive should expose game-oriented choices such as shape, material, urgency, placement, hierarchy, entry motion, icon treatment, compactness, and diegetic-versus-overlay presentation.
|
|
27
|
+
|
|
28
|
+
**Portable minimap adoption.** Existing React games pass `MarkerView[]`, a
|
|
29
|
+
`MarkerSource`, or their existing `MarkerSet` to `Minimap`/`Compass`/`WorldMap`.
|
|
30
|
+
Use `createMarkerSource` (`@jgengine/core/world/markers`) to cache a projection
|
|
31
|
+
from an external ECS/store snapshot; it needs no `GameProvider`, Three.js, or
|
|
32
|
+
React Three Fiber. Follow [the portable minimap recipe](recipes/portable-minimap.md)
|
|
33
|
+
instead of mirroring external entities into `createMarkerSet`.
|
|
34
|
+
|
|
35
|
+
**Minimap** is already shipped, not hand-rolled: `Minimap` / `WorldMap` / `Compass` from `@jgengine/react/map` render a framed SVG minimap over a `MarkerSet` (`createMarkerSet`, `@jgengine/core/world/markers`) and optional `FogField`, projecting via `@jgengine/core/world/minimap` (`projectToMinimap`, `headingToBearing`, edge-clamp). A game feeds flat props (`markers`, `center: [x,z]`, `worldRadius`, `size`, `facingYaw`, `rotate`) and repopulates the marker set each HUD tick from live entities — no custom canvas painter. `MinimapPanel` (same module) composes zone-label + clock header slots and a `Compass` strip around a `Minimap` — pass `zoneLabel`/`clock` nodes (e.g. from `useGameClock().calendar`) and `showCompass`/`compassProps`; omit either header slot and it collapses, `showCompass={false}` drops the strip. **Baked terrain background**: to draw the authored terrain under the markers, pass `background={document.minimap?.background}` and `mapBounds={document.minimap?.bounds}` to `Minimap`/`WorldMap` (or `MinimapPanel`, which forwards both through) — the editor's `bake_minimap` action stores a top-down terrain PNG + world bounds on `EditorDocument.minimap` (see `jgengine-editor`), and runtime reads that stored PNG with no re-rasterization. Both are undefined until baked, so an unbaked game renders the plain map unchanged. Rolling a **custom** minimap around your own course/route content (not the full `Minimap` panel)? `MinimapChrome` is the headless chrome layer other games were re-drawing by hand: nest it inside your own `<svg>` for the ring `frame`, and pass `markers: MinimapChromeMarker[]` (`{ id, position, heading?, color?, radius?, glyph? }`) for edge-clamped dots that draw as a rotated arrow whenever a marker carries a `heading` (the compass-arrow / player-blip layer) — never hand-roll `clampToMinimapEdge` + a `<polygon>` again. **Nameplates**: `WorldNameplates` (`@jgengine/shell/world/WorldHud`) billboards a name + optional 78×6px HP bar over every nearby non-local entity — turn it on declaratively via `defineGame({ nameplates: { roles, maxDistance } })` (mirrors `worldHealthBars`) rather than mounting it by hand; skin every part via `className`/`data-*` slots (`nameplateClassName`, `nameClassName`, `barClassName`, `fillClassName`) or swap the whole markup with `renderNameplate`. When the game does **not** run the JGengine entity store — it owns enemies in its own array/ECS/store — reach for the data-first **entity-frame** seam instead of `WorldNameplates`: `EntityFrames` (`@jgengine/react/entityFrames`, `entity-frames` capability) takes a caller-owned `{ id, worldPosition, ... }[]` plus a `project` (world→screen) and a `renderFrame` that composes the shipped `HealthBar` + a name (reskinned via `barTokens`/`HudTheme`); it owns only the reusable behavior — projection offset, off-screen/behind-camera culling, and nearest-on-top stacking (`layoutEntityFrames` is the pure, unit-tested core). An R3F game gets the projector for free from `WorldEntityFrames` / `useWorldProjection` (`@jgengine/shell/world/WorldEntityFrames`), which samples the live camera and composes the react primitive — overhead nameplates/health bars from your own array in a few lines, no store mirroring. Follow [the overhead entity-frames recipe](recipes/overhead-entity-frames.md). **Selection rings**: `WorldObjectHighlights` (`@jgengine/shell/world/WorldHud`) draws a ground ring over every `ctx.scene.object.selection`-ed placed object — the object-layer counterpart to `WorldEntityBars`/`WorldNameplates`; mount it once in the game's scene (`color`/`radius`/`y` props, `radius` otherwise derived from the object's catalog `halfExtents`) instead of hand-rolling a selection highlight through `WorldOverlay` against external state. **Playing cards** are already shipped, not hand-rolled: `CardFace` / `StackedPile` / `CardStack` (with `Suit`, `Rank`, `PlayingCard`) from `@jgengine/react/cards` render a French-deck face (rank + suit pips, patterned back when `faceDown`) and an overlapping fan. `StackedPile` composes the existing drag system (`useDragLayer`/`DraggableCard`/`DropZone`) — pass a `layer` for drag and `dropId` for a drop target; `CardStack` binds a fan to a headless `CardPileState` zone (`@jgengine/core/cards/cardPile`). Never draw cards on a canvas or reinvent pointer handling. **Swing timer**: `swingTimerState(player, target, prevPeriod, prevTimer)` (`@jgengine/core/ui/swingTimer`) is the pure, parameter-in/next-state-out core for a melee swing bar (recovers period on the reset edge; hidden unless auto-attacking a live non-object target) — thread the returned `nextPeriod`/`nextTimer` back via refs, render a thin `Meter`.
|
|
36
|
+
|
|
37
|
+
**HUD widgets split into three reusable layers** — data, renderer, chrome — so a second presentation drops in without a forked widget. **Chrome**: `HudFrame` (`@jgengine/react/hudFrame`, `hud-frame` capability) is the single framed-panel primitive — `<HudFrame variation="glass"|"plate"|"retro"|"themed" shape="rounded"|"circle"|"square" title aside padding width style>` renders a `data-hud-frame` div with an optional small-caps `title`/`aside` header; `glass` is byte-identical to the built-in widgets' dark-glass panel, `plate` a heavier opaque plate, `retro` a hard black-outlined frame, `themed` driven by the `--jg-frame-*` `HudTheme` tokens (the token-skinning path SKILL.md promotes). Need only the CSS on your own element? spread `hudFrameStyle(variation, shape)`. Reach for it instead of hand-rolling a bordered panel `<div>` per widget. **Renderer**: `MinimapTrack` (`@jgengine/react/map`, `minimap-track-hud` capability) is a structurally-different second minimap — a horizontal linear progress rail (vs the radial `Minimap`) with colored zone `spans` and gate/exit/player `pips` placed by 0..1 fraction, driven by core `trackFraction([x,z], { from, to })` (`@jgengine/core/world/minimapTrack`, `minimap-track` capability — the 1D analogue of `projectToMinimap`). It reads no store; the caller supplies fractions. Use it for corridor/route progress; use the radial `Minimap` for open worlds — both share `HudFrame` chrome. **Data**: `useLiveMarkers(rebuild, { intervalMs?, deps? })` (`@jgengine/react/hooks`, `live-markers` capability) returns a self-ticking `MarkerSet` that each HUD tick clears and re-runs your `rebuild(markers, ctx)` against the live scene — replaces the hand-rolled `useState` ticker + `useEffect` + `createMarkerSet` + `markers.clear()` + rescan boilerplate; feed the returned set straight into `Minimap`/`MinimapPanel`.
|
|
38
|
+
|
|
39
|
+
**Heat/streak gauges** are already shipped, not hand-rolled `clamp(value/max)`: build the gauge with `createEventMeter` (`@jgengine/core/stats/eventMeter`, `event-meter` capability — ult/adrenaline `"hold"` mode or streak/combo `"reset"` mode over tagged event gains) and bind it to a component with `useEventMeter(meter)` (`@jgengine/react`, `event-meter-hud` capability) — it self-ticks a heartbeat and returns `{ value, fraction, tier, ready }` for a `Meter`/`BossBar`-style fill, no manual `setInterval` polling of `meter.value()`.
|
|
40
|
+
|
|
41
|
+
**Action cards / command bars / ability bars** are a headless data → renderer → chrome split, not a hand-rolled cooldown grid. The model is caller-owned data: build `ActionDef[]` (`@jgengine/core/ui/actionModel` — `id`, optional `label`/`icon`/`hotkey`/`group`/`active`/`disabled`, plus `cooldown` via `actionCooldown`/`actionCooldownFromFraction` and `costs` via `actionCost`); `resolveAction`/`resolveActionCollection` compute `enabled` + ordered blocking `reasons` (cooldown, then unmet cost, then caller reasons). React side (`@jgengine/react`): `useActionBar(defs, { columns, wrap, hotkeys, onActivate })` is the DATA/HOOK layer (`action-bar-model` — focus, hover, `moveGridFocus` keyboard/controller grid nav, `actionByHotkey` routing); `ActionBarChrome` is the composable RENDERER (`action-bar-chrome` — `role="toolbar"`, roving tabindex, `layout: "grid" | "list"`, swap per-item chrome with `renderItem`); `ActionButton` is the default item chrome (`action-button` — radial cooldown wipe, cost line, `Keycap` hotkey, full rest/hover/focus/pressed/active/disabled states, `aria-disabled`/`aria-pressed`/`aria-keyshortcuts`); `ActionBar` is the batteries-included skinned default. A radial UI ignores the chrome and lays out `ActionButton`s from the model. Bind a combat `AbilityKit` with `actionFromAbilitySlot(slot, { label, icon, hotkey, resourceId })` over `useAbilitySlots`. Tooltips use the shared seam (no more DOM `title=` hand-rolling): `actionTooltip(resolved)` → `TooltipContent`, rendered by `ActionTooltip` inside a `Popover` (`popover` — pure `placePopover` viewport flip/clamp math from `@jgengine/core/ui/tooltipModel`, SSR-safe). **Selection panels / party frames / squad summaries** are the same split for entities: feed caller-owned `EntitySummaryDef[]` (`@jgengine/core/ui/selectionModel` — `id`, `name`, `kind`, `icon`, `vitals`, `tags`) to `summarizeSelection` (primary focus, `kind` buckets, `grouped` past a threshold, `selectionWindow` virtualization) or the `useSelectionView` hook (`selection-model` — focus stepping + keyboard nav). Renderers (`@jgengine/react`): `EntityPortrait` (`entity-portrait` selectable tile + health strip), `EntitySummary` (`entity-summary` primary detail with `VitalBar` rows), `SelectionGroupChip` (`selection-group-chip` icon+count), `SelectionCollectionChrome` (`selection-collection` portrait strip that flips to group chips when large, roving tabindex), and `SelectionPanel` (`selection-panel` batteries-included primary-summary-beside-strip default). Keep layout, dimensions, hotkeys, catalog-id mapping, renderer, and chrome caller-owned — the defaults are examples, not fixed RTS rules; never fork the model to reskin.
|
|
42
|
+
|
|
43
|
+
**Inventory grid** is already shipped, not hand-rolled: `<InventoryGrid inventoryId columns>` (`@jgengine/react`, backed by the `useInventoryGrid` hook) is a drop-in NxN grid bound to a live inventory — it renders the bound inventory's slots and wires drag-to-move / swap / stack / split through the built-in `inventory.move` / `inventory.split` commands, so a bag is one element plus a reskin, never a re-derived slot matrix over raw divs. It is distinct from the atomic `EquipmentSlots` / `WeaponSlots`, which are single-slot targets for paperdoll and equip layouts — reach for `InventoryGrid` for the bag/backpack surface and the atomic slots for the worn/equipped layout. Reskin via `className`/`data-*` slots and HudTheme tokens; the mechanics stay caller-agnostic.
|
|
44
|
+
|
|
45
|
+
**Toggleable game windows** are already shipped, not hand-rolled open/close state: `usePanels(defs)` + `<PanelHost>` + `<Window>` (`@jgengine/react`) over the headless `@jgengine/core/ui/panelModel` are THE way to build openable windows — bag (`B`), character (`C`), spellbook, quest log — with keybind toggles, drag-to-move, close buttons, ESC-to-close, and z-stack ordering all handled. Declare each window in `defs` (id, keybind, title), mount one `<PanelHost>`, and render each panel's body inside a `<Window>`; the game owns the body content and skin, the model owns focus/stacking/keybind wiring. Never re-derive window toggling from local `useState` booleans and hand-rolled keydown listeners.
|
|
46
|
+
|
|
47
|
+
**Character sheet / paperdoll** is already shipped, not hand-rolled: `<CharacterSheet>` / `<Paperdoll>` / `<StatList>` (`@jgengine/react`) compose the paperdoll equip layout (worn `EquipmentSlots` arranged around a portrait) and the derived-stat readout into one window body. `Paperdoll` is the equip-slot arrangement, `StatList` the derived-stat rows, `CharacterSheet` the batteries-included default that pairs them — the game composes and reskins them (slot layout, stat labels/terminology, skin) rather than re-deriving a paperdoll from raw divs and manual stat math.
|
|
48
|
+
|
|
49
|
+
**Toast/announcer stacks**: two supported sources, pick the one that matches where the message comes from. An engine event already bound onto `ctx.game.feed` (kill feed, loot log, quest update) renders with `@jgengine/react`'s `ToastStack({ action })` directly. A game-raised one-off message with its own lifetime (boss-intro banner, objective callout) belongs in a `createToastQueue` (`@jgengine/core/game/toasts`, `toast-feed` capability — see `jgengine-gameplay`) rendered from `queue.list()`; don't reimplement the append-cap-plus-TTL-prune loop as a local `Toast`/`pushToast` pair.
|
|
50
|
+
|
|
51
|
+
Do not create a primitive whose only value is wrapping a `div` with border radius.
|
|
52
|
+
|
|
53
|
+
## Rendering — post-processing, lighting, shadows
|
|
54
|
+
|
|
55
|
+
**Look presets.** Unset `look` is `"neutral"` (#1719): the game's own `lighting`/`backdrop`/`postProcessing` with an AgX tone-map and no styling. One field picks a whole art direction: `"photoreal"` (high-contrast PBR post), `"cinematic"` (day sky, view-following shadow-casting sun, tone-map/bloom/SSAO/vignette), `"toon"` (clean graphic grade), `"comic"` (ink outlines + cel bands + saturated grade — Borderlands-style), `"retro"` (pixelated, posterized, no smoothing), and `"flat"` (bare ambient+directional rig, no post). Non-neutral presets add a day sky unless the world declares one. The preset only fills knobs you didn't set — any explicit `lighting`/`backdrop`/`postProcessing` always wins. `resolveGameLook` (`@jgengine/core/render/lookPreset`) is the pure resolver; assert on it in a `bun test` (the post/sky layers are not visible to `summarizeEnvironment`). HUD-presentation and no-camera games are unaffected.
|
|
56
|
+
|
|
57
|
+
The post chain itself is never hand-wired render passes. Set `defineGame({ postProcessing })` (`PostProcessingConfig` from `@jgengine/core/render/postProcessing`) and the shell mounts an `EffectComposer` and owns the render: RenderPass → AO → Bloom → tone-map output → Stylize → Grade. Each stage is a config object, `false` to skip, or omitted for its tuned default:
|
|
58
|
+
|
|
59
|
+
- `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `aces`) + `exposure`.
|
|
60
|
+
- `bloom: { strength, radius, threshold }` — HDR glow around bright pixels (defaults 0.32 / 0.55 / 0.85).
|
|
61
|
+
- `grade: { lift, gain, gamma, saturation, vignette, grain }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain.
|
|
62
|
+
- `ao: { radius, intensity, distanceFalloff, blend }` — ground-truth ambient occlusion; heavier than the rest, omit or `false` on low-end targets.
|
|
63
|
+
- `stylize: { outline, bands, pixelSize }` — art style without touching materials. `outline: { color, thickness, opacity, depthThreshold, normalThreshold, fadeDistance }` traces ink lines from depth and normal edges (one extra normal/depth scene render, shadow maps reused); `bands` quantizes brightness into cel bands keeping hue; `pixelSize` snaps the frame to retro blocks. Mix freely: outline alone is a clean comic ink, `bands: 3` plus outline is cartoon, `pixelSize` plus `bands` is retro. Mark custom overlay effects with `POSTFX_OVERLAY_USERDATA` so they are not outlined; transparent, alpha-cut, points, lines and sprites are skipped automatically.
|
|
64
|
+
|
|
65
|
+
**Orbit camera occlusion:** the third-person orbit rig takes `camera: { collision: { enabled, padding?, minTargetDistance? } }` — a spring-arm that raycasts target→camera each frame and pulls the boom in past walls/terrain so the camera never clips inside geometry. Off by default (unchanged chase feel); enable it for any world with interiors or dense structures.
|
|
66
|
+
|
|
67
|
+
Lighting is `defineGame({ lighting })` (`LightingConfig`): ambient / hemisphere / directional, replacing the shell's default lights when set. A `DirectionalLightingConfig` with `castShadow` takes `shadowMapSize`, `shadowCameraSize`, `shadowBias`, `shadowNormalBias` for crisp contact shadows. Sky-lit worlds (a `sky()` world feature) get a high-res sun whose shadow camera follows the view each frame, so grounded shadows stay sharp under the player anywhere in a large world.
|
|
68
|
+
|
|
69
|
+
## First-person viewmodel, WorldOverlay props, render cues (#542)
|
|
70
|
+
|
|
71
|
+
Three composable-chrome seams for FPS/TPS polish — a placement hook plus a good default, never a forced look:
|
|
72
|
+
|
|
73
|
+
- **Custom viewmodel.** `defineGame({ viewmodel })` supplies a component rendered inside the shell's camera-locked, muzzle-tracked anchor in place of the built-in three-mesh gun — read when the active rig is first-person. It receives `ViewmodelProps` (`@jgengine/shell/camera`): `{ cuesRef }`, a live `MutableRefObject<EntityRenderCues>` for the followed entity — read `cuesRef.current` inside your own `useFrame` to drive bob/recoil/reload poses; never store it as render state (it updates every frame). `camera.firstPerson.viewmodel: false` renders no viewmodel at all regardless of `defineGame`'s `viewmodel` field — the explicit opt-out always wins. `readFirstPersonMuzzle(target: THREE.Vector3)` (also `@jgengine/shell/camera`) writes the live muzzle world position into `target` and returns `false` when no viewmodel is mounted — the seam for muzzle-flash lights, tracer origins, or shell casings.
|
|
74
|
+
- **Weapon pose.** `camera.weapon: (entityId) => { handling, presentation } | null` hands the rigs the held weapon's `createWeaponHandling` frame and a `WeaponPresentationTuning` (`@jgengine/core/combat/weaponPresentation`). First person poses the viewmodel anchor from it (hip and ADS offsets, viewmodel FOV, `adsZoom`, sway from look speed, bob from `cuesRef`, recoil kick) and adds the recoil to the look; `shoulder` takes its ADS blend from `adsProgress` and adds the same recoil. A custom `viewmodel` rides the posed anchor. `bun run drive weapon-handling` (and `weapon-handling-shoulder`) is the dev demo.
|
|
75
|
+
- **`WorldOverlay` receives `{ ctx }`.** `defineGame({ WorldOverlay })` components take `WorldOverlayProps` (`@jgengine/core/game/playableGame`) — `ctx: GameContext` handed directly, no extra hook or module-global workaround needed to reach engine state from canvas-layer VFX.
|
|
76
|
+
- **Render cues for any custom rig.** `useEntityRenderCues(instanceId, tuning?)` (`@jgengine/shell/render/useEntityRenderCues`) returns the same `cuesRef` the viewmodel gets, for a custom `renderEntity` component: velocity-driven `bobPhase`, `firing`/`recoil` from `ctx.game.playEntityAnimation(instanceId, "fire")`, `reloading` from `"reload"`/`"reloadEnd"`, `hit` from `combat.hitReaction`, `dead` from `entity.died` — no diffing the parent group's position, no game-side module map for attack timing. `RenderCueTuning` (`@jgengine/core/combat/renderCues`) overrides the bob rate / recoil decay / pulse durations; see `jgengine-combat`'s `combat/renderCues` for the underlying pure math.
|
|
77
|
+
`@jgengine/shell/terrain`'s `ProceduralGround`/`CarvedTerrain`/`GrassField` are the game-facing terrain surface; the height-field math underneath (`arenaField`, `flatField`, `fractalNoise`, `resolveGroundStep`, `valueNoise`, `withNormal`, re-exported from `@jgengine/core/world/terrain`) and `GamePlayerShell`'s physics helpers (`applyMotionImpulses`, `nearbyObstacles`, `resolvePhysicsTuning`, `hasEnvironmentTerrain`, sourced from `core/runtime/motionIntents` / `core/movement`) are internal building blocks the shell composes for you — reach for `defineGame({ movement, world })` and the ground primitives above instead of calling these directly.
|
|
78
|
+
|
|
79
|
+
## 15. Compact implementation API appendix
|
|
80
|
+
|
|
81
|
+
Use the main `jgengine` skill for the authoritative engine API routing. The React package exposes `GameProvider`, hooks, and headless primitives from `@jgengine/react` and its documented subpaths. Common hooks include player/game state, entities, stats, inventory, quests, prompts, clocks, markers, fog, and engine stores. The shell provides `GameHost` (the game mount), input integration, devtools, and `GameUiPreview`; `@jgengine/shell/GamePlayerShell` (the internal player `GameHost` composes) also re-exports the movement primitives it wires up internally (`applyMotionImpulses`, `nearbyObstacles`, `resolvePhysicsTuning`, `hasEnvironmentTerrain`) for a host that needs to replicate its physics loop, and `@jgengine/shell/terrain` re-exports the `jgengine-world` field builders (`arenaField`, `flatField`, `fractalNoise`, `resolveGroundStep`, `valueNoise`, `withNormal`) alongside its own `CarvedTerrain`/`GrassField` components so a shell consumer never needs a second import from core.
|
|
82
|
+
|
|
83
|
+
Use these APIs to bind state; do not let API wiring dictate visual composition. Keybind labels should derive from the game’s binding table, and UI actions should dispatch through game commands rather than existing only as click handlers.
|
|
84
|
+
|
|
85
|
+
The shipped building blocks — inventory grids, toggleable windows, character sheets, bars — are legitimate to compose and are the right starting point; reach for them instead of re-deriving inventory, windowing, or a paperdoll. But neither they nor the headless seams are finished design on their own: the game must still arrange, place, name, and art-direct them into its own look. Compose the building blocks, then skin and lay them out — don't ship them unarranged.
|
|
86
|
+
|
|
87
|
+
## 2D sprite presentation
|
|
88
|
+
|
|
89
|
+
Core owns renderer-independent atlas data and animation state. Convert Aseprite or TexturePacker JSON with `fromAseprite` / `fromTexturePacker` from `@jgengine/core/assets/spriteAtlas`, then drive a clip with `createSpriteClipPlayer` from `@jgengine/core/render/sprite2d`. Persist `snapshot()` and restore it on load; use `retune({ speed })` for runtime playback changes. Use `sortingOrder(layers, layer, offset)` when a game maps named presentation layers to renderer order.
|
|
@@ -206,51 +206,7 @@ Every persistent panel must justify why it exists, remains visible, has that sha
|
|
|
206
206
|
|
|
207
207
|
## 6. Game UI primitives
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
- `HudAnchor`
|
|
212
|
-
- `StatReadout`
|
|
213
|
-
- `Meter`
|
|
214
|
-
- `ObjectiveTracker`
|
|
215
|
-
- `ActionPrompt`
|
|
216
|
-
- `Reticle`
|
|
217
|
-
- `MinimapFrame`
|
|
218
|
-
- `DialoguePlate`
|
|
219
|
-
- `Countdown`
|
|
220
|
-
- `BossBar`
|
|
221
|
-
- `ItemPickup`
|
|
222
|
-
- `DamageIndicator`
|
|
223
|
-
- `PauseScreen`
|
|
224
|
-
- `ResultsScreen`
|
|
225
|
-
- `VirtualControlZone`
|
|
226
|
-
- `ScreenTransition`
|
|
227
|
-
|
|
228
|
-
A primitive should expose game-oriented choices such as shape, material, urgency, placement, hierarchy, entry motion, icon treatment, compactness, and diegetic-versus-overlay presentation.
|
|
229
|
-
|
|
230
|
-
**Portable minimap adoption.** Existing React games pass `MarkerView[]`, a
|
|
231
|
-
`MarkerSource`, or their existing `MarkerSet` to `Minimap`/`Compass`/`WorldMap`.
|
|
232
|
-
Use `createMarkerSource` (`@jgengine/core/world/markers`) to cache a projection
|
|
233
|
-
from an external ECS/store snapshot; it needs no `GameProvider`, Three.js, or
|
|
234
|
-
React Three Fiber. Follow [the portable minimap recipe](recipes/portable-minimap.md)
|
|
235
|
-
instead of mirroring external entities into `createMarkerSet`.
|
|
236
|
-
|
|
237
|
-
**Minimap** is already shipped, not hand-rolled: `Minimap` / `WorldMap` / `Compass` from `@jgengine/react/map` render a framed SVG minimap over a `MarkerSet` (`createMarkerSet`, `@jgengine/core/world/markers`) and optional `FogField`, projecting via `@jgengine/core/world/minimap` (`projectToMinimap`, `headingToBearing`, edge-clamp). A game feeds flat props (`markers`, `center: [x,z]`, `worldRadius`, `size`, `facingYaw`, `rotate`) and repopulates the marker set each HUD tick from live entities — no custom canvas painter. `MinimapPanel` (same module) composes zone-label + clock header slots and a `Compass` strip around a `Minimap` — pass `zoneLabel`/`clock` nodes (e.g. from `useGameClock().calendar`) and `showCompass`/`compassProps`; omit either header slot and it collapses, `showCompass={false}` drops the strip. **Baked terrain background**: to draw the authored terrain under the markers, pass `background={document.minimap?.background}` and `mapBounds={document.minimap?.bounds}` to `Minimap`/`WorldMap` (or `MinimapPanel`, which forwards both through) — the editor's `bake_minimap` action stores a top-down terrain PNG + world bounds on `EditorDocument.minimap` (see `jgengine-editor`), and runtime reads that stored PNG with no re-rasterization. Both are undefined until baked, so an unbaked game renders the plain map unchanged. Rolling a **custom** minimap around your own course/route content (not the full `Minimap` panel)? `MinimapChrome` is the headless chrome layer other games were re-drawing by hand: nest it inside your own `<svg>` for the ring `frame`, and pass `markers: MinimapChromeMarker[]` (`{ id, position, heading?, color?, radius?, glyph? }`) for edge-clamped dots that draw as a rotated arrow whenever a marker carries a `heading` (the compass-arrow / player-blip layer) — never hand-roll `clampToMinimapEdge` + a `<polygon>` again. **Nameplates**: `WorldNameplates` (`@jgengine/shell/world/WorldHud`) billboards a name + optional 78×6px HP bar over every nearby non-local entity — turn it on declaratively via `defineGame({ nameplates: { roles, maxDistance } })` (mirrors `worldHealthBars`) rather than mounting it by hand; skin every part via `className`/`data-*` slots (`nameplateClassName`, `nameClassName`, `barClassName`, `fillClassName`) or swap the whole markup with `renderNameplate`. When the game does **not** run the JGengine entity store — it owns enemies in its own array/ECS/store — reach for the data-first **entity-frame** seam instead of `WorldNameplates`: `EntityFrames` (`@jgengine/react/entityFrames`, `entity-frames` capability) takes a caller-owned `{ id, worldPosition, ... }[]` plus a `project` (world→screen) and a `renderFrame` that composes the shipped `HealthBar` + a name (reskinned via `barTokens`/`HudTheme`); it owns only the reusable behavior — projection offset, off-screen/behind-camera culling, and nearest-on-top stacking (`layoutEntityFrames` is the pure, unit-tested core). An R3F game gets the projector for free from `WorldEntityFrames` / `useWorldProjection` (`@jgengine/shell/world/WorldEntityFrames`), which samples the live camera and composes the react primitive — overhead nameplates/health bars from your own array in a few lines, no store mirroring. Follow [the overhead entity-frames recipe](recipes/overhead-entity-frames.md). **Selection rings**: `WorldObjectHighlights` (`@jgengine/shell/world/WorldHud`) draws a ground ring over every `ctx.scene.object.selection`-ed placed object — the object-layer counterpart to `WorldEntityBars`/`WorldNameplates`; mount it once in the game's scene (`color`/`radius`/`y` props, `radius` otherwise derived from the object's catalog `halfExtents`) instead of hand-rolling a selection highlight through `WorldOverlay` against external state. **Playing cards** are already shipped, not hand-rolled: `CardFace` / `StackedPile` / `CardStack` (with `Suit`, `Rank`, `PlayingCard`) from `@jgengine/react/cards` render a French-deck face (rank + suit pips, patterned back when `faceDown`) and an overlapping fan. `StackedPile` composes the existing drag system (`useDragLayer`/`DraggableCard`/`DropZone`) — pass a `layer` for drag and `dropId` for a drop target; `CardStack` binds a fan to a headless `CardPileState` zone (`@jgengine/core/cards/cardPile`). Never draw cards on a canvas or reinvent pointer handling. **Swing timer**: `swingTimerState(player, target, prevPeriod, prevTimer)` (`@jgengine/core/ui/swingTimer`) is the pure, parameter-in/next-state-out core for a melee swing bar (recovers period on the reset edge; hidden unless auto-attacking a live non-object target) — thread the returned `nextPeriod`/`nextTimer` back via refs, render a thin `Meter`.
|
|
238
|
-
|
|
239
|
-
**HUD widgets split into three reusable layers** — data, renderer, chrome — so a second presentation drops in without a forked widget. **Chrome**: `HudFrame` (`@jgengine/react/hudFrame`, `hud-frame` capability) is the single framed-panel primitive — `<HudFrame variation="glass"|"plate"|"retro"|"themed" shape="rounded"|"circle"|"square" title aside padding width style>` renders a `data-hud-frame` div with an optional small-caps `title`/`aside` header; `glass` is byte-identical to the built-in widgets' dark-glass panel, `plate` a heavier opaque plate, `retro` a hard black-outlined frame, `themed` driven by the `--jg-frame-*` `HudTheme` tokens (the token-skinning path SKILL.md promotes). Need only the CSS on your own element? spread `hudFrameStyle(variation, shape)`. Reach for it instead of hand-rolling a bordered panel `<div>` per widget. **Renderer**: `MinimapTrack` (`@jgengine/react/map`, `minimap-track-hud` capability) is a structurally-different second minimap — a horizontal linear progress rail (vs the radial `Minimap`) with colored zone `spans` and gate/exit/player `pips` placed by 0..1 fraction, driven by core `trackFraction([x,z], { from, to })` (`@jgengine/core/world/minimapTrack`, `minimap-track` capability — the 1D analogue of `projectToMinimap`). It reads no store; the caller supplies fractions. Use it for corridor/route progress; use the radial `Minimap` for open worlds — both share `HudFrame` chrome. **Data**: `useLiveMarkers(rebuild, { intervalMs?, deps? })` (`@jgengine/react/hooks`, `live-markers` capability) returns a self-ticking `MarkerSet` that each HUD tick clears and re-runs your `rebuild(markers, ctx)` against the live scene — replaces the hand-rolled `useState` ticker + `useEffect` + `createMarkerSet` + `markers.clear()` + rescan boilerplate; feed the returned set straight into `Minimap`/`MinimapPanel`.
|
|
240
|
-
|
|
241
|
-
**Heat/streak gauges** are already shipped, not hand-rolled `clamp(value/max)`: build the gauge with `createEventMeter` (`@jgengine/core/stats/eventMeter`, `event-meter` capability — ult/adrenaline `"hold"` mode or streak/combo `"reset"` mode over tagged event gains) and bind it to a component with `useEventMeter(meter)` (`@jgengine/react`, `event-meter-hud` capability) — it self-ticks a heartbeat and returns `{ value, fraction, tier, ready }` for a `Meter`/`BossBar`-style fill, no manual `setInterval` polling of `meter.value()`.
|
|
242
|
-
|
|
243
|
-
**Action cards / command bars / ability bars** are a headless data → renderer → chrome split, not a hand-rolled cooldown grid. The model is caller-owned data: build `ActionDef[]` (`@jgengine/core/ui/actionModel` — `id`, optional `label`/`icon`/`hotkey`/`group`/`active`/`disabled`, plus `cooldown` via `actionCooldown`/`actionCooldownFromFraction` and `costs` via `actionCost`); `resolveAction`/`resolveActionCollection` compute `enabled` + ordered blocking `reasons` (cooldown, then unmet cost, then caller reasons). React side (`@jgengine/react`): `useActionBar(defs, { columns, wrap, hotkeys, onActivate })` is the DATA/HOOK layer (`action-bar-model` — focus, hover, `moveGridFocus` keyboard/controller grid nav, `actionByHotkey` routing); `ActionBarChrome` is the composable RENDERER (`action-bar-chrome` — `role="toolbar"`, roving tabindex, `layout: "grid" | "list"`, swap per-item chrome with `renderItem`); `ActionButton` is the default item chrome (`action-button` — radial cooldown wipe, cost line, `Keycap` hotkey, full rest/hover/focus/pressed/active/disabled states, `aria-disabled`/`aria-pressed`/`aria-keyshortcuts`); `ActionBar` is the batteries-included skinned default. A radial UI ignores the chrome and lays out `ActionButton`s from the model. Bind a combat `AbilityKit` with `actionFromAbilitySlot(slot, { label, icon, hotkey, resourceId })` over `useAbilitySlots`. Tooltips use the shared seam (no more DOM `title=` hand-rolling): `actionTooltip(resolved)` → `TooltipContent`, rendered by `ActionTooltip` inside a `Popover` (`popover` — pure `placePopover` viewport flip/clamp math from `@jgengine/core/ui/tooltipModel`, SSR-safe). **Selection panels / party frames / squad summaries** are the same split for entities: feed caller-owned `EntitySummaryDef[]` (`@jgengine/core/ui/selectionModel` — `id`, `name`, `kind`, `icon`, `vitals`, `tags`) to `summarizeSelection` (primary focus, `kind` buckets, `grouped` past a threshold, `selectionWindow` virtualization) or the `useSelectionView` hook (`selection-model` — focus stepping + keyboard nav). Renderers (`@jgengine/react`): `EntityPortrait` (`entity-portrait` selectable tile + health strip), `EntitySummary` (`entity-summary` primary detail with `VitalBar` rows), `SelectionGroupChip` (`selection-group-chip` icon+count), `SelectionCollectionChrome` (`selection-collection` portrait strip that flips to group chips when large, roving tabindex), and `SelectionPanel` (`selection-panel` batteries-included primary-summary-beside-strip default). Keep layout, dimensions, hotkeys, catalog-id mapping, renderer, and chrome caller-owned — the defaults are examples, not fixed RTS rules; never fork the model to reskin.
|
|
244
|
-
|
|
245
|
-
**Inventory grid** is already shipped, not hand-rolled: `<InventoryGrid inventoryId columns>` (`@jgengine/react`, backed by the `useInventoryGrid` hook) is a drop-in NxN grid bound to a live inventory — it renders the bound inventory's slots and wires drag-to-move / swap / stack / split through the built-in `inventory.move` / `inventory.split` commands, so a bag is one element plus a reskin, never a re-derived slot matrix over raw divs. It is distinct from the atomic `EquipmentSlots` / `WeaponSlots`, which are single-slot targets for paperdoll and equip layouts — reach for `InventoryGrid` for the bag/backpack surface and the atomic slots for the worn/equipped layout. Reskin via `className`/`data-*` slots and HudTheme tokens; the mechanics stay caller-agnostic.
|
|
246
|
-
|
|
247
|
-
**Toggleable game windows** are already shipped, not hand-rolled open/close state: `usePanels(defs)` + `<PanelHost>` + `<Window>` (`@jgengine/react`) over the headless `@jgengine/core/ui/panelModel` are THE way to build openable windows — bag (`B`), character (`C`), spellbook, quest log — with keybind toggles, drag-to-move, close buttons, ESC-to-close, and z-stack ordering all handled. Declare each window in `defs` (id, keybind, title), mount one `<PanelHost>`, and render each panel's body inside a `<Window>`; the game owns the body content and skin, the model owns focus/stacking/keybind wiring. Never re-derive window toggling from local `useState` booleans and hand-rolled keydown listeners.
|
|
248
|
-
|
|
249
|
-
**Character sheet / paperdoll** is already shipped, not hand-rolled: `<CharacterSheet>` / `<Paperdoll>` / `<StatList>` (`@jgengine/react`) compose the paperdoll equip layout (worn `EquipmentSlots` arranged around a portrait) and the derived-stat readout into one window body. `Paperdoll` is the equip-slot arrangement, `StatList` the derived-stat rows, `CharacterSheet` the batteries-included default that pairs them — the game composes and reskins them (slot layout, stat labels/terminology, skin) rather than re-deriving a paperdoll from raw divs and manual stat math.
|
|
250
|
-
|
|
251
|
-
**Toast/announcer stacks**: two supported sources, pick the one that matches where the message comes from. An engine event already bound onto `ctx.game.feed` (kill feed, loot log, quest update) renders with `@jgengine/react`'s `ToastStack({ action })` directly. A game-raised one-off message with its own lifetime (boss-intro banner, objective callout) belongs in a `createToastQueue` (`@jgengine/core/game/toasts`, `toast-feed` capability — see `jgengine-gameplay`) rendered from `queue.list()`; don't reimplement the append-cap-plus-TTL-prune loop as a local `Toast`/`pushToast` pair.
|
|
252
|
-
|
|
253
|
-
Do not create a primitive whose only value is wrapping a `div` with border radius.
|
|
209
|
+
The primitive catalog (minimaps, nameplates, action/selection bars, inventory, windows, character sheet, toasts), rendering and post-processing, the first-person viewmodel, the API appendix, and 2D sprites are in [reference-primitives.md](reference-primitives.md). Search [capabilities.md](capabilities.md) first.
|
|
254
210
|
|
|
255
211
|
## 7. Complete interaction states
|
|
256
212
|
|
|
@@ -281,30 +237,6 @@ Do not communicate all states with background-color changes alone. Use appropria
|
|
|
281
237
|
|
|
282
238
|
Focus must look authored while remaining accessible. Menus should support keyboard/controller-style focus navigation when practical.
|
|
283
239
|
|
|
284
|
-
## Rendering — post-processing, lighting, shadows
|
|
285
|
-
|
|
286
|
-
**Default look is cinematic (#773).** A 3D game reads lit-like-a-shipped-game out of the box: unset `look` (or `look: "cinematic"`) composes the existing sky/lighting/post knobs — a real day sky with a view-following shadow-casting sun + hemisphere fill, and a tuned tone-map/bloom/gentle-SSAO/vignette post stack. One field opts out: `defineGame({ look: "flat" })` restores the bare ambient+directional rig (the old default) with no post. The preset only fills knobs you didn't set — any explicit `lighting`/`backdrop`/`postProcessing` always wins, and it never adds a sky when the world already declares one. `resolveGameLook` (`@jgengine/core/render/lookPreset`) is the pure resolver behind it — expand a `look` into concrete `{ lighting, backdrop, postProcessing }` and assert on the result in a `bun test` (the post/sky layers are not visible to `summarizeEnvironment`). HUD-presentation and no-camera games are unaffected.
|
|
287
|
-
|
|
288
|
-
The post chain itself is never hand-wired render passes. Set `defineGame({ postProcessing })` (`PostProcessingConfig` from `@jgengine/core/render/postProcessing`) and the shell mounts an `EffectComposer` and owns the render: RenderPass → AO → Bloom → tone-map output → Grade. Each stage is a config object, `false` to skip, or omitted for its tuned default:
|
|
289
|
-
|
|
290
|
-
- `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `aces`) + `exposure`.
|
|
291
|
-
- `bloom: { strength, radius, threshold }` — HDR glow around bright pixels (defaults 0.32 / 0.55 / 0.85).
|
|
292
|
-
- `grade: { lift, gain, gamma, saturation, vignette, grain }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain.
|
|
293
|
-
- `ao: { radius, intensity, distanceFalloff, blend }` — ground-truth ambient occlusion; heavier than the rest, omit or `false` on low-end targets.
|
|
294
|
-
|
|
295
|
-
**Orbit camera occlusion:** the third-person orbit rig takes `camera: { collision: { enabled, padding?, minTargetDistance? } }` — a spring-arm that raycasts target→camera each frame and pulls the boom in past walls/terrain so the camera never clips inside geometry. Off by default (unchanged chase feel); enable it for any world with interiors or dense structures.
|
|
296
|
-
|
|
297
|
-
Lighting is `defineGame({ lighting })` (`LightingConfig`): ambient / hemisphere / directional, replacing the shell's default lights when set. A `DirectionalLightingConfig` with `castShadow` takes `shadowMapSize`, `shadowCameraSize`, `shadowBias`, `shadowNormalBias` for crisp contact shadows. Sky-lit worlds (a `sky()` world feature) get a high-res sun whose shadow camera follows the view each frame, so grounded shadows stay sharp under the player anywhere in a large world.
|
|
298
|
-
|
|
299
|
-
## First-person viewmodel, WorldOverlay props, render cues (#542)
|
|
300
|
-
|
|
301
|
-
Three composable-chrome seams for FPS/TPS polish — a placement hook plus a good default, never a forced look:
|
|
302
|
-
|
|
303
|
-
- **Custom viewmodel.** `defineGame({ viewmodel })` supplies a component rendered inside the shell's camera-locked, muzzle-tracked anchor in place of the built-in three-mesh gun — read when the active rig is first-person. It receives `ViewmodelProps` (`@jgengine/shell/camera`): `{ cuesRef }`, a live `MutableRefObject<EntityRenderCues>` for the followed entity — read `cuesRef.current` inside your own `useFrame` to drive bob/recoil/reload poses; never store it as render state (it updates every frame). `camera.firstPerson.viewmodel: false` renders no viewmodel at all regardless of `defineGame`'s `viewmodel` field — the explicit opt-out always wins. `readFirstPersonMuzzle(target: THREE.Vector3)` (also `@jgengine/shell/camera`) writes the live muzzle world position into `target` and returns `false` when no viewmodel is mounted — the seam for muzzle-flash lights, tracer origins, or shell casings.
|
|
304
|
-
- **`WorldOverlay` receives `{ ctx }`.** `defineGame({ WorldOverlay })` components take `WorldOverlayProps` (`@jgengine/core/game/playableGame`) — `ctx: GameContext` handed directly, no extra hook or module-global workaround needed to reach engine state from canvas-layer VFX.
|
|
305
|
-
- **Render cues for any custom rig.** `useEntityRenderCues(instanceId, tuning?)` (`@jgengine/shell/render/useEntityRenderCues`) returns the same `cuesRef` the viewmodel gets, for a custom `renderEntity` component: velocity-driven `bobPhase`, `firing`/`recoil` from `ctx.game.playEntityAnimation(instanceId, "fire")`, `reloading` from `"reload"`/`"reloadEnd"`, `hit` from `combat.hitReaction`, `dead` from `entity.died` — no diffing the parent group's position, no game-side module map for attack timing. `RenderCueTuning` (`@jgengine/core/combat/renderCues`) overrides the bob rate / recoil decay / pulse durations; see `jgengine-combat`'s `combat/renderCues` for the underlying pure math.
|
|
306
|
-
`@jgengine/shell/terrain`'s `ProceduralGround`/`CarvedTerrain`/`GrassField` are the game-facing terrain surface; the height-field math underneath (`arenaField`, `flatField`, `fractalNoise`, `resolveGroundStep`, `valueNoise`, `withNormal`, re-exported from `@jgengine/core/world/terrain`) and `GamePlayerShell`'s physics helpers (`applyMotionImpulses`, `nearbyObstacles`, `resolvePhysicsTuning`, `hasEnvironmentTerrain`, sourced from `core/runtime/motionIntents` / `core/movement`) are internal building blocks the shell composes for you — reach for `defineGame({ movement, world })` and the ground primitives above instead of calling these directly.
|
|
307
|
-
|
|
308
240
|
## Main menu
|
|
309
241
|
|
|
310
242
|
**The main menu is the game's front-end, not a marketing splash.** The hosting website/runner loads straight into the game's own menu (see rejection criteria) — there is no engine-supplied main menu, and the page must not render a title card, tagline, or "enter game" button of its own. What the player first sees is a screen the game designs and skins in its own art direction: the same shape/material/type language as its HUD, not a landing page, hero banner, feature list, or store page. Homage/credit lines are fine as small print; the screen's job is to start play, not to sell it.
|
|
@@ -327,7 +259,7 @@ Only show entries the game supports, and disable (don't delete) verbs that are t
|
|
|
327
259
|
|
|
328
260
|
**Settings is game-owned and reachable from a place that makes sense.** Every game ships settings — but as *its* settings, skinned in its own art direction and placed inline with its own UI, never the engine's stock face dropped into a corner. The two canonical homes are the **main menu** and the **pause** screen; put the entry where the player would look, not floating over live play. The engine gives you the menu's contents and behavior for free — you own its skin, layout choice, and placement. If the default four layouts do not match the game, drive `useSettings()` and render the rows entirely inside your own chrome.
|
|
329
261
|
|
|
330
|
-
**Settings menu (themed, four layouts, no forced chrome).** The engine builds the whole menu for free — Sound (master + per-bus volume), Graphics (
|
|
262
|
+
**Settings menu (themed, four layouts, no forced chrome).** The engine builds the whole menu for free — Sound (master + per-bus volume), Graphics (Low/Medium/High/Ultra tier, render scale, frame-rate limit (V-Sync or 30/60/120/144 cap), texture filtering, shadows, per-stage post toggles, UI scale, FPS counter), Gameplay (FOV slider, default 40–120), Controls (per-action key rebinding, inline click-to-rebind, persisted) — from the game's `audio.buses` and `input` map. What it does **not** do is bolt a fixed gear onto every game: **there is no auto trigger.** You place the entry yourself so it lives *inline with your game's own UI*, never a stray corner overlay. Drop `<SettingsTrigger className=…>` (from `@jgengine/react`) anywhere in your HUD or menu — headless button, `className` for skin/placement, optional `children` to replace the default gear glyph, renders nothing when there's nothing to show. Or call `useSettings().open()` from your own control. Tune the menu via `defineGame({ settings })` (`GameSettingsConfig` from `@jgengine/core/settings/settingsModel`):
|
|
331
263
|
|
|
332
264
|
- `variant: "panel" | "sheet" | "sidebar" | "fullscreen"` — the layout + skin (default `panel`; `sheet` is the mobile bottom-sheet). All four are fixed-size (no shrink-to-content jitter) and read the game's `--jg-*` theme tokens, falling back to a neutral dark skin.
|
|
333
265
|
- `actions: SettingsActionDef[]` — game-state actions (Restart, Quit to menu, …). They become the **first "Game" tab, shown before anything else** — the home for buttons that used to float over the HUD. Each: `{ id, label, kind?: "default"|"danger", description?, run(ctx) }`; the menu closes right after `run`.
|
|
@@ -525,14 +457,6 @@ Require revision when any of these are true:
|
|
|
525
457
|
- settings ship as the engine's unskinned stock menu instead of the game's own skin and placement, or sit floating over live play with no sensible home
|
|
526
458
|
- credits are absent from the game, or attribution lives only in a build artifact instead of a reachable in-game screen
|
|
527
459
|
|
|
528
|
-
## 15. Compact implementation API appendix
|
|
529
|
-
|
|
530
|
-
Use the main `jgengine` skill for the authoritative engine API routing. The React package exposes `GameProvider`, hooks, and headless primitives from `@jgengine/react` and its documented subpaths. Common hooks include player/game state, entities, stats, inventory, quests, prompts, clocks, markers, fog, and engine stores. The shell provides `GameHost` (the game mount), input integration, devtools, and `GameUiPreview`; `@jgengine/shell/GamePlayerShell` (the internal player `GameHost` composes) also re-exports the movement primitives it wires up internally (`applyMotionImpulses`, `nearbyObstacles`, `resolvePhysicsTuning`, `hasEnvironmentTerrain`) for a host that needs to replicate its physics loop, and `@jgengine/shell/terrain` re-exports the `jgengine-world` field builders (`arenaField`, `flatField`, `fractalNoise`, `resolveGroundStep`, `valueNoise`, `withNormal`) alongside its own `CarvedTerrain`/`GrassField` components so a shell consumer never needs a second import from core.
|
|
531
|
-
|
|
532
|
-
Use these APIs to bind state; do not let API wiring dictate visual composition. Keybind labels should derive from the game’s binding table, and UI actions should dispatch through game commands rather than existing only as click handlers.
|
|
533
|
-
|
|
534
|
-
The shipped building blocks — inventory grids, toggleable windows, character sheets, bars — are legitimate to compose and are the right starting point; reach for them instead of re-deriving inventory, windowing, or a paperdoll. But neither they nor the headless seams are finished design on their own: the game must still arrange, place, name, and art-direct them into its own look. Compose the building blocks, then skin and lay them out — don't ship them unarranged.
|
|
535
|
-
|
|
536
460
|
## Definition of done
|
|
537
461
|
|
|
538
462
|
UI work is complete only when:
|
|
@@ -551,6 +475,3 @@ UI work is complete only when:
|
|
|
551
475
|
- screenshots have been reviewed and revised
|
|
552
476
|
- the implementation remains accessible and performant
|
|
553
477
|
- future generated UI is explicitly prevented from falling back to generic website-card styling
|
|
554
|
-
## 2D sprite presentation
|
|
555
|
-
|
|
556
|
-
Core owns renderer-independent atlas data and animation state. Convert Aseprite or TexturePacker JSON with `fromAseprite` / `fromTexturePacker` from `@jgengine/core/assets/spriteAtlas`, then drive a clip with `createSpriteClipPlayer` from `@jgengine/core/render/sprite2d`. Persist `snapshot()` and restore it on load; use `retune({ speed })` for runtime playback changes. Use `sortingOrder(layers, layer, offset)` when a game maps named presentation layers to renderer order.
|
|
@@ -9,30 +9,38 @@ description: Build spatial runtime, movement, interaction, AI, and environments.
|
|
|
9
9
|
|
|
10
10
|
This skill owns runtime spatial behavior: authored-scene consumption, environments, movement, input mapping, interaction, AI, navigation, physics, visibility, time, and spatial audio. Scene creation and placement stay in `jgengine-editor`; presentation stays in `jgengine-ui`.
|
|
11
11
|
|
|
12
|
-
Search [capabilities.md](capabilities.md) by intent before designing a primitive. Use [api.md](api.md) for exact exports and [reference.md](reference.md) for environment, camera, navigation, physics, audio, and authored-scene recipes.
|
|
12
|
+
Search [capabilities.md](capabilities.md) by intent before designing a primitive. Use [api.md](api.md) for exact exports and [reference.md](reference.md) for environment, camera, navigation, physics, audio, and authored-scene recipes. Culling and streaming overrides are in [visibility.md](visibility.md).
|
|
13
13
|
|
|
14
14
|
For a *connected* walkthrough that wires several primitives into a running loop, read the goal-oriented recipes under [recipes/](recipes/) — organized by composition seam (what connects to what), never by genre. Start there before reading a game's source to learn how pieces fit; a hybrid game is just a different composition of the same primitives.
|
|
15
15
|
|
|
16
16
|
- [recipes/commanding-entities.md](recipes/commanding-entities.md) — select → order → tick → resolve: direct command over a group of entities (the command loop behind squad control, base-defense, herding, autobattle, or a mutating game's real-time phase).
|
|
17
|
+
- [recipes/npc-senses.md](recipes/npc-senses.md) — perceive → remember → decide → route: perception memory written into a decision-graph blackboard, with nav-mesh routing (guards, mobs, pedestrians, police response).
|
|
17
18
|
- [recipes/building-kit.md](recipes/building-kit.md) — bind the facade generator's part slots to real models: one massing, swappable art per style, blocks where the kit is silent.
|
|
19
|
+
- [recipes/vehicle-feel.md](recipes/vehicle-feel.md) — feel target → handling metrics → physical knobs for a ground vehicle, plus the camera, sound and rumble hooks that hang off its telemetry.
|
|
20
|
+
- [recipes/flight-feel.md](recipes/flight-feel.md) — an aircraft as physical numbers: surfaces, inertia and engine, with the symptom → knob table for how it flies.
|
|
21
|
+
- [recipes/controls.md](recipes/controls.md) — actions → layered contexts that rebind live → keyboard, touch and gamepad on the same actions → per-axis feel.
|
|
22
|
+
- [recipes/character-animation.md](recipes/character-animation.md) — clips → graph → rig → ground: data-driven state machines, triggers and clip events, root motion, foot IK that keeps feet on slopes, and instanced crowds.
|
|
18
23
|
|
|
19
24
|
## Canonical workflows
|
|
20
25
|
|
|
21
26
|
### Declare the place
|
|
22
27
|
|
|
23
|
-
A world is the place you play in: substrate + laws, via `world()` from `@jgengine/core/world/place` — `{ id, ground: { mode: flat|round|voxel|board, size, surface?, generator? }, physics? }`. Thin is complete: flat with `Infinity` axes for 3D, `board` for 2D, or omit `world` for pure UI/rules games. Never put seed, sky, or vegetation in a world definition — seeds derive from world id + save/run (`seedForPlace`); sky look and foliage scatter are editor-authored scene content, with the engine default sky when the document has none. `ground.surface` carries matter/feel laws (metal vs slime vs felt) that the same rule systems read — swap the surface, not the rules. See [reference.md](reference.md) for the full shape; `environment()` is the
|
|
28
|
+
A world is the place you play in: substrate + laws, via `world()` from `@jgengine/core/world/place` — `{ id, ground: { mode: flat|round|voxel|board, size, surface?, generator? }, physics? }`. Thin is complete: flat with `Infinity` axes for 3D, `board` for 2D, or omit `world` for pure UI/rules games. Never put seed, sky, or vegetation in a world definition — seeds derive from world id + save/run (`seedForPlace`); sky look and foliage scatter are editor-authored scene content, with the engine default sky when the document has none. `ground.surface` carries matter/feel laws (metal vs slime vs felt) that the same rule systems read — swap the surface, not the rules. See [reference.md](reference.md) for the full shape; `environment()` is the consumption path for editor/preset-written scene data: a created 3D game's `world.ts` passes it the document's sculpt, sky, and clearings through `environmentContentFromDocument`, never code-declared noise or coordinates.
|
|
24
29
|
|
|
25
30
|
### Authored world
|
|
26
31
|
|
|
27
32
|
1. Load the scene document through the shared authored-scene feature.
|
|
28
33
|
2. Render objects, paths, terrain, foliage, and markers generically.
|
|
29
34
|
3. Query the same ids/layers for spawns, routes, plots, and interaction.
|
|
30
|
-
4.
|
|
35
|
+
4. Collision for generated and studio content comes from its data through `ctx.world.solids`; never hand-place invisible blockers for it (see reference.md, World solids).
|
|
36
|
+
5. Keep derived caches rebuildable; the document remains authoritative.
|
|
31
37
|
|
|
32
38
|
### Movement and interaction
|
|
33
39
|
|
|
34
40
|
Choose input intent, controller/motor, collision/navigation, and camera as separate seams. Commands express game intent; world systems resolve motion. Interaction targets come from bounded spatial/sensor queries, not full-world scans.
|
|
35
41
|
|
|
42
|
+
Walk feel is data, not code. `movement.feel` sets `groundAcceleration`, `airAcceleration` and `groundFriction` (response rates, 1/s) plus `runMultiplier`, `crouchMultiplier`, jump forgiveness (`jumpBufferMs`, `coyoteMs`) and jump shape (`jumpCutFactor`, `apexGravityScale`, `fallGravityScale`, `landingRecoveryMs`), and `physics` sets `gravity` and `jumpVelocity`. A floaty platformer and a weighty shooter differ only in these numbers; pin them with `measureMovement` (`movement/movementProbe`) in a test.
|
|
43
|
+
|
|
36
44
|
### Stateful placed objects
|
|
37
45
|
|
|
38
46
|
Per-instance state belongs on the placement, not in a game-owned map keyed by `instanceId`. Set `SceneObject.state` (opaque) through `ctx.scene.object.setState`/`patchState`; declare `slotInventory` on the catalog entry and mutate contents through `ctx.scene.object.slots`, which enforces the declared capacity and `accepts`. Both replicate and save with the object through the always-on `objects` snapshot module, and `toRuntimeObjectRow`/`fromRuntimeObjectRow` (`@jgengine/core/multiplayer`) convert to and from the host's persisted rows.
|
|
@@ -41,6 +49,10 @@ Per-instance state belongs on the placement, not in a game-owned map keyed by `i
|
|
|
41
49
|
|
|
42
50
|
Compose perception, selection, planning/behavior, movement, and lifecycle independently. Inject randomness and scheduling. Use spatial indexes, interest tiers, or bounded candidate sets for scale.
|
|
43
51
|
|
|
52
|
+
NPC decisions are a `decisionGraph` behavior: the graph and its blackboard are data, actions are registered by name, and perception or game systems write facts into `behaviorControl(ctx).blackboard(id)` rather than actions querying the world. Set `thinkInterval` so crowds think a few times a second, not every frame, and release movement or claims in `onAbort`.
|
|
53
|
+
|
|
54
|
+
Route on a polygon nav mesh through one `createNavMeshQuery(mesh)` per mesh; price terrain and gate doors with `areaCosts` via `retune`, and keep `maxNodes` bounded so a long request returns a `partial` route instead of stalling a frame.
|
|
55
|
+
|
|
44
56
|
### Fallback-seam diagnostics
|
|
45
57
|
|
|
46
58
|
Render seams fall back to placeholders when content is unauthored: default green ground (no environment), primitive capsule/box actors (no `entityModels`/`objectModels` mapping), stylized proxy foliage (no scatter `resolveItem`). These placeholders are sometimes intended, so runtime never changes — but a dev-only, opt-in probe (`@jgengine/core/devtools/fallbackSeams`) tells "unauthored/misconfigured" apart from "intended". The shell arms it via `armFallbackSeams(devtoolsEnabled)`; each seam reports at its choice site (`reportFallbackSeam`), and counts surface through the devtools `fallbacks` probe. Disarmed (production) it is a pure, allocation-free no-op.
|