@jgengine/core 0.22.0 → 0.24.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 +110 -0
- package/dist/audio/uiCues.d.ts +26 -0
- package/dist/audio/uiCues.js +118 -0
- package/dist/commands/commandRegistry.d.ts +4 -0
- package/dist/commands/commandRegistry.js +5 -2
- package/dist/game/events.d.ts +12 -0
- package/dist/game/notices.d.ts +85 -0
- package/dist/game/notices.js +105 -0
- package/dist/game/playableGame.d.ts +37 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +104 -1
- package/dist/render/lightingBalance.d.ts +13 -0
- package/dist/render/lightingBalance.js +32 -0
- package/dist/render/lookPreset.d.ts +10 -1
- package/dist/render/lookPreset.js +28 -3
- package/dist/render/postProcessing.d.ts +6 -1
- package/dist/render/postProcessing.js +2 -0
- package/dist/runtime/commandDispatch.js +10 -3
- package/dist/runtime/commandRunner.d.ts +3 -0
- package/dist/runtime/commandRunner.js +3 -1
- package/dist/runtime/context/scene.d.ts +3 -0
- package/dist/runtime/context/scene.js +6 -0
- package/dist/runtime/gameContext.js +13 -0
- package/dist/runtime/gameContextTypes.d.ts +19 -0
- package/dist/runtime/transport.d.ts +4 -1
- package/dist/settings/graphicsProfile.d.ts +7 -0
- package/dist/settings/graphicsProfile.js +4 -0
- package/dist/settings/graphicsSettings.d.ts +9 -0
- package/dist/settings/graphicsSettings.js +29 -0
- package/dist/world/buildings.d.ts +7 -0
- package/dist/world/buildings.js +20 -0
- package/dist/world/environmentSummary.js +7 -3
- package/dist/world/features.d.ts +24 -5
- package/dist/world/features.js +2 -0
- package/dist/world/terraform.d.ts +2 -0
- package/dist/world/terraform.js +1 -0
- package/dist/world/terrain.d.ts +3 -2
- package/dist/world/terrain.js +16 -3
- package/package.json +1 -1
- package/skills/jgengine/api.md +18 -18
- package/skills/jgengine-gameplay/api.md +18 -4
- package/skills/jgengine-gameplay/capabilities.md +5 -0
- package/skills/jgengine-world/api.md +27 -14
- package/skills/jgengine-world/capabilities.md +4 -0
- package/skills/jgengine-world/reference.md +27 -8
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,116 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.24.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.24.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.21.0`; `@jgengine/github` is `0.11.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `buildGroundCoverageMask`, `createGroundEdgeFade` and `applyGroundEdgeFade` (`@jgengine/shell/terrain`) fade any ground material out at the outer edge of a merged mesh footprint, leaving seams between layers inside it crisp.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- City studio ground (roads, junctions, block land, sidewalks, medians, drives, parks, crops, curbs, markings) now fades into the terrain where the town's ground ends instead of stopping at a hard edge. `meta.edgeFade: false` on the city volume keeps the old hard edge.
|
|
42
|
+
|
|
43
|
+
## 0.23.0
|
|
44
|
+
|
|
45
|
+
### Migrate
|
|
46
|
+
|
|
47
|
+
- **Bump lockstep SDK packages to `^0.23.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.20.0`; `@jgengine/github` is `0.10.0`.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- `player_pose` agent-bridge verb (`@jgengine/shell`, `window.__jgengineAgent`, `drive --rpc '{"method":"player_pose"}'`) returns the local player's position and heading, and `debug_snapshot` carries the same `player` field, so a drive can prove movement from position deltas instead of screenshots.
|
|
52
|
+
- `sim_advance` agent-bridge verb (`@jgengine/shell`, `drive --rpc '{"method":"sim_advance","seconds":30}'`) steps the mounted sim driver — input, movement, `onTick`, behaviors, bound actions — in 0.05 s frames without rendering, so a headless drive can reach waves, shifts and timers that never arrive at software-rendered frame rates. Capped at 120 s per call.
|
|
53
|
+
- `time_scale` agent-bridge verb sets the game clock's timescale and reports `{ timescale, now }`.
|
|
54
|
+
- `GameContextObjectEntry.scenery` (`@jgengine/core`): an object kind placed once and left alone. Its runtime placements now render through the shared instanced static-object batches that editor-authored props already used.
|
|
55
|
+
- `BUILDING_STYLE_PROBABILITIES` and `resolveBuildingProbabilities` (`@jgengine/core/world/buildings`): each `BuildingStyle` now carries facade-part probabilities, so an industrial yard shows few windows and roof plant, a ruin loses awnings and clotheslines, coastal fronts gain awnings. `building({ probabilities })` overrides any part.
|
|
56
|
+
- `debug_summary` agent-bridge verb and `debug_snapshot` `detail: "summary"`: fps, frame/sim ms, draw calls, triangles, entities, warnings and top culprit in about 1 KB. `drive --rpc '{"method":"debug_snapshot"}'` prints the summary; `"detail":"full"` keeps the full snapshot.
|
|
57
|
+
- `modelLoadStarts()` (`@jgengine/shell/render/modelLoad`) counts model streaming bursts for capture quiescence checks.
|
|
58
|
+
- `jgengine` warns when it is not the CLI the caller meant: a global install run inside a jgengine checkout, a project-local `jgengine` shadowed, or a CLI built for an older SDK than the project installs. Harness runs print the CLI version and harness revision.
|
|
59
|
+
- `withGraphicsQualityOverride` and `parseGraphicsQuality` (`@jgengine/core/settings/graphicsSettings`) pin a graphics tier for one session without persisting it.
|
|
60
|
+
- The player shell reads `?quality=<tier>` and pins that tier for the page. `bun run shoot`/`drive --quality draft` use it (low tier, half size) for fast iteration shots; `--quality full` stays the default for PR evidence.
|
|
61
|
+
- `jgengine doctor` flags a silent game: one that neither declares `defineGame({ audio })` cues nor calls `ctx.game.audio.play`/`loop`. Four reference games shipped without a single sound; the fix text points at procedural `SoundDef` synth patches, which need no asset files.
|
|
62
|
+
- `drive --key mouse0:<ms>` (also `mouse1`, `mouse2`) holds that mouse button over the play canvas, matching the shell's pointer binding codes, so hold-to-fire weapons can be driven headlessly. Keyboard codes are unchanged.
|
|
63
|
+
- `drive --right-click-at x,y` sends a native right click in CSS viewport pixels, so RTS move/attack/gather orders and context actions can be driven headlessly; `--click-at` stays a left click.
|
|
64
|
+
- `ctx.scene.entity.selection` (`@jgengine/core`): the player's selected entities as a reactive `SelectionSet`. The shell pointer (`pointer.select` click / marquee) now writes it instead of private component state, so game HUDs can show the selected unit's card or a "3 units selected" summary and can set the selection themselves (select-all, control groups); the selection ring follows either way. Despawned ids drop out.
|
|
65
|
+
- `LightingConfig.iblAmbientScale` (default 0.5) and `LightingConfig.fallbackKey` (default true); `balanceLightingForIbl` in `@jgengine/core/render/lightingBalance` applies them.
|
|
66
|
+
- `DirectionalLightingConfig.shadowRadius` (PCF penumbra in texels, default 1.25).
|
|
67
|
+
- `DEFAULT_AO` in `@jgengine/core/render/postProcessing` and `mergePostProcessing` in `@jgengine/core/render/lookPreset`.
|
|
68
|
+
- Loaded GLB models get a default painted finish: flat-colour, vertex-colour and palette-atlas materials have their exported roughness/metalness/`envMapIntensity` normalized and compile with one shared object-space detail shader (mottling, wear, roughness break-up, micro-normal, cavity). Normal-mapped, cutout and transparent materials are untouched.
|
|
69
|
+
- `GraphicsProfile.surfaceDetail` (`"off" | "lite" | "full"`, `@jgengine/core/settings/graphicsProfile`): `"lite"` on low, `"full"` on medium and up.
|
|
70
|
+
- `ModelConfig.surfaceDetail: false` opts one model out; `defineGame({ surfaceDetail: false })` opts a game out.
|
|
71
|
+
- `createSurfaceRibbonMaterial`, `ribbonSurfaceFrame`, `resolvePathSurface` and `useSurfaceRibbonMaterial` (`@jgengine/shell/terrain`) shade ground ribbons with the terrain's procedural surface functions, plus wear and a noise-warped soft edge.
|
|
72
|
+
- `road({ surface })` (`@jgengine/core/world/features`) picks the carriageway surface; `false` keeps flat colour.
|
|
73
|
+
- `AuthoredScene`/`AuthoredPaths` `pathSurfaceDetail`/`surfaceDetail` props and path `meta.surface`, `meta.edgeSoftness`, `meta.wear`, `meta.surfaceDetail`.
|
|
74
|
+
- `EditableTerrain.gridBounds` (`@jgengine/core/world/terraform`) exposes the world extent of the paint/height cell grid.
|
|
75
|
+
- Painted terrain renders each layer's `surface` as its own procedural ground (grass, dirt, gravel, rock, sand, mud, snow, road) with albedo variation, roughness and micro-normals, blended along noise-warped, height-aware edges, in play and in the editor viewport. No game config or assets needed.
|
|
76
|
+
- `TerrainDetailConfig.surfaceStrength` (0..1, default 1) scales the per-surface response; `detail: false` opts a terrain out to the flat vertex-colour ground.
|
|
77
|
+
- `@jgengine/shell/terrain` exports `packTerrainPaintWeights`, `createTerrainPaintTextures`, `terrainSurfaceClass`, `createTerrainDetailMaterial` and `TerrainQualityContext`/`useTerrainQuality`.
|
|
78
|
+
- `ctx.game.notices` (`@jgengine/core/game/notices`, `createNoticeFeed`): a player-facing notice feed with info/success/warn/deny tones, stacking dedupe (repeats show ×N and refresh their lifetime), a cap, TTL, `onPush`, `retune` and `snapshot`/`restore`. Pure state on the context clock, so headless and hosted runs are unaffected.
|
|
79
|
+
- The shell mounts `NoticeStack` (`@jgengine/react/notices`) for every game: animated toned cards below top HUD bars, 14px text on `--jg-*` vars with fallbacks, no pointer capture. Configure with `notices: { placement, hidden, refusals, cues, cap, ttlMs }` or remove with `notices: false`.
|
|
80
|
+
- Refusal feedback: `validate` may return `{ reason, message }`. `reason` stays the machine code; a `message` is shown automatically as a deduped deny notice with the deny cue when the local player's command is refused, locally or from a host. `ctx.game.commands` emits a `command.refused` event for every refusal; `attachRefusalNotices` is the headless hookup.
|
|
81
|
+
- UI sound cues: `useUiCue()` / `playUiCue` (`@jgengine/react/uiCues`) play `click`, `hover`, `open`, `close`, `confirm`, `deny`, `notify`. Soft procedural defaults (`@jgengine/core/audio/uiCues`) mix on a new "ui" bus with a "UI volume" setting under master volume. A game sound named `ui.<cue>` replaces one; `audio.uiCues` overrides or silences cues (`false` for all).
|
|
82
|
+
- `notice_push` and `run_command` agent-bridge verbs for `drive --rpc`.
|
|
83
|
+
- `ui-feedback` dev probe (apps/dev) exercising refusals, the feed and cues.
|
|
84
|
+
- `HudTheme` gains a type scale with a 12px floor, spacing, motion (durations and easings), elevation (layered shadows and a lit top rim), and hover/press/focus/disabled state tokens; `hudThemeVars` emits them as `--jg-type-*`, `--jg-space-*`, `--jg-motion-*`, `--jg-ease*`, `--jg-elevation-*`, `--jg-highlight`, `--jg-hover-*`, `--jg-press-*`, `--jg-focus-ring`, and `--jg-disabled-opacity`. `@jgengine/react/hudTokens` reads them in inline styles (`hudType`, `hudSpace`, `hudTransition`, `hudElevation`, `hudFont`, `HUD_FOCUS_RING`).
|
|
85
|
+
- Engine stylesheet `@jgengine/react/styles.css` (also `ENGINE_STYLESHEET` / `useEngineStyles` in `@jgengine/react/engineStyles`): every `jg-*` keyframe the shared blocks and registry use, `[data-jg-*]` interaction states, quiet hover/press/focus defaults for plain HUD buttons, and reduced-motion handling for `prefers-reduced-motion` and `[data-reduced-motion="true"]`. The `jgengine create` template imports it.
|
|
86
|
+
- `HudThemeRoot`: the shell wraps every `GameUI` in it, applying `defaultHudTheme` and the stylesheet. `defineGame({ hudTheme })` swaps the root theme; `hudTheme: false` opts out.
|
|
87
|
+
- Primitives `HudButton`, `HudPanel` (open/close transition via `usePresence`), and `TweenNumber` / `useTweenedValue` with an up/down delta flash; `useReducedMotion` in `@jgengine/react/accessibility`.
|
|
88
|
+
- `HudInteractProvider` / `useHudInteract`, plus `cue` and `onInteract` on the primitives, windows, modals, and `ActionButton`: one seam for a UI sound system.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- Static-object batches group same-model props per 64 m cell from 3 placements (was 24 m cells and more than 8), so sparse city dressing such as palms, lamps and benches shares draws. Harbor Heat's opening drops from 5,310 to 4,650 draw calls.
|
|
93
|
+
- `building()` districts with a non-generic `style` generate a different facade mix than before (previously a style changed colours only). `generic` is unchanged.
|
|
94
|
+
- `bun run shoot/drive` and the scaffolded/bundled `shoot.mjs`/`drive.mjs` parse one shared flag table: `--click-at`, `--right-click-at`, `--width`, `--height`, `--port`, `--size`, `--settle`, `--param`, `--probe`, `--reload`, key chords and mouse holds work in both; unknown flags fail instead of being ignored or taken as a game id; dev-runner-only flags are refused by name in the portable harness.
|
|
95
|
+
- Play-mode capture readiness waits until staged `--state`/`--run` commands have ticked, their entities mounted and model streaming stopped, and never declares ready over "Loading game view…". A rejected staged command fails the capture.
|
|
96
|
+
- Play-mode captures of a game with a start menu and no `capture.play` click its start action and report what they clicked.
|
|
97
|
+
- Monorepo tests, type-checks and `bun run gen` resolve `@jgengine/*` to package source through the `paths` in `tsconfig.base.json` (`bun run gen:source-paths` keeps every workspace tsconfig in sync), so a fresh checkout or worktree is testable with no build. Published packages are unchanged: `exports` still point at `dist`, and the built output is byte-identical.
|
|
98
|
+
- The export manifest is computed from the files each package's build emits, not from `dist` on disk, so it can no longer drift with a stale or partial build. `bun run ship:preflight` now fails with the fix (`bun run gen`) when any generated artifact is stale.
|
|
99
|
+
- `bun run agent:worktree -- <name>` produces a clean, ready worktree locally and in cloud sessions: it links dependencies, `Games` and the dev runner's ignored assets from the main checkout instead of installing and building. `agent:bootstrap` only installs; pass `--build` to also build dist.
|
|
100
|
+
- The procedural IBL probe has a zenith/horizon/ground gradient and an HDR sun disc plus a broad lobe, with the horizon leaning toward the ambient colour, at intensity 0.45 (was a 2-colour gradient at 0.35), so forms get a lit and a shaded side from IBL alone.
|
|
101
|
+
- Under IBL (every look but `"flat"`), authored ambient is halved, and lighting with no directional light gets a soft shadowless key light (intensity 0.6, tinted by the ambient colour).
|
|
102
|
+
- A post chain that omits `ao` now runs `DEFAULT_AO` on medium tier and above; `ao: false` still opts out. `NEUTRAL_POST_PROCESSING` no longer sets `ao: false`.
|
|
103
|
+
- Styled look presets deep-merge their post stages under a partial game `postProcessing` instead of being dropped whole.
|
|
104
|
+
- Shadows use `PCFShadowMap` instead of `PCFSoftShadowMap`, which three r182 compiles as unfiltered basic shadows (stair-stepped edges), with a default radius of 1.25 texels on configured and cascaded lights.
|
|
105
|
+
- GTAO samples are tiered: 8 on medium, 12 on high (was 16), 16 on ultra.
|
|
106
|
+
- A model `material.rim` override now chains the material's live program cache key instead of a snapshot.
|
|
107
|
+
- Authored paths now render as dirt (or their `meta.surface`/kind) with wheel ruts and edges that fade into the terrain, instead of flat-colour ribbons with hard cut edges.
|
|
108
|
+
- `road()` carriageways, curbs, sidewalks and welded junctions, and city studio ground (streets, block land, sidewalks, drives, parks), shade with matching procedural asphalt, gravel, concrete and dirt. City volumes opt out with `meta.surfaceDetail: false`.
|
|
109
|
+
- Terrain noise and micro-normal GLSL moved to shared exports in `terrainSurfaceShader.ts` so terrain and ribbons compile the same code.
|
|
110
|
+
- A terrain without `detail` now renders with default procedural detail instead of flat vertex colour.
|
|
111
|
+
- Slope rock no longer fades with `detail.strength`; it follows `surfaceStrength`.
|
|
112
|
+
- The textured terrain path's normal map is applied in view space (it was added to the view-space normal as a world-space offset).
|
|
113
|
+
- The `low` graphics tier takes a cheap terrain surface path without micro-normals.
|
|
114
|
+
- The shell plays UI cues for every engine HUD primitive (`HudButton`, `HudPanel`, `Window`, `ModalHost`, `ActionButton`): activate → `click` (or the primitive's `cue` when it names a cue, e.g. `cue="confirm"`), button/action hover → `hover`, panel open/close → `open`/`close`. Off with `notices: false` or `notices.cues: false`; `audio.uiCues` silences single cues. `uiCueForInteraction` (`@jgengine/react/uiCues`) exposes the mapping.
|
|
115
|
+
- `CommandRejection`, `CommandValidationError`, `CommandResult`, `RunCommandResult` and `TransportRunCommandResult` carry an optional `message`; the ws hosts forward it. Results without one are unchanged.
|
|
116
|
+
- `defaultHudTheme` is the engine's default look: a system UI font stack led by Inter, layered translucent panels with a lit rim and hairline borders, and gradient slots.
|
|
117
|
+
- `Window` / `PanelHost` render on `HudPanel` with enter and exit transitions and a `HudButton` close; `ModalHost` fades its backdrop and lifts its dialog, with an exit; `ConfirmDialog` / `PauseMenu` use `HudPanel` and `HudButton`; `HudFrame` and `ActionButton` read the font, elevation, focus, and motion tokens.
|
|
118
|
+
- Text under 12px in `@jgengine/react` and `registry/jgengine` now uses the type scale's `xs` step (12px floor).
|
|
119
|
+
|
|
120
|
+
### Fixed
|
|
121
|
+
|
|
122
|
+
- Games on the `rts` and `topDown` camera rigs hear positional sound again. The spatial-audio listener sat at the camera, 35–50 m above the ground, so the default 24 m falloff silenced every ground event even at screen centre; for these rigs it now sits where the camera looks at the ground. Chase, first-person and other rigs keep the camera-anchored listener.
|
|
123
|
+
- The bundled `jgengine drive` and `jgengine shoot` harnesses let Chrome pick a free debug port instead of guessing one from the clock, so they no longer attach to another Chrome that holds the same port. An interrupted drive or shoot (SIGINT/SIGTERM) now closes its Chrome instead of leaving it running.
|
|
124
|
+
- `jgengine doctor` flags component folders reached through a `tsconfig` path alias (such as a shared `@/components/ui/*` registry) that game code imports but `src/index.css` never scans with `@source`. Their Tailwind classes were silently not generated, so those HUD blocks rendered tiny and unstyled; the fix text names the exact `@source` line to add.
|
|
125
|
+
- `jgengine doctor` now flags a 3D game that sets neither a `look` preset nor `postProcessing`; such a game renders the unstyled `"neutral"` look with no tone-map grade, AO or bloom. Its fix text no longer claims cinematic is the default. Write `look: "neutral"` when unstyled output is deliberate.
|
|
126
|
+
- `jgengine create` scaffolds name `look: "cinematic"` explicitly, so a new 3D game starts graded instead of on the unstyled default.
|
|
127
|
+
- Generated city districts (`@jgengine/shell` `CityRenderer`) fade their developed ground to transparent over its outer 9 m, so it melts into the terrain or authored paint beyond it instead of ending in a hard, stair-stepped green rim.
|
|
128
|
+
- City districts (`@jgengine/shell` `CityRenderer`) draw a crossing of gravel lanes as gravel instead of a dark asphalt slab; a crossing that touches any paved street stays asphalt.
|
|
129
|
+
- The published CLI and scaffold browser driver restore game keyboard focus before key holds and wait for a rendered frame under the held key. Menu clicks and slow headless rendering no longer silently swallow movement inputs; failed frame sampling releases held keys.
|
|
130
|
+
- After a click on a control that then unmounts (a start screen's "Play", a menu's last button), `@jgengine/shell` returns focus to the play surface instead of leaving it on `<body>`, so gameplay keys work without an extra click on the canvas.
|
|
131
|
+
- `sim_advance` (`@jgengine/shell` agent bridge) yields to the browser every ten simulated seconds, so models for entities spawned mid-advance mount as in play; a 30 s advance through Beacon Bastion's first wave previously left every raider with health bars but no model.
|
|
132
|
+
- Generated city districts no longer lay a checkerboard over their ground. The developed-ground fabric drew one flat quad per 2.5 m cell with a per-cell random roll that flipped neighbours between zone bands; it is now one shared-vertex sheet coloured from smooth noise, so yards and lots blend.
|
|
133
|
+
- Authored terrain paint (paths, clearings, painted layers) now blends smoothly between neighbouring cells in the runtime ground and the editor sculpt view, instead of snapping each vertex to its 2 m cell and drawing grid-aligned stair-step edges. Unpainted cells blend toward the caller's base ground color, so painted edges fade into the terrain palette.
|
|
134
|
+
- A terrain detail `material` no longer overrides the ground's authored roughness with its roughness map. The map now varies roughness around the authored value, so scanned ground maps (roughness near 0.45) stop giving dry terrain a wet specular sheen; games no longer need to lower `material.strength` to stay matte.
|
|
135
|
+
- A terrain without a `waterLevel` no longer paints the detail shader's beach sand band. Previously the band defaulted to water at height 0, so any flat terrain near 0 (a park, a meadow) rendered as beach; six reference games set `detail.waterLevel` far below the world only to avoid it. Set `waterLevel` on the terrain or `detail.waterLevel` to keep a shoreline.
|
|
136
|
+
- Atomic vitals bars (`HealthBar`, `ManaBar`, … in `@jgengine/react`) cap their frame at a quarter of the bar height, so thin bars (e.g. `height: "4px"`) show their fill instead of only the two 2px frame lines.
|
|
137
|
+
- Built-in world health bars (`worldHealthBars`, `@jgengine/shell`) span about 1.4 m at the entity's on-screen scale (28–112 px) instead of a fixed 112 px, so top-down and RTS cameras no longer paint bars ten times wider than the units under them.
|
|
138
|
+
|
|
29
139
|
## 0.22.0
|
|
30
140
|
|
|
31
141
|
### Migrate
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AudioBusDef, SoundDef } from "./audioFalloff.js";
|
|
2
|
+
import type { NoticeTone } from "../game/notices.js";
|
|
3
|
+
/** Interface sound cues every game gets: press, hover, panel open/close, confirm, refusal, notification. */
|
|
4
|
+
export type UiCue = "click" | "hover" | "open" | "close" | "confirm" | "deny" | "notify";
|
|
5
|
+
/** Every {@link UiCue}, in a stable order. */
|
|
6
|
+
export declare const UI_CUES: readonly UiCue[];
|
|
7
|
+
/** Bus the default UI cues mix through; the settings menu shows its volume. */
|
|
8
|
+
export declare const UI_CUE_BUS = "ui";
|
|
9
|
+
/** Catalog id a cue plays under: `ui.click`, `ui.deny`, ... A game sound with that id replaces the default. */
|
|
10
|
+
export declare function uiCueSoundId(cue: UiCue): string;
|
|
11
|
+
/** The cue a notice of each tone plays when it appears. */
|
|
12
|
+
export declare const NOTICE_TONE_CUES: Readonly<Record<NoticeTone, UiCue>>;
|
|
13
|
+
/** Short, soft procedural patches: no samples to load, quiet enough to sit under game audio. */
|
|
14
|
+
export declare const DEFAULT_UI_CUE_SOUNDS: Readonly<Record<UiCue, SoundDef>>;
|
|
15
|
+
/** Per-cue override: a replacement sound, or `false` to silence that cue. `false` for the whole map silences all. */
|
|
16
|
+
export type UiCueOverrides = false | Partial<Record<UiCue, SoundDef | false>>;
|
|
17
|
+
/**
|
|
18
|
+
* Merge the default UI cue patches into a game's sound catalog. A game sound already named `ui.<cue>` wins,
|
|
19
|
+
* then `overrides`, then the default. Adds the `ui` bus (gain 0.8) when the game did not declare one.
|
|
20
|
+
*
|
|
21
|
+
* @capability ui-sound-cues default procedural click/hover/open/close/confirm/deny/notify cues on a "ui" bus, overridable per game
|
|
22
|
+
*/
|
|
23
|
+
export declare function withUiCueSounds(sounds: Readonly<Record<string, SoundDef>> | undefined, buses: Readonly<Record<string, AudioBusDef>> | undefined, overrides?: UiCueOverrides): {
|
|
24
|
+
sounds: Record<string, SoundDef>;
|
|
25
|
+
buses: Record<string, AudioBusDef> | undefined;
|
|
26
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Every {@link UiCue}, in a stable order. */
|
|
2
|
+
export const UI_CUES = ["click", "hover", "open", "close", "confirm", "deny", "notify"];
|
|
3
|
+
/** Bus the default UI cues mix through; the settings menu shows its volume. */
|
|
4
|
+
export const UI_CUE_BUS = "ui";
|
|
5
|
+
/** Catalog id a cue plays under: `ui.click`, `ui.deny`, ... A game sound with that id replaces the default. */
|
|
6
|
+
export function uiCueSoundId(cue) {
|
|
7
|
+
return `ui.${cue}`;
|
|
8
|
+
}
|
|
9
|
+
/** The cue a notice of each tone plays when it appears. */
|
|
10
|
+
export const NOTICE_TONE_CUES = {
|
|
11
|
+
info: "notify",
|
|
12
|
+
success: "confirm",
|
|
13
|
+
warn: "notify",
|
|
14
|
+
deny: "deny",
|
|
15
|
+
};
|
|
16
|
+
function uiSound(cue, def) {
|
|
17
|
+
return { id: uiCueSoundId(cue), bus: UI_CUE_BUS, positional: false, priority: 2, ...def };
|
|
18
|
+
}
|
|
19
|
+
/** Short, soft procedural patches: no samples to load, quiet enough to sit under game audio. */
|
|
20
|
+
export const DEFAULT_UI_CUE_SOUNDS = {
|
|
21
|
+
click: uiSound("click", {
|
|
22
|
+
gain: 0.5,
|
|
23
|
+
maxVoices: 2,
|
|
24
|
+
synth: {
|
|
25
|
+
voices: [
|
|
26
|
+
{ kind: "tone", wave: "triangle", freq: 1560, slideTo: 1120, duration: 0.05, gain: 0.32, attack: 0.002 },
|
|
27
|
+
{ kind: "noise", filterType: "highpass", filterFreq: 4200, duration: 0.018, gain: 0.08 },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
hover: uiSound("hover", {
|
|
32
|
+
gain: 0.35,
|
|
33
|
+
maxVoices: 1,
|
|
34
|
+
synth: { voices: [{ kind: "tone", wave: "sine", freq: 2240, duration: 0.03, gain: 0.12, attack: 0.002 }] },
|
|
35
|
+
}),
|
|
36
|
+
open: uiSound("open", {
|
|
37
|
+
gain: 0.5,
|
|
38
|
+
maxVoices: 2,
|
|
39
|
+
synth: {
|
|
40
|
+
voices: [
|
|
41
|
+
{ kind: "tone", wave: "triangle", freq: 523, duration: 0.09, gain: 0.26 },
|
|
42
|
+
{ kind: "tone", wave: "triangle", freq: 784, duration: 0.12, gain: 0.24, delay: 0.045 },
|
|
43
|
+
{ kind: "noise", filterType: "bandpass", filterFreq: 2400, duration: 0.08, gain: 0.04 },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
close: uiSound("close", {
|
|
48
|
+
gain: 0.5,
|
|
49
|
+
maxVoices: 2,
|
|
50
|
+
synth: {
|
|
51
|
+
voices: [
|
|
52
|
+
{ kind: "tone", wave: "triangle", freq: 784, duration: 0.08, gain: 0.22 },
|
|
53
|
+
{ kind: "tone", wave: "triangle", freq: 523, duration: 0.11, gain: 0.22, delay: 0.04 },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
confirm: uiSound("confirm", {
|
|
58
|
+
gain: 0.5,
|
|
59
|
+
maxVoices: 2,
|
|
60
|
+
synth: {
|
|
61
|
+
voices: [
|
|
62
|
+
{ kind: "tone", wave: "sine", freq: 659, duration: 0.12, gain: 0.26 },
|
|
63
|
+
{ kind: "tone", wave: "sine", freq: 831, duration: 0.12, gain: 0.24, delay: 0.06 },
|
|
64
|
+
{ kind: "tone", wave: "sine", freq: 988, duration: 0.22, gain: 0.24, delay: 0.12 },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
deny: uiSound("deny", {
|
|
69
|
+
gain: 0.55,
|
|
70
|
+
maxVoices: 1,
|
|
71
|
+
synth: {
|
|
72
|
+
voices: [
|
|
73
|
+
{ kind: "tone", wave: "triangle", freq: 233, slideTo: 207, duration: 0.09, gain: 0.34, attack: 0.004 },
|
|
74
|
+
{ kind: "tone", wave: "triangle", freq: 185, slideTo: 164, duration: 0.15, gain: 0.34, delay: 0.085, attack: 0.004 },
|
|
75
|
+
{ kind: "noise", filterType: "lowpass", filterFreq: 900, duration: 0.05, gain: 0.08 },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
notify: uiSound("notify", {
|
|
80
|
+
gain: 0.45,
|
|
81
|
+
maxVoices: 1,
|
|
82
|
+
synth: {
|
|
83
|
+
voices: [
|
|
84
|
+
{ kind: "tone", wave: "sine", freq: 1319, duration: 0.32, gain: 0.22 },
|
|
85
|
+
{ kind: "tone", wave: "sine", freq: 1976, duration: 0.24, gain: 0.08 },
|
|
86
|
+
{ kind: "tone", wave: "sine", freq: 1760, duration: 0.36, gain: 0.16, delay: 0.07 },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Merge the default UI cue patches into a game's sound catalog. A game sound already named `ui.<cue>` wins,
|
|
93
|
+
* then `overrides`, then the default. Adds the `ui` bus (gain 0.8) when the game did not declare one.
|
|
94
|
+
*
|
|
95
|
+
* @capability ui-sound-cues default procedural click/hover/open/close/confirm/deny/notify cues on a "ui" bus, overridable per game
|
|
96
|
+
*/
|
|
97
|
+
export function withUiCueSounds(sounds, buses, overrides) {
|
|
98
|
+
if (overrides === false)
|
|
99
|
+
return { sounds: { ...sounds }, buses: buses === undefined ? undefined : { ...buses } };
|
|
100
|
+
const merged = {};
|
|
101
|
+
let usesUiBus = false;
|
|
102
|
+
for (const cue of UI_CUES) {
|
|
103
|
+
const id = uiCueSoundId(cue);
|
|
104
|
+
if (sounds?.[id] !== undefined)
|
|
105
|
+
continue;
|
|
106
|
+
const override = overrides?.[cue];
|
|
107
|
+
if (override === false)
|
|
108
|
+
continue;
|
|
109
|
+
const def = override ?? DEFAULT_UI_CUE_SOUNDS[cue];
|
|
110
|
+
merged[id] = { ...def, id };
|
|
111
|
+
if (def.bus === UI_CUE_BUS)
|
|
112
|
+
usesUiBus = true;
|
|
113
|
+
}
|
|
114
|
+
const nextBuses = usesUiBus && buses?.[UI_CUE_BUS] === undefined
|
|
115
|
+
? { ...buses, [UI_CUE_BUS]: { id: UI_CUE_BUS, gain: 0.8 } }
|
|
116
|
+
: buses;
|
|
117
|
+
return { sounds: { ...merged, ...sounds }, buses: nextBuses };
|
|
118
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
/** Why a command refused: `reason` is a stable machine code; `message`, when set, is shown to the player. */
|
|
1
2
|
export interface CommandRejection {
|
|
2
3
|
reason: string;
|
|
4
|
+
/** Player-facing text ("Needs 40 more gold"). The shell shows it in the notice feed with the deny cue. */
|
|
5
|
+
message?: string;
|
|
3
6
|
}
|
|
4
7
|
export type CommandDecodeResult<TInput> = {
|
|
5
8
|
ok: true;
|
|
@@ -22,6 +25,7 @@ export type CommandResult<TState> = {
|
|
|
22
25
|
} | {
|
|
23
26
|
status: "rejected";
|
|
24
27
|
reason: string;
|
|
28
|
+
message?: string;
|
|
25
29
|
} | {
|
|
26
30
|
status: "unknown-command";
|
|
27
31
|
};
|
|
@@ -25,8 +25,11 @@ export function createCommandRegistry() {
|
|
|
25
25
|
decoded = result.value;
|
|
26
26
|
}
|
|
27
27
|
const rejection = definition.validate?.(state, decoded) ?? null;
|
|
28
|
-
if (rejection)
|
|
29
|
-
return
|
|
28
|
+
if (rejection) {
|
|
29
|
+
return rejection.message === undefined
|
|
30
|
+
? { status: "rejected", reason: rejection.reason }
|
|
31
|
+
: { status: "rejected", reason: rejection.reason, message: rejection.message };
|
|
32
|
+
}
|
|
30
33
|
const next = definition.apply(state, decoded);
|
|
31
34
|
return { status: "applied", state: next === undefined ? state : next };
|
|
32
35
|
},
|
package/dist/game/events.d.ts
CHANGED
|
@@ -248,6 +248,17 @@ export interface AudioLoopSetEvent {
|
|
|
248
248
|
/** Highpass cutoff in Hz. */
|
|
249
249
|
highpass?: number;
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* A command run through `ctx.game.commands` (or a bound host) was refused. `reason` is the machine code;
|
|
253
|
+
* `message` is the player-facing text the command's `validate` returned, when it gave one.
|
|
254
|
+
*/
|
|
255
|
+
export interface CommandRefusedEvent {
|
|
256
|
+
command: string;
|
|
257
|
+
reason: string;
|
|
258
|
+
message?: string;
|
|
259
|
+
/** `runAs` actor, or null for a plain `run`. */
|
|
260
|
+
actor: string | null;
|
|
261
|
+
}
|
|
251
262
|
/** Stop and dispose the retained loop `id`; an unknown `id` is ignored (#1051). */
|
|
252
263
|
export interface AudioLoopStopEvent {
|
|
253
264
|
id: string;
|
|
@@ -287,6 +298,7 @@ export interface GameEventMap {
|
|
|
287
298
|
"audio.loopStop": AudioLoopStopEvent;
|
|
288
299
|
"entity.animation": EntityAnimationEvent;
|
|
289
300
|
"animation.event": AnimationClipEvent;
|
|
301
|
+
"command.refused": CommandRefusedEvent;
|
|
290
302
|
}
|
|
291
303
|
export type GameEventHandler<TPayload> = (payload: TPayload) => void;
|
|
292
304
|
export interface GameEvents<TMap extends GameEventMap = GameEventMap> {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { GameEvents } from "./events.js";
|
|
2
|
+
/** How a notice reads: neutral info, a win, a caution, or a refused action. */
|
|
3
|
+
export type NoticeTone = "info" | "success" | "warn" | "deny";
|
|
4
|
+
/** What a caller pushes: plain text, or text with a tone, a dedupe key and a lifetime. */
|
|
5
|
+
export interface NoticeInput {
|
|
6
|
+
text: string;
|
|
7
|
+
/** Default "info". */
|
|
8
|
+
tone?: NoticeTone;
|
|
9
|
+
/** Live notices with the same key stack into one (count +1, lifetime refreshed). Default `${tone}:${text}`. */
|
|
10
|
+
key?: string;
|
|
11
|
+
/** Lifetime in ms. Defaults to the feed's `ttlMs`, or `denyTtlMs` for deny. */
|
|
12
|
+
ttlMs?: number;
|
|
13
|
+
}
|
|
14
|
+
/** A live notice as the HUD renders it. */
|
|
15
|
+
export interface Notice {
|
|
16
|
+
id: string;
|
|
17
|
+
text: string;
|
|
18
|
+
tone: NoticeTone;
|
|
19
|
+
key: string;
|
|
20
|
+
/** How many pushes stacked into this notice; render "×N" when above 1. */
|
|
21
|
+
count: number;
|
|
22
|
+
/** Clock time (ms) of the latest push. */
|
|
23
|
+
at: number;
|
|
24
|
+
expiresAt: number;
|
|
25
|
+
}
|
|
26
|
+
/** Plain JSON state of a {@link NoticeFeed}. */
|
|
27
|
+
export interface NoticeFeedState {
|
|
28
|
+
notices: Notice[];
|
|
29
|
+
counter: number;
|
|
30
|
+
}
|
|
31
|
+
/** Tunables for {@link createNoticeFeed}; all retunable at runtime. */
|
|
32
|
+
export interface NoticeFeedConfig {
|
|
33
|
+
/** Maximum live notices; the oldest falls off. Default 4. */
|
|
34
|
+
cap?: number;
|
|
35
|
+
/** Default lifetime in ms. Default 3200. */
|
|
36
|
+
ttlMs?: number;
|
|
37
|
+
/** Lifetime for deny notices, which are short reactions. Default 2400. */
|
|
38
|
+
denyTtlMs?: number;
|
|
39
|
+
}
|
|
40
|
+
/** A capped, self-expiring, deduping notice list every game reaches through `ctx.game.notices`. */
|
|
41
|
+
export interface NoticeFeed {
|
|
42
|
+
/** Raise a notice (or stack it onto a live one with the same key); returns the live entry. */
|
|
43
|
+
push(input: NoticeInput | string): Notice;
|
|
44
|
+
/** Remove one notice early. */
|
|
45
|
+
dismiss(id: string): void;
|
|
46
|
+
/** Drop expired notices as of the feed clock. */
|
|
47
|
+
prune(): void;
|
|
48
|
+
/** Live notices, oldest first. Stable reference until the next change. */
|
|
49
|
+
list(): readonly Notice[];
|
|
50
|
+
clear(): void;
|
|
51
|
+
/** Called after every change. */
|
|
52
|
+
subscribe(listener: () => void): () => void;
|
|
53
|
+
/** Called once per push; `stacked` is true when it merged into a live notice. */
|
|
54
|
+
onPush(listener: (notice: Notice, stacked: boolean) => void): () => void;
|
|
55
|
+
retune(config: NoticeFeedConfig): void;
|
|
56
|
+
snapshot(): NoticeFeedState;
|
|
57
|
+
restore(next: NoticeFeedState): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Player-facing notice feed: info/success/warn/deny messages with stacking dedupe and TTL. Pure state with
|
|
61
|
+
* an injected clock, so it runs headless; the shell renders it and plays a UI cue per push.
|
|
62
|
+
*
|
|
63
|
+
* @capability notice-feed push toned, deduped, self-expiring player notices from game logic via ctx.game.notices
|
|
64
|
+
*/
|
|
65
|
+
export declare function createNoticeFeed(config?: NoticeFeedConfig & {
|
|
66
|
+
now?: () => number;
|
|
67
|
+
}): NoticeFeed;
|
|
68
|
+
/** Options for {@link attachRefusalNotices}. */
|
|
69
|
+
export interface RefusalNoticeOptions {
|
|
70
|
+
/** Which actors' refusals to show. Default: commands run without an actor or by `localUserId`. */
|
|
71
|
+
accept?: (refusal: {
|
|
72
|
+
command: string;
|
|
73
|
+
reason: string;
|
|
74
|
+
actor: string | null;
|
|
75
|
+
}) => boolean;
|
|
76
|
+
/** The local player's id, used by the default `accept`. */
|
|
77
|
+
localUserId?: () => string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Show every player-facing command refusal (`validate` returned `{ reason, message }`) as a deny notice.
|
|
81
|
+
* Refusals without a `message` stay silent: `reason` is a machine string. Returns the detach function.
|
|
82
|
+
*
|
|
83
|
+
* @capability refusal-feedback surface a refused command's player-facing message in the notice feed automatically
|
|
84
|
+
*/
|
|
85
|
+
export declare function attachRefusalNotices(events: Pick<GameEvents, "on">, notices: Pick<NoticeFeed, "push">, options?: RefusalNoticeOptions): () => void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const DEFAULT_CAP = 4;
|
|
2
|
+
const DEFAULT_TTL_MS = 3200;
|
|
3
|
+
const DEFAULT_DENY_TTL_MS = 2400;
|
|
4
|
+
/**
|
|
5
|
+
* Player-facing notice feed: info/success/warn/deny messages with stacking dedupe and TTL. Pure state with
|
|
6
|
+
* an injected clock, so it runs headless; the shell renders it and plays a UI cue per push.
|
|
7
|
+
*
|
|
8
|
+
* @capability notice-feed push toned, deduped, self-expiring player notices from game logic via ctx.game.notices
|
|
9
|
+
*/
|
|
10
|
+
export function createNoticeFeed(config = {}) {
|
|
11
|
+
const now = config.now ?? Date.now;
|
|
12
|
+
let cap = config.cap ?? DEFAULT_CAP;
|
|
13
|
+
let ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
|
|
14
|
+
let denyTtlMs = config.denyTtlMs ?? DEFAULT_DENY_TTL_MS;
|
|
15
|
+
let notices = [];
|
|
16
|
+
let counter = 0;
|
|
17
|
+
const listeners = new Set();
|
|
18
|
+
const pushListeners = new Set();
|
|
19
|
+
function set(next) {
|
|
20
|
+
if (next === notices)
|
|
21
|
+
return;
|
|
22
|
+
notices = next;
|
|
23
|
+
for (const listener of [...listeners])
|
|
24
|
+
listener();
|
|
25
|
+
}
|
|
26
|
+
function prunedAt(time) {
|
|
27
|
+
return notices.every((notice) => notice.expiresAt > time) ? notices : notices.filter((notice) => notice.expiresAt > time);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
push(raw) {
|
|
31
|
+
const input = typeof raw === "string" ? { text: raw } : raw;
|
|
32
|
+
const tone = input.tone ?? "info";
|
|
33
|
+
const key = input.key ?? `${tone}:${input.text}`;
|
|
34
|
+
const time = now();
|
|
35
|
+
const life = input.ttlMs ?? (tone === "deny" ? denyTtlMs : ttlMs);
|
|
36
|
+
const live = prunedAt(time);
|
|
37
|
+
const index = live.findIndex((notice) => notice.key === key);
|
|
38
|
+
let notice;
|
|
39
|
+
if (index >= 0) {
|
|
40
|
+
const previous = live[index];
|
|
41
|
+
notice = { ...previous, text: input.text, tone, count: previous.count + 1, at: time, expiresAt: time + life };
|
|
42
|
+
const next = [...live];
|
|
43
|
+
next[index] = notice;
|
|
44
|
+
set(next);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
notice = { id: `notice-${counter}`, text: input.text, tone, key, count: 1, at: time, expiresAt: time + life };
|
|
48
|
+
counter += 1;
|
|
49
|
+
const next = [...live, notice];
|
|
50
|
+
set(cap > 0 && next.length > cap ? next.slice(next.length - cap) : next);
|
|
51
|
+
}
|
|
52
|
+
for (const listener of [...pushListeners])
|
|
53
|
+
listener(notice, index >= 0);
|
|
54
|
+
return notice;
|
|
55
|
+
},
|
|
56
|
+
dismiss(id) {
|
|
57
|
+
if (notices.some((notice) => notice.id === id))
|
|
58
|
+
set(notices.filter((notice) => notice.id !== id));
|
|
59
|
+
},
|
|
60
|
+
prune() {
|
|
61
|
+
set(prunedAt(now()));
|
|
62
|
+
},
|
|
63
|
+
list: () => notices,
|
|
64
|
+
clear() {
|
|
65
|
+
if (notices.length > 0)
|
|
66
|
+
set([]);
|
|
67
|
+
},
|
|
68
|
+
subscribe(listener) {
|
|
69
|
+
listeners.add(listener);
|
|
70
|
+
return () => listeners.delete(listener);
|
|
71
|
+
},
|
|
72
|
+
onPush(listener) {
|
|
73
|
+
pushListeners.add(listener);
|
|
74
|
+
return () => pushListeners.delete(listener);
|
|
75
|
+
},
|
|
76
|
+
retune(next) {
|
|
77
|
+
cap = next.cap ?? cap;
|
|
78
|
+
ttlMs = next.ttlMs ?? ttlMs;
|
|
79
|
+
denyTtlMs = next.denyTtlMs ?? denyTtlMs;
|
|
80
|
+
if (cap > 0 && notices.length > cap)
|
|
81
|
+
set(notices.slice(notices.length - cap));
|
|
82
|
+
},
|
|
83
|
+
snapshot: () => ({ notices: notices.map((notice) => ({ ...notice })), counter }),
|
|
84
|
+
restore(next) {
|
|
85
|
+
counter = next.counter;
|
|
86
|
+
set(next.notices.map((notice) => ({ ...notice })));
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Show every player-facing command refusal (`validate` returned `{ reason, message }`) as a deny notice.
|
|
92
|
+
* Refusals without a `message` stay silent: `reason` is a machine string. Returns the detach function.
|
|
93
|
+
*
|
|
94
|
+
* @capability refusal-feedback surface a refused command's player-facing message in the notice feed automatically
|
|
95
|
+
*/
|
|
96
|
+
export function attachRefusalNotices(events, notices, options = {}) {
|
|
97
|
+
const accept = options.accept ?? ((refusal) => refusal.actor === null || options.localUserId === undefined || refusal.actor === options.localUserId());
|
|
98
|
+
return events.on("command.refused", (refusal) => {
|
|
99
|
+
if (refusal.message === undefined || refusal.message === "")
|
|
100
|
+
return;
|
|
101
|
+
if (!accept(refusal))
|
|
102
|
+
return;
|
|
103
|
+
notices.push({ text: refusal.message, tone: "deny", key: `refusal:${refusal.command}:${refusal.message}` });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AnimGraph } from "../anim/animGraph.js";
|
|
2
2
|
import type { AudioBusDef, SoundDef } from "../audio/audioFalloff.js";
|
|
3
3
|
import type { MusicTheme } from "../audio/music.js";
|
|
4
|
+
import type { UiCueOverrides } from "../audio/uiCues.js";
|
|
5
|
+
import type { NoticeFeedConfig } from "./notices.js";
|
|
4
6
|
import type { MaterialAsset, MaterialAssignment, MaterialSurfaceParameters, MaterialTextureRole } from "../material/materialAsset.js";
|
|
5
7
|
import type { EditorCatalogDefinition, EditorDocument } from "../editor/types.js";
|
|
6
8
|
import type { PostProcessingConfig } from "../render/postProcessing.js";
|
|
@@ -29,6 +31,17 @@ import type { GameCameraConfig } from "./cameraConfig.js";
|
|
|
29
31
|
import type { ViewportsConfig } from "./viewports.js";
|
|
30
32
|
import type { LootFilterRule } from "./lootFilter.js";
|
|
31
33
|
import type { RarityStyle } from "./worldItem.js";
|
|
34
|
+
/** How the shell presents `ctx.game.notices`; every field is optional. */
|
|
35
|
+
export interface NoticePresentationConfig extends NoticeFeedConfig {
|
|
36
|
+
/** Screen band for the feed. "top" sits below top-edge HUD bars, "center" over the play area, "bottom" above action bars. Default "top". */
|
|
37
|
+
placement?: "top" | "center" | "bottom";
|
|
38
|
+
/** Keep the feed and cues but render nothing (the game draws `ctx.game.notices` itself). Default false. */
|
|
39
|
+
hidden?: boolean;
|
|
40
|
+
/** Show refused commands' `message` as deny notices. Default true. */
|
|
41
|
+
refusals?: boolean;
|
|
42
|
+
/** Play a UI cue per new notice (deny → deny, success → confirm, else notify). Default true. */
|
|
43
|
+
cues?: boolean;
|
|
44
|
+
}
|
|
32
45
|
export type { CameraFollowState, CameraKeyframe, CameraProjection, CameraRigKind, CameraWeaponView, CameraShakeConfig, ChaseCameraConfig, ChaseView, CinematicCameraConfig, FirstPersonCameraConfig, GameCameraConfig, InspectionCameraConfig, InspectionZoomAnchor, LockOnCameraConfig, ObserverCameraConfig, PlayerFovConfig, RtsCameraConfig, ShoulderCameraConfig, SideScrollCameraConfig, TopDownCameraConfig, TurntableCameraConfig, } from "./cameraConfig.js";
|
|
33
46
|
export { CAMERA_FRUSTUM_DEFAULTS } from "./cameraConfig.js";
|
|
34
47
|
/**
|
|
@@ -229,6 +242,8 @@ export interface ModelConfig {
|
|
|
229
242
|
collisionMesh?: CollisionMeshData;
|
|
230
243
|
/** Per-entity PBR tint/finish override (#151.3); cloned onto each `MeshStandardMaterial` in the model so shared GLTF caches stay untouched. */
|
|
231
244
|
material?: ModelMaterialOverride;
|
|
245
|
+
/** `false` keeps this model's exported material params and skips the engine's default painted surface detail. Default true. */
|
|
246
|
+
surfaceDetail?: boolean;
|
|
232
247
|
/** Reusable authored surfaces available to this model's named slot assignments. */
|
|
233
248
|
materialAssets?: readonly MaterialAsset[];
|
|
234
249
|
/** Sparse per-slot assignments; unselected imported materials keep their original appearance. */
|
|
@@ -310,6 +325,8 @@ export interface DirectionalLightingConfig {
|
|
|
310
325
|
shadowBias?: number;
|
|
311
326
|
/** Shadow normal bias to fight peter-panning. Default 0.02. */
|
|
312
327
|
shadowNormalBias?: number;
|
|
328
|
+
/** PCF penumbra width in shadow-map texels; 0 is a hard edge. Default 1.25. */
|
|
329
|
+
shadowRadius?: number;
|
|
313
330
|
}
|
|
314
331
|
export interface HemisphereLightingConfig {
|
|
315
332
|
skyColor?: string;
|
|
@@ -340,6 +357,16 @@ export interface LightingConfig {
|
|
|
340
357
|
/** Maximum dynamic point + spot lights mounted by the shell. Default 8. */
|
|
341
358
|
maxDynamicLights?: number;
|
|
342
359
|
hemisphere?: HemisphereLightingConfig;
|
|
360
|
+
/**
|
|
361
|
+
* Multiplier on `ambient` intensity while image-based light is on (every look but `"flat"`), so the
|
|
362
|
+
* shaped sky light carries the fill instead of a flat wash. Default 0.5; 1 keeps the authored value.
|
|
363
|
+
*/
|
|
364
|
+
iblAmbientScale?: number;
|
|
365
|
+
/**
|
|
366
|
+
* With no `directional` entry, the shell adds a soft shadowless key light so forms still get a lit
|
|
367
|
+
* and a shaded side. Default true; `false` keeps the scene lit by ambient/hemisphere/IBL alone.
|
|
368
|
+
*/
|
|
369
|
+
fallbackKey?: boolean;
|
|
343
370
|
}
|
|
344
371
|
export interface BackdropFogConfig {
|
|
345
372
|
color?: string;
|
|
@@ -610,6 +637,8 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
|
|
|
610
637
|
shadows?: boolean;
|
|
611
638
|
/** Per-quality renderer budgets; omitted tiers use {@link DEFAULT_GRAPHICS_PROFILES}. */
|
|
612
639
|
graphics?: Partial<Record<"low" | "medium" | "high", Partial<GraphicsProfile>>>;
|
|
640
|
+
/** Default painted finish on loaded models (normalized glTF params plus procedural mottling, wear and micro-normal, scaled by the graphics tier's `surfaceDetail`). Default true; `false` renders every model with its exported material. */
|
|
641
|
+
surfaceDetail?: boolean;
|
|
613
642
|
/** Pointer-driven input: click-to-move, box-select, right-click verbs, cursor aim (#22/#30/#31). */
|
|
614
643
|
pointer?: PointerConfig;
|
|
615
644
|
/** Touch controls on coarse-pointer devices. Unset derives a scheme from `input` (virtual joystick for movement actions, on-screen buttons for the rest); a config refines it with gestures and curated buttons; `false` opts out. */
|
|
@@ -679,6 +708,8 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
|
|
|
679
708
|
buses?: Record<string, AudioBusDef>;
|
|
680
709
|
music?: Record<string, MusicTheme>;
|
|
681
710
|
musicBus?: string;
|
|
711
|
+
/** Default UI cues (`ui.click`, `ui.deny`, ...) on a "ui" bus. A sound named `ui.<cue>` in `sounds` replaces one; a map entry replaces or (`false`) silences one; `false` silences all. */
|
|
712
|
+
uiCues?: UiCueOverrides;
|
|
682
713
|
};
|
|
683
714
|
/** Continuous positional emitter keyed by entity kind name: while a matching entity exists, the shell plays and repositions the mapped `audio.sounds` id (looping engine hum, campfire crackle, footstep loop) with listener-distance falloff. */
|
|
684
715
|
entitySounds?: Record<string, string>;
|
|
@@ -702,6 +733,12 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
|
|
|
702
733
|
postProcessing?: PostProcessingConfig;
|
|
703
734
|
/** F2+D debug overlay (frame/sim timing, logs, backend latency, keybinds, live tunables). On for every game by default; `false` disables the toggle. */
|
|
704
735
|
devtools?: boolean;
|
|
736
|
+
/**
|
|
737
|
+
* Shell-mounted notice feed for `ctx.game.notices`: toned, stacked, self-expiring messages with a UI cue
|
|
738
|
+
* per push, plus automatic deny notices for refused commands that return a `message`. On by default;
|
|
739
|
+
* `false` removes the renderer, the cues and the refusal hookup (the feed itself stays on ctx).
|
|
740
|
+
*/
|
|
741
|
+
notices?: NoticePresentationConfig | false;
|
|
705
742
|
/** Player settings menu. Auto-mounted for every game (Sound / Graphics / Gameplay / Controls); unset uses the defaults, `false` opts out. Add game-specific rows via `extra`, switch overlay/full-page via `mode`, or swap the gear for compact on-screen buttons via `surface: "quick"`. */
|
|
706
743
|
settings?: GameSettingsConfig | false;
|
|
707
744
|
/** Automatic camera frustum + distance culling and asset streaming. On by default with conservative margins; unset uses engine defaults, `{ enabled: false }` opts out. Override bounds/distance/pins per entity kind or object catalog id. */
|
package/dist/meta/changelog.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.24.0";
|
|
3
3
|
/** One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md). */
|
|
4
4
|
export interface ChangelogEntry {
|
|
5
5
|
migrate: readonly string[];
|