@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 +19 -0
- package/dist/audio/feedbackCues.d.ts +30 -0
- package/dist/audio/feedbackCues.js +119 -0
- package/dist/game/events.d.ts +11 -0
- package/dist/game/playableGame.d.ts +3 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +18 -1
- package/dist/runtime/gameContext.js +3 -0
- package/dist/runtime/gameContextTypes.d.ts +16 -0
- package/package.json +1 -1
- package/skills/jgengine/api.md +3 -2
- package/skills/jgengine-gameplay/api.md +1 -0
- package/skills/jgengine-world/api.md +14 -4
- package/skills/jgengine-world/capabilities.md +4 -0
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
|
+
}
|
package/dist/game/events.d.ts
CHANGED
|
@@ -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>;
|
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.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[];
|
package/dist/meta/changelog.js
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
/** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
|
|
2
|
-
export const VERSION = "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.
|
|
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",
|
package/skills/jgengine/api.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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"`
|