@jgengine/core 0.31.0 → 0.32.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 CHANGED
@@ -26,6 +26,25 @@ 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.32.0
30
+
31
+ ### Migrate
32
+
33
+ - **Bump lockstep SDK packages to `^0.32.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.29.0`; `@jgengine/github` is `0.19.0`.
34
+
35
+ ### Added
36
+
37
+ - Audio mixing grew a mute that composes with gain: `AudioEngine.setBusMute` / `setMasterMute` keep the player's volume while silencing a bus, `masterLevel()` / `busLevel()` report the composed value, and Settings carries a mute row per bus plus "Mute all" (`sound.muted`, `sound.bus.<id>.muted`). `AudioEngine.play` and `playOneShot` hold cues in a bounded 16-slot queue while the browser keeps the context suspended and play them on the first gesture instead of dropping them; `installGestureResume` wires that gesture for both presentation paths.
38
+ - `AudioEngine.measure()` reads master RMS and peak from a lazily built analyser, and `installAudioProbe` publishes `window.__jgengineAudio` so a headless capture can prove sound with a measured level rather than a screenshot.
39
+ - `@jgengine/core/audio/feedbackCues` ships seven procedural reactions - `hit`, `hurt`, `kill`, `pickup`, `objective`, `wave`, `deny` - on an `sfx` bus, merged into every game's catalog as `feel.<cue>`. A game sound with that id replaces one; `audio.feedbackCues` replaces or silences them.
40
+ - `@jgengine/shell/audio/eventFeedback` maps the events the engine already announces to those reactions: `combat.damage` (local player reads as hurt, anything else as a hit), `entity.died`, `worldItem.picked_up`, `quest.completed`, `stat.levelUp` and `command.refused`. Each row carries a sound, a gamepad pulse, camera trauma and a caption; the mapping and reactions are data (`DEFAULT_FEEDBACK_BINDINGS`, `DEFAULT_FEEDBACK_REACTIONS`) a game can replace, cooldowns fold a burst into one impact, reduced motion keeps sound and rumble but drops the camera kick, and the resolved reaction is pooled so handling an event allocates nothing per frame. `attachWaveFeedback(runner, ctx.game.feedback)` announces each new wave.
41
+ - `ctx.game.feedback.cue(cue, caption?)` announces a moment no engine event covers and emits `feedback.cue`.
42
+ - `@jgengine/shell/audio/captions` and `CaptionOverlay` render a bounded, deduping, self-expiring caption line under the accessibility captions setting; `FeedbackHost` mounts the whole layer - feedback mapping, gesture resume, audio probe and captions - for both the 3D and HUD presentation paths.
43
+
44
+ ### Changed
45
+
46
+ - The CI gate runs nine test shards instead of ten, and a `types` job whose stages overlap, so a docs-only pull request fits the account's 20-job runner cap with room for another run beside it. `scripts/test-shard.ts` owns which files each shard runs and what it prepares first; `bun scripts/type-shard.ts` runs the `check-types` plan two stages at a time and then the tsc fan-out alone.
47
+
29
48
  ## 0.31.0
30
49
 
31
50
  ### Migrate
@@ -0,0 +1,30 @@
1
+ import type { AudioBusDef, SoundDef } from "./audioFalloff.js";
2
+ /**
3
+ * A gameplay moment the engine reacts to: taking or dealing a hit, a kill, a pickup, an objective,
4
+ * a wave starting, or a refused action. The game names the moment; the engine owns the reaction.
5
+ */
6
+ export type FeedbackCue = "hit" | "hurt" | "kill" | "pickup" | "objective" | "wave" | "deny";
7
+ /** Every {@link FeedbackCue}, in a stable order. */
8
+ export declare const FEEDBACK_CUES: readonly FeedbackCue[];
9
+ /** Bus the default feedback cues mix through; the settings menu shows its volume and mute. */
10
+ export declare const FEEDBACK_CUE_BUS = "sfx";
11
+ /** Catalog id a cue plays under: `feel.hit`, `feel.kill`, ... A game sound with that id replaces the default. */
12
+ export declare function feedbackCueSoundId(cue: FeedbackCue): string;
13
+ /**
14
+ * Short procedural reactions - no samples to load, so every game has impact feedback without
15
+ * authoring audio. Loud enough to read, quiet enough to sit under a weapon's own sound.
16
+ */
17
+ export declare const DEFAULT_FEEDBACK_CUE_SOUNDS: Readonly<Record<FeedbackCue, SoundDef>>;
18
+ /** Per-cue override: a replacement sound, or `false` to silence that cue. `false` for the whole map silences all. */
19
+ export type FeedbackCueOverrides = false | Partial<Record<FeedbackCue, SoundDef | false>>;
20
+ /**
21
+ * Merge the default feedback reactions into a game's sound catalog. A game sound already named
22
+ * `feel.<cue>` wins, then `overrides`, then the default. Adds the `sfx` bus (gain 0.9) when the game
23
+ * did not declare one, so the impact layer has its own volume and mute row.
24
+ *
25
+ * @capability feedback-cue-sounds default procedural hit/hurt/kill/pickup/objective/wave/deny reactions on an "sfx" bus, overridable per game
26
+ */
27
+ export declare function withFeedbackCueSounds(sounds: Readonly<Record<string, SoundDef>> | undefined, buses: Readonly<Record<string, AudioBusDef>> | undefined, overrides?: FeedbackCueOverrides): {
28
+ sounds: Record<string, SoundDef>;
29
+ buses: Record<string, AudioBusDef> | undefined;
30
+ };
@@ -0,0 +1,119 @@
1
+ /** Every {@link FeedbackCue}, in a stable order. */
2
+ export const FEEDBACK_CUES = ["hit", "hurt", "kill", "pickup", "objective", "wave", "deny"];
3
+ /** Bus the default feedback cues mix through; the settings menu shows its volume and mute. */
4
+ export const FEEDBACK_CUE_BUS = "sfx";
5
+ /** Catalog id a cue plays under: `feel.hit`, `feel.kill`, ... A game sound with that id replaces the default. */
6
+ export function feedbackCueSoundId(cue) {
7
+ return `feel.${cue}`;
8
+ }
9
+ function feelSound(cue, def) {
10
+ return { id: feedbackCueSoundId(cue), bus: FEEDBACK_CUE_BUS, positional: false, priority: 5, ...def };
11
+ }
12
+ /**
13
+ * Short procedural reactions - no samples to load, so every game has impact feedback without
14
+ * authoring audio. Loud enough to read, quiet enough to sit under a weapon's own sound.
15
+ */
16
+ export const DEFAULT_FEEDBACK_CUE_SOUNDS = {
17
+ hit: feelSound("hit", {
18
+ gain: 0.55,
19
+ maxVoices: 4,
20
+ synth: {
21
+ voices: [
22
+ { kind: "noise", filterType: "bandpass", filterFreq: 1800, duration: 0.05, gain: 0.3 },
23
+ { kind: "tone", wave: "triangle", freq: 210, slideTo: 150, duration: 0.07, gain: 0.24, attack: 0.002 },
24
+ ],
25
+ },
26
+ }),
27
+ hurt: feelSound("hurt", {
28
+ gain: 0.65,
29
+ maxVoices: 2,
30
+ synth: {
31
+ voices: [
32
+ { kind: "tone", wave: "triangle", freq: 165, slideTo: 110, duration: 0.16, gain: 0.32, attack: 0.003 },
33
+ { kind: "noise", filterType: "lowpass", filterFreq: 900, duration: 0.1, gain: 0.14 },
34
+ ],
35
+ },
36
+ }),
37
+ kill: feelSound("kill", {
38
+ gain: 0.6,
39
+ maxVoices: 2,
40
+ synth: {
41
+ voices: [
42
+ { kind: "noise", filterType: "lowpass", filterFreq: 1400, duration: 0.12, gain: 0.24 },
43
+ { kind: "tone", wave: "sawtooth", freq: 330, slideTo: 220, duration: 0.14, gain: 0.18 },
44
+ { kind: "tone", wave: "sine", freq: 110, slideTo: 78, duration: 0.2, gain: 0.22, delay: 0.03 },
45
+ ],
46
+ },
47
+ }),
48
+ pickup: feelSound("pickup", {
49
+ gain: 0.5,
50
+ maxVoices: 3,
51
+ synth: {
52
+ voices: [
53
+ { kind: "tone", wave: "sine", freq: 880, duration: 0.09, gain: 0.2, attack: 0.003 },
54
+ { kind: "tone", wave: "sine", freq: 1320, duration: 0.11, gain: 0.16, delay: 0.05, attack: 0.003 },
55
+ ],
56
+ },
57
+ }),
58
+ objective: feelSound("objective", {
59
+ gain: 0.55,
60
+ maxVoices: 1,
61
+ synth: {
62
+ voices: [
63
+ { kind: "tone", wave: "triangle", freq: 523, duration: 0.14, gain: 0.22 },
64
+ { kind: "tone", wave: "triangle", freq: 659, duration: 0.14, gain: 0.2, delay: 0.08 },
65
+ { kind: "tone", wave: "triangle", freq: 784, duration: 0.26, gain: 0.2, delay: 0.16 },
66
+ ],
67
+ },
68
+ }),
69
+ wave: feelSound("wave", {
70
+ gain: 0.6,
71
+ maxVoices: 1,
72
+ synth: {
73
+ voices: [
74
+ { kind: "tone", wave: "sawtooth", freq: 98, duration: 0.5, gain: 0.16, attack: 0.04, sustain: 0.24 },
75
+ { kind: "tone", wave: "sawtooth", freq: 147, duration: 0.5, gain: 0.12, attack: 0.05, sustain: 0.24 },
76
+ { kind: "noise", filterType: "lowpass", filterFreq: 600, duration: 0.45, gain: 0.12, decay: 1 },
77
+ ],
78
+ },
79
+ }),
80
+ deny: feelSound("deny", {
81
+ gain: 0.5,
82
+ maxVoices: 1,
83
+ synth: {
84
+ voices: [
85
+ { kind: "tone", wave: "square", freq: 196, slideTo: 170, duration: 0.07, gain: 0.16, attack: 0.002 },
86
+ { kind: "tone", wave: "square", freq: 155, slideTo: 130, duration: 0.1, gain: 0.16, delay: 0.07, attack: 0.002 },
87
+ ],
88
+ },
89
+ }),
90
+ };
91
+ /**
92
+ * Merge the default feedback reactions into a game's sound catalog. A game sound already named
93
+ * `feel.<cue>` wins, then `overrides`, then the default. Adds the `sfx` bus (gain 0.9) when the game
94
+ * did not declare one, so the impact layer has its own volume and mute row.
95
+ *
96
+ * @capability feedback-cue-sounds default procedural hit/hurt/kill/pickup/objective/wave/deny reactions on an "sfx" bus, overridable per game
97
+ */
98
+ export function withFeedbackCueSounds(sounds, buses, overrides) {
99
+ if (overrides === false)
100
+ return { sounds: { ...sounds }, buses: buses === undefined ? undefined : { ...buses } };
101
+ const merged = {};
102
+ let usesBus = false;
103
+ for (const cue of FEEDBACK_CUES) {
104
+ const id = feedbackCueSoundId(cue);
105
+ if (sounds?.[id] !== undefined)
106
+ continue;
107
+ const override = overrides?.[cue];
108
+ if (override === false)
109
+ continue;
110
+ const def = override ?? DEFAULT_FEEDBACK_CUE_SOUNDS[cue];
111
+ merged[id] = { ...def, id };
112
+ if (def.bus === FEEDBACK_CUE_BUS)
113
+ usesBus = true;
114
+ }
115
+ const nextBuses = usesBus && buses?.[FEEDBACK_CUE_BUS] === undefined
116
+ ? { ...buses, [FEEDBACK_CUE_BUS]: { id: FEEDBACK_CUE_BUS, gain: 0.9 } }
117
+ : buses;
118
+ return { sounds: { ...merged, ...sounds }, buses: nextBuses };
119
+ }
@@ -2,6 +2,7 @@ import type { EffectResult } from "../combat/effects.js";
2
2
  import type { DeathReason } from "../combat/deathReason.js";
3
3
  import type { TelegraphShape } from "../combat/telegraph.js";
4
4
  import type { CameraShake } from "../combat/hitReaction.js";
5
+ import type { FeedbackCue } from "../audio/feedbackCues.js";
5
6
  import type { CombatVfxInstanceEvent } from "./vfxInstance.js";
6
7
  export type { DeathReason };
7
8
  export interface EntityDiedEvent {
@@ -242,6 +243,15 @@ export interface AudioPlayEvent {
242
243
  }
243
244
  /** Request that the shell's audio engine resume its (browser-gesture-suspended) context; carries no payload. */
244
245
  export type AudioResumeEvent = Record<string, never>;
246
+ /**
247
+ * A gameplay moment the game announces itself, for the moments no engine event covers (a wave
248
+ * starting, a scripted beat). The shell reacts with the cue's sound, rumble, camera kick and caption.
249
+ */
250
+ export interface FeedbackCueEvent {
251
+ cue: FeedbackCue;
252
+ /** Caption text that replaces the cue's default label ("WAVE 3"). */
253
+ caption?: string;
254
+ }
245
255
  /** Crossfade the procedural soundtrack to `theme` (null fades out), optionally transposing the incoming theme by `transpose` semitones. */
246
256
  export interface AudioMusicEvent {
247
257
  theme: string | null;
@@ -325,6 +335,7 @@ export interface GameEventMap {
325
335
  "audio.loopStop": AudioLoopStopEvent;
326
336
  "entity.animation": EntityAnimationEvent;
327
337
  "animation.event": AnimationClipEvent;
338
+ "feedback.cue": FeedbackCueEvent;
328
339
  "command.refused": CommandRefusedEvent;
329
340
  }
330
341
  export type GameEventHandler<TPayload> = (payload: TPayload) => void;
@@ -2,6 +2,7 @@ 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
4
  import type { UiCueOverrides } from "../audio/uiCues.js";
5
+ import type { FeedbackCueOverrides } from "../audio/feedbackCues.js";
5
6
  import type { NoticeFeedConfig } from "./notices.js";
6
7
  import type { MaterialAsset, MaterialAssignment, MaterialSurfaceParameters, MaterialTextureRole } from "../material/materialAsset.js";
7
8
  import type { EditorCatalogDefinition, EditorDocument } from "../editor/types.js";
@@ -786,6 +787,8 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
786
787
  hudCues?: boolean;
787
788
  /** 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. */
788
789
  uiCues?: UiCueOverrides;
790
+ /** Default gameplay reactions (`feel.hit`, `feel.kill`, `feel.pickup`, ...) on an "sfx" bus, played by the shell from the events the engine announces. A sound named `feel.<cue>` in `sounds` replaces one; a map entry replaces or (`false`) silences one; `false` silences all. */
791
+ feedbackCues?: FeedbackCueOverrides;
789
792
  };
790
793
  /** 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. */
791
794
  entitySounds?: Record<string, string>;
@@ -1,5 +1,5 @@
1
1
  /** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
2
- export declare const VERSION = "0.31.0";
2
+ export declare const VERSION = "0.32.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[];
@@ -1,7 +1,24 @@
1
1
  /** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
2
- export const VERSION = "0.31.0";
2
+ export const VERSION = "0.32.0";
3
3
  /** Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`). */
4
4
  export const CHANGELOG = {
5
+ "0.32.0": {
6
+ migrate: [
7
+ "Bump lockstep SDK packages to `^0.32.0`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.29.0`; `@jgengine/github` is `0.19.0`.",
8
+ ],
9
+ added: [
10
+ "Audio mixing grew a mute that composes with gain: `AudioEngine.setBusMute` / `setMasterMute` keep the player's volume while silencing a bus, `masterLevel()` / `busLevel()` report the composed value, and Settings carries a mute row per bus plus \"Mute all\" (`sound.muted`, `sound.bus.<id>.muted`). `AudioEngine.play` and `playOneShot` hold cues in a bounded 16-slot queue while the browser keeps the context suspended and play them on the first gesture instead of dropping them; `installGestureResume` wires that gesture for both presentation paths.",
11
+ "`AudioEngine.measure()` reads master RMS and peak from a lazily built analyser, and `installAudioProbe` publishes `window.__jgengineAudio` so a headless capture can prove sound with a measured level rather than a screenshot.",
12
+ "`@jgengine/core/audio/feedbackCues` ships seven procedural reactions - `hit`, `hurt`, `kill`, `pickup`, `objective`, `wave`, `deny` - on an `sfx` bus, merged into every game's catalog as `feel.<cue>`. A game sound with that id replaces one; `audio.feedbackCues` replaces or silences them.",
13
+ "`@jgengine/shell/audio/eventFeedback` maps the events the engine already announces to those reactions: `combat.damage` (local player reads as hurt, anything else as a hit), `entity.died`, `worldItem.picked_up`, `quest.completed`, `stat.levelUp` and `command.refused`. Each row carries a sound, a gamepad pulse, camera trauma and a caption; the mapping and reactions are data (`DEFAULT_FEEDBACK_BINDINGS`, `DEFAULT_FEEDBACK_REACTIONS`) a game can replace, cooldowns fold a burst into one impact, reduced motion keeps sound and rumble but drops the camera kick, and the resolved reaction is pooled so handling an event allocates nothing per frame. `attachWaveFeedback(runner, ctx.game.feedback)` announces each new wave.",
14
+ "`ctx.game.feedback.cue(cue, caption?)` announces a moment no engine event covers and emits `feedback.cue`.",
15
+ "`@jgengine/shell/audio/captions` and `CaptionOverlay` render a bounded, deduping, self-expiring caption line under the accessibility captions setting; `FeedbackHost` mounts the whole layer - feedback mapping, gesture resume, audio probe and captions - for both the 3D and HUD presentation paths.",
16
+ ],
17
+ changed: [
18
+ "The CI gate runs nine test shards instead of ten, and a `types` job whose stages overlap, so a docs-only pull request fits the account's 20-job runner cap with room for another run beside it. `scripts/test-shard.ts` owns which files each shard runs and what it prepares first; `bun scripts/type-shard.ts` runs the `check-types` plan two stages at a time and then the tsc fan-out alone.",
19
+ ],
20
+ removed: [],
21
+ },
5
22
  "0.31.0": {
6
23
  migrate: [
7
24
  "Bump lockstep SDK packages to `^0.31.0`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.28.0`; `@jgengine/github` is `0.18.0`.",
@@ -505,6 +505,9 @@ export function createGameContext(options) {
505
505
  setLoop: (id, params) => events.emit("audio.loopSet", { id, ...params }),
506
506
  stopLoop: (id) => events.emit("audio.loopStop", { id }),
507
507
  },
508
+ feedback: {
509
+ cue: (cue, caption) => events.emit("feedback.cue", caption === undefined ? { cue } : { cue, caption }),
510
+ },
508
511
  feed: {
509
512
  bind: (action) => feed.bind(action, events),
510
513
  push(action, entry) {
@@ -1,5 +1,6 @@
1
1
  import type { AuthoredSimulationRuntime } from "../world/authoredSimulation.js";
2
2
  import type { Territory } from "../world/territory.js";
3
+ import type { FeedbackCue } from "../audio/feedbackCues.js";
3
4
  import type { NoticeFeed } from "../game/notices.js";
4
5
  import type { CurrencyDefinition } from "../economy/currency.js";
5
6
  import type { CardPile, CardPileConfig } from "../cards/cardPile.js";
@@ -436,6 +437,15 @@ export interface GameContextRace {
436
437
  /** Lazily creates (on first call, `config` required) or returns the existing reactive race for `id` — discrete mutations and eventful `update` calls bump `ctx.version()`, so HUDs stop hand-managing `store.set` (#286.2). */
437
438
  state(id: string, config?: RaceStateConfig): RaceState;
438
439
  }
440
+ /**
441
+ * The game-announced feel seam on `ctx.game`: name a moment the engine has no event for and the
442
+ * shell plays its cue's sound, rumble, camera kick and caption. The reaction table lives in the
443
+ * shell, so a game never encodes what a hit or a wave should sound like.
444
+ */
445
+ export interface GameFeedback {
446
+ /** Raise `cue` (`"hit" | "hurt" | "kill" | "pickup" | "objective" | "wave" | "deny"`); `caption` replaces the cue's default caption text. Emits `feedback.cue`. */
447
+ cue(cue: FeedbackCue, caption?: string): void;
448
+ }
439
449
  /** Reachable audio seam on `ctx.game`: `play`, `music`, and `resume` route through the `audio.play`/`audio.music`/`audio.resume` events the shell's audio engine listens on, so game code triggers sound without importing the shell. Retained `loop`/`setLoop`/`stopLoop` add id-keyed loops with live pitch/gain control over `audio.loopStart`/`audio.loopSet`/`audio.loopStop` (#1051). */
440
450
  export interface GameAudio {
441
451
  play(sound: string, at?: readonly [number, number, number]): void;
@@ -500,6 +510,12 @@ export interface GameContext {
500
510
  */
501
511
  notices: NoticeFeed;
502
512
  audio: GameAudio;
513
+ /**
514
+ * Announce a gameplay moment the engine has no event for (a wave starting, a scripted beat).
515
+ * The shell answers with the cue's sound, gamepad pulse, camera kick and caption, so a game
516
+ * never encodes the reaction itself.
517
+ */
518
+ feedback: GameFeedback;
503
519
  /** Play a model's one-shot animation clip bound to `event` in its `animation.oneShots` (e.g. an attack swing) — emits `entity.animation` for the shell to pick up. */
504
520
  playEntityAnimation(instanceId: string, event: string): void;
505
521
  feed: GameContextFeed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/core",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "jgengine core — pure-TypeScript game framework SDK (npm @jgengine/core). Entity stores, commands, combat, inventory, multiplayer seams. Zero dependencies. Not an ECS. Agent skills ship in the tarball. https://jgengine.com",
5
5
  "keywords": [
6
6
  "jgengine",
@@ -6,7 +6,7 @@
6
6
 
7
7
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
8
8
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
9
- - `VERSION` (const): const VERSION: "0.31.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.32.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
10
10
 
11
11
  ## @jgengine/core/authoring
12
12
 
@@ -285,7 +285,7 @@
285
285
 
286
286
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
287
287
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
288
- - `VERSION` (const): const VERSION: "0.31.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
288
+ - `VERSION` (const): const VERSION: "0.32.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
289
289
 
290
290
  ## @jgengine/core/runtime/adapter
291
291
 
@@ -410,6 +410,7 @@
410
410
  - `GameContextRace` (interface): interface GameContextRace { state(id: string, config?: RaceStateConfig): RaceState } — ⚠ undocumented
411
411
  - `GameContextTurn` (interface): interface GameContextTurn { loop(id: string, config?: TurnLoopConfig): TurnLoop } — ⚠ undocumented
412
412
  - `GameContextWorld` (interface): interface GameContextWorld { ground: TerrainField; groundHeightAt(x: number, z: number): number; solids: WorldSolids } — ⚠ undocumented
413
+ - `GameFeedback` (interface): interface GameFeedback — The game-announced feel seam on `ctx.game`: name a moment the engine has no event for and the shell plays its cue's sound, rumble, camera kick and caption. The reaction table lives in the shell, so a game never encodes what a hit or a wave should sound like.
413
414
  - `HitReactionInput` (interface): interface HitReactionInput { from: string; to: string; config: HitReactionConfig | ImpactPresetName; power?: number } — ⚠ undocumented · used by `resolveHitReaction` (@jgengine/core/combat): Resolves hit feel (hitstop, knockback impulse, camera shake) from either a named `impactPresets` event (`resolveHitReaction("explosion", inp…
414
415
  - `MoveTowardCommitOptions` (interface): interface MoveTowardCommitOptions extends MoveTowardOptions — Options for {@link SceneEntityContext.moveTowardCommit}: {@link MoveTowardOptions} plus an optional facing turn.
415
416
  - `SceneEntityContext` (interface): interface SceneEntityContext { spawn(name: string, options?: SpawnOptions): string; despawn(instanceId: string): boolean; update: EntityStore["update"]; setPose(instanceId: string, pose: EntityPose): boolean; setVelocity: EntityStore["setVelocity"]; setPoseConstraint: … — ⚠ undocumented
@@ -604,6 +604,7 @@
604
604
  - `EntityAnimationEvent` (interface): interface EntityAnimationEvent — Request that an entity's rig play a one-shot animation clip bound to `event` in its `animation.oneShots` (e.g. an "attack" swing); the shell resolves the clip and plays it once over the locomotion state. With an `animation.graph`, `event` arms the trigger of that name.
605
605
  - `EntityDiedEvent` (interface): interface EntityDiedEvent { instanceId: string; catalogId: string; userId?: string; displayName?: string; reason: DeathReason; source?: string; position: [number, number, number]; serverId?: string } — ⚠ undocumented
606
606
  - `EntityFloatTextEvent` (interface): interface EntityFloatTextEvent { instanceId?: string; position: [number, number, number]; text: string; kind: string; amount?: number; hitType?: string; element?: string; crit?: boolean; scale?: number } — ⚠ undocumented
607
+ - `FeedbackCueEvent` (interface): interface FeedbackCueEvent — A gameplay moment the game announces itself, for the moments no engine event covers (a wave starting, a scripted beat). The shell reacts with the cue's sound, rumble, camera kick and caption.
607
608
  - `FormChangedEvent` (interface): interface FormChangedEvent { instanceId: string; formId: string | null } — ⚠ undocumented
608
609
  - `GameEventHandler` (type): type GameEventHandler<TPayload> = (payload: TPayload) => void — ⚠ undocumented
609
610
  - `GameEventMap` (interface): interface GameEventMap { "entity.died": EntityDiedEvent; "combat.damage": DamageAppliedEvent; "entity.floatText": EntityFloatTextEvent; "combat.telegraph": CombatTelegraphEvent; "combat.vfx": CombatVfxEvent; "combat.vfxInstance": CombatVfxInstanceEvent; "combat.t… — ⚠ undocumented · used by `createGameEvents`: A typed publish/subscribe bus for gameplay events that systems and HUDs subscribe to.
@@ -455,11 +455,11 @@
455
455
 
456
456
  ## @jgengine/core/audio/audioFalloff
457
457
 
458
- - `AudioBusDef` (interface): interface AudioBusDef { id: AudioBusId; gain?: number } — ⚠ undocumented · used by `withUiCueSounds` (@jgengine/core/audio/uiCues): Merge the default UI cue patches into a game's sound catalog.
458
+ - `AudioBusDef` (interface): interface AudioBusDef { id: AudioBusId; gain?: number } — ⚠ undocumented · used by `withFeedbackCueSounds` (@jgengine/core/audio/feedbackCues): Merge the default feedback reactions into a game's sound catalog.
459
459
  - `AudioBusId` (type): type AudioBusId = string — ⚠ undocumented
460
460
  - `AudioFalloffConfig` (interface): interface AudioFalloffConfig { minDistance?: number; maxDistance?: number; curve?: FalloffCurve } — ⚠ undocumented
461
461
  - `FalloffCurve` (type): type FalloffCurve = "linear" | "inverse" | "none" — ⚠ undocumented
462
- - `SoundDef` (interface): interface SoundDef { id: string; url?: string; synth?: SynthPatch; bus: AudioBusId; gain?: number; loop?: boolean; maxVoices?: number; priority?: number; positional?: boolean; falloff?: AudioFalloffConfig; doppler?: number; spatial?: {panning: "hrtf" | "equal… — ⚠ undocumented · used by `DEFAULT_UI_CUE_SOUNDS` (@jgengine/core/audio/uiCues): Short, soft procedural patches: no samples to load, quiet enough to sit under game audio.
462
+ - `SoundDef` (interface): interface SoundDef { id: string; url?: string; synth?: SynthPatch; bus: AudioBusId; gain?: number; loop?: boolean; maxVoices?: number; priority?: number; positional?: boolean; falloff?: AudioFalloffConfig; doppler?: number; spatial?: {panning: "hrtf" | "equal… — ⚠ undocumented · used by `DEFAULT_FEEDBACK_CUE_SOUNDS` (@jgengine/core/audio/feedbackCues): Short procedural reactions - no samples to load, so every game has impact feedback without authoring audio.
463
463
  - `computeFalloffGain` (function): function computeFalloffGain(distance: number, config: AudioFalloffConfig = {}): number — ⚠ undocumented
464
464
  - `distance3` (function): function distance3(a: { x: number; y: number; z: number }, b: { x: number; y: number; z: number }): number — ⚠ undocumented
465
465
  - `resolveEmitterGain` (function): function resolveEmitterGain(distance: number, sound: Pick<SoundDef, "gain" | "positional" | "falloff">, busGain: number): number — ⚠ undocumented
@@ -481,6 +481,16 @@
481
481
  - `EngineLayersState` (interface): interface EngineLayersState — Serializable {@link EngineLayers} state.
482
482
  - `createEngineLayers` (function): function createEngineLayers(initial: EngineLayersConfig): EngineLayers — Layered engine sound: each set of rpm-keyed samples is equal-power crossfaded between the two samples bracketing the current rpm and pitched by `rpm / layer.rpm`; the on- and off-load sets are equal-power crossfaded by smoothed `engineLoad`. A config with only one load set plays it at every load. `play` drives `ctx.game.audio` directly, so a game needs no per-layer glue.
483
483
 
484
+ ## @jgengine/core/audio/feedbackCues
485
+
486
+ - `DEFAULT_FEEDBACK_CUE_SOUNDS` (const): const DEFAULT_FEEDBACK_CUE_SOUNDS: Readonly<Record<FeedbackCue, SoundDef>> — Short procedural reactions - no samples to load, so every game has impact feedback without authoring audio. Loud enough to read, quiet enough to sit under a weapon's own sound.
487
+ - `FEEDBACK_CUES` (const): const FEEDBACK_CUES: readonly FeedbackCue[] — Every {@link FeedbackCue}, in a stable order.
488
+ - `FEEDBACK_CUE_BUS` (const): const FEEDBACK_CUE_BUS: "sfx" — Bus the default feedback cues mix through; the settings menu shows its volume and mute.
489
+ - `FeedbackCue` (type): type FeedbackCue = "hit" | "hurt" | "kill" | "pickup" | "objective" | "wave" | "deny" — A gameplay moment the engine reacts to: taking or dealing a hit, a kill, a pickup, an objective, a wave starting, or a refused action. The game names the moment; the engine owns the reaction.
490
+ - `FeedbackCueOverrides` (type): type FeedbackCueOverrides = false | Partial<Record<FeedbackCue, SoundDef | false>> — Per-cue override: a replacement sound, or `false` to silence that cue. `false` for the whole map silences all.
491
+ - `feedbackCueSoundId` (function): function feedbackCueSoundId(cue: FeedbackCue): string — Catalog id a cue plays under: `feel.hit`, `feel.kill`, ... A game sound with that id replaces the default.
492
+ - `withFeedbackCueSounds` (function): function withFeedbackCueSounds(sounds: Readonly<Record<string, SoundDef>> | undefined, buses: Readonly<Record<string, AudioBusDef>> | undefined, overrides?: FeedbackCueOverrides): { sounds: Record<string, SoundDef>; buses: Record<string, AudioBusDef> | undefined } — Merge the default feedback reactions into a game's sound catalog. A game sound already named `feel.<cue>` wins, then `overrides`, then the default. Adds the `sfx` bus (gain 0.9) when the game did not declare one, so the impact layer has its own volume and mute row.
493
+
484
494
  ## @jgengine/core/audio/music
485
495
 
486
496
  - `MusicInstrument` (type): type MusicInstrument = | "strings" | "flute" | "harp" | "horn" | "choir" | "bell" | "timpani" | "bass" | "stacc" | "pad" | "lute" | "dulcimer" | "frameDrum" | "warDrum" | "reed" | "pipe" | "squareLead" | "woodBlock" | "tinyBell" | "piano" | "shaker" | "brassStab" | "cymSwell" | "oboe" — Named synthesised instrument. Each maps to a voice in the shell's instrument library (`@jgengine/shell/audio/musicVoices`); an unknown name falls back to a plain sine voice so a theme is never silent.
@@ -2358,7 +2368,7 @@
2358
2368
  - `AssetSpace` (interface): interface AssetSpace — Catalog-level asset-space metadata: how a model is authored relative to the engine, owned by the catalog entry rather than re-derived per game. Every field is optional so an unmeasured asset stays valid; {@link resolveFacingRotationY}/{@link assetUnitScale}/{@link resolveAnchorOffset} supply the documented defaults.
2359
2369
  - `AssetSpaceIssue` (interface): interface AssetSpaceIssue — One problem found by {@link validateAssetSpace}: which metadata field and why it is invalid.
2360
2370
  - `AttackMoveOrderPayload` (interface): interface AttackMoveOrderPayload — Payload for an attack-move: advance toward `point`, engaging hostiles encountered en route.
2361
- - `AudioBusDef` (interface): interface AudioBusDef { id: AudioBusId; gain?: number } — ⚠ undocumented · used by `withUiCueSounds` (@jgengine/core/audio/uiCues): Merge the default UI cue patches into a game's sound catalog.
2371
+ - `AudioBusDef` (interface): interface AudioBusDef { id: AudioBusId; gain?: number } — ⚠ undocumented · used by `withFeedbackCueSounds` (@jgengine/core/audio/feedbackCues): Merge the default feedback reactions into a game's sound catalog.
2362
2372
  - `AudioFalloffConfig` (interface): interface AudioFalloffConfig { minDistance?: number; maxDistance?: number; curve?: FalloffCurve } — ⚠ undocumented
2363
2373
  - `AuthoredNavLink` (interface): interface AuthoredNavLink — A connection the scene author places between two spots — a door through a wall, a ladder to a deck, a jump across a gap. Authored as world points, not polygon ids, so it survives a re-bake: {@link NavRuntime.applyMesh} resolves each end onto the nearest walkable polygon.
2364
2374
  - `AuthoredTrigger` (interface): interface AuthoredTrigger — One resolved trigger binding from a document object.
@@ -2847,7 +2857,7 @@
2847
2857
  - `SnapMode` (type): type SnapMode = "grid" | "free" | "surface" — ⚠ undocumented
2848
2858
  - `SnowEnvironmentDescriptor` (type): type SnowEnvironmentDescriptor = { kind: "snow" } & Required< Pick<SnowEnvironmentConfig, "area" | "density" | "speed" | "flakeSize" | "drift" | "wind" | "color" | "opacity"> > — ⚠ undocumented · used by `snow`: Declares a snowfall weather effect for `environment()` — area, density, drift, wind, and flake opacity.
2849
2859
  - `SoilRules` (interface): interface SoilRules — Fully-defaulted soil params parsed from a volume's `meta`.
2850
- - `SoundDef` (interface): interface SoundDef { id: string; url?: string; synth?: SynthPatch; bus: AudioBusId; gain?: number; loop?: boolean; maxVoices?: number; priority?: number; positional?: boolean; falloff?: AudioFalloffConfig; doppler?: number; spatial?: {panning: "hrtf" | "equal… — ⚠ undocumented · used by `DEFAULT_UI_CUE_SOUNDS` (@jgengine/core/audio/uiCues): Short, soft procedural patches: no samples to load, quiet enough to sit under game audio.
2860
+ - `SoundDef` (interface): interface SoundDef { id: string; url?: string; synth?: SynthPatch; bus: AudioBusId; gain?: number; loop?: boolean; maxVoices?: number; priority?: number; positional?: boolean; falloff?: AudioFalloffConfig; doppler?: number; spatial?: {panning: "hrtf" | "equal… — ⚠ undocumented · used by `DEFAULT_FEEDBACK_CUE_SOUNDS` (@jgengine/core/audio/feedbackCues): Short procedural reactions - no samples to load, so every game has impact feedback without authoring audio.
2851
2861
  - `SpatialGrid` (class): class SpatialGrid — A uniform-grid broad-phase over the x/z plane, separate from the rigid-body sim, for cheap same-tick proximity across hundreds–thousands of simple movers (swarm enemies). Rebuild each tick from the caller's own position arrays, then `queryCircle` (enemies hitting the player / an AoE) or `forEachPair` (mutual separation). Both are precise: no false negatives, no false positives beyond the exact distance test.
2852
2862
  - `SpawnDirectorConfig` (interface): interface SpawnDirectorConfig { waves: readonly WaveManifest[]; maxAlive?: number; escalationPerSecond?: number; alertBudgetPerSecond?: number; alertDecayPerSecond?: number; playerBudgetPerSecond?: number; maxSpawnsPerTick?: number; loop?: boolean; seed?: number; spawnP… — ⚠ undocumented
2853
2863
  - `SpawnDirectorState` (interface): interface SpawnDirectorState { wave: number; elapsed: number; waveElapsed: number; budget: number; alert: number; spawnedThisWave: number; spawnedTotal: number; rng: RandomSeed; done: boolean } — ⚠ undocumented
@@ -370,6 +370,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
370
370
 
371
371
  - `createFastTravelNetwork` (function) · `import { createFastTravelNetwork } from "@jgengine/core/world"`
372
372
 
373
+ ## feedback-cue-sounds — default procedural hit/hurt/kill/pickup/objective/wave/deny reactions on an "sfx" bus, overridable per game
374
+
375
+ - `withFeedbackCueSounds` (function) · `import { withFeedbackCueSounds } from "@jgengine/core/audio/feedbackCues"`
376
+
373
377
  ## feedback-mixer — map sim telemetry to camera, audio and haptic parameters through curves, smoothing and threshold events
374
378
 
375
379
  - `createFeedbackMixer` (function) · `import { createFeedbackMixer } from "@jgengine/core/vfx/feedbackMixer"`