@sadhaka/loom-engine 0.10.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/LICENSE +21 -0
- package/README.md +344 -0
- package/dist/animation/animation-clip.d.ts +12 -0
- package/dist/animation/animation-clip.d.ts.map +1 -0
- package/dist/animation/animation-clip.js +85 -0
- package/dist/animation/animation-clip.js.map +1 -0
- package/dist/animation/animation-state-pool.d.ts +25 -0
- package/dist/animation/animation-state-pool.d.ts.map +1 -0
- package/dist/animation/animation-state-pool.js +113 -0
- package/dist/animation/animation-state-pool.js.map +1 -0
- package/dist/asset/sprite-sheet-loader.d.ts +41 -0
- package/dist/asset/sprite-sheet-loader.d.ts.map +1 -0
- package/dist/asset/sprite-sheet-loader.js +313 -0
- package/dist/asset/sprite-sheet-loader.js.map +1 -0
- package/dist/audio/audio-bus.d.ts +43 -0
- package/dist/audio/audio-bus.d.ts.map +1 -0
- package/dist/audio/audio-bus.js +258 -0
- package/dist/audio/audio-bus.js.map +1 -0
- package/dist/combat/mob-catalog.d.ts +29 -0
- package/dist/combat/mob-catalog.d.ts.map +1 -0
- package/dist/combat/mob-catalog.js +104 -0
- package/dist/combat/mob-catalog.js.map +1 -0
- package/dist/components/health.d.ts +28 -0
- package/dist/components/health.d.ts.map +1 -0
- package/dist/components/health.js +150 -0
- package/dist/components/health.js.map +1 -0
- package/dist/components/interactable.d.ts +30 -0
- package/dist/components/interactable.d.ts.map +1 -0
- package/dist/components/interactable.js +94 -0
- package/dist/components/interactable.js.map +1 -0
- package/dist/components/particle-emitter.d.ts +62 -0
- package/dist/components/particle-emitter.d.ts.map +1 -0
- package/dist/components/particle-emitter.js +193 -0
- package/dist/components/particle-emitter.js.map +1 -0
- package/dist/components/pursue.d.ts +23 -0
- package/dist/components/pursue.d.ts.map +1 -0
- package/dist/components/pursue.js +96 -0
- package/dist/components/pursue.js.map +1 -0
- package/dist/components/ranged-attack.d.ts +44 -0
- package/dist/components/ranged-attack.d.ts.map +1 -0
- package/dist/components/ranged-attack.js +120 -0
- package/dist/components/ranged-attack.js.map +1 -0
- package/dist/components/sprite.d.ts +27 -0
- package/dist/components/sprite.d.ts.map +1 -0
- package/dist/components/sprite.js +122 -0
- package/dist/components/sprite.js.map +1 -0
- package/dist/components/transform.d.ts +30 -0
- package/dist/components/transform.d.ts.map +1 -0
- package/dist/components/transform.js +150 -0
- package/dist/components/transform.js.map +1 -0
- package/dist/director/director-bridge.d.ts +22 -0
- package/dist/director/director-bridge.d.ts.map +1 -0
- package/dist/director/director-bridge.js +23 -0
- package/dist/director/director-bridge.js.map +1 -0
- package/dist/director/director-encounter-system.d.ts +21 -0
- package/dist/director/director-encounter-system.d.ts.map +1 -0
- package/dist/director/director-encounter-system.js +128 -0
- package/dist/director/director-encounter-system.js.map +1 -0
- package/dist/director/director-system.d.ts +19 -0
- package/dist/director/director-system.d.ts.map +1 -0
- package/dist/director/director-system.js +179 -0
- package/dist/director/director-system.js.map +1 -0
- package/dist/director/event-envelope.d.ts +144 -0
- package/dist/director/event-envelope.d.ts.map +1 -0
- package/dist/director/event-envelope.js +108 -0
- package/dist/director/event-envelope.js.map +1 -0
- package/dist/director/knot-context-resource.d.ts +25 -0
- package/dist/director/knot-context-resource.d.ts.map +1 -0
- package/dist/director/knot-context-resource.js +152 -0
- package/dist/director/knot-context-resource.js.map +1 -0
- package/dist/director/mock-director-bridge.d.ts +18 -0
- package/dist/director/mock-director-bridge.d.ts.map +1 -0
- package/dist/director/mock-director-bridge.js +75 -0
- package/dist/director/mock-director-bridge.js.map +1 -0
- package/dist/director/snapshot-recovery.d.ts +37 -0
- package/dist/director/snapshot-recovery.d.ts.map +1 -0
- package/dist/director/snapshot-recovery.js +180 -0
- package/dist/director/snapshot-recovery.js.map +1 -0
- package/dist/director/sse-director-bridge.d.ts +42 -0
- package/dist/director/sse-director-bridge.d.ts.map +1 -0
- package/dist/director/sse-director-bridge.js +280 -0
- package/dist/director/sse-director-bridge.js.map +1 -0
- package/dist/engine.d.ts +25 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +166 -0
- package/dist/engine.js.map +1 -0
- package/dist/entity.d.ts +17 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +77 -0
- package/dist/entity.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/input/input-manager.d.ts +91 -0
- package/dist/input/input-manager.d.ts.map +1 -0
- package/dist/input/input-manager.js +349 -0
- package/dist/input/input-manager.js.map +1 -0
- package/dist/input/tap-to-walk.d.ts +27 -0
- package/dist/input/tap-to-walk.d.ts.map +1 -0
- package/dist/input/tap-to-walk.js +118 -0
- package/dist/input/tap-to-walk.js.map +1 -0
- package/dist/input/virtual-dpad.d.ts +34 -0
- package/dist/input/virtual-dpad.d.ts.map +1 -0
- package/dist/input/virtual-dpad.js +267 -0
- package/dist/input/virtual-dpad.js.map +1 -0
- package/dist/renderer/camera.d.ts +26 -0
- package/dist/renderer/camera.d.ts.map +1 -0
- package/dist/renderer/camera.js +39 -0
- package/dist/renderer/camera.js.map +1 -0
- package/dist/renderer/canvas2d-device.d.ts +26 -0
- package/dist/renderer/canvas2d-device.d.ts.map +1 -0
- package/dist/renderer/canvas2d-device.js +252 -0
- package/dist/renderer/canvas2d-device.js.map +1 -0
- package/dist/renderer/graphics-device.d.ts +36 -0
- package/dist/renderer/graphics-device.d.ts.map +1 -0
- package/dist/renderer/graphics-device.js +12 -0
- package/dist/renderer/graphics-device.js.map +1 -0
- package/dist/renderer/iso-projection.d.ts +11 -0
- package/dist/renderer/iso-projection.d.ts.map +1 -0
- package/dist/renderer/iso-projection.js +59 -0
- package/dist/renderer/iso-projection.js.map +1 -0
- package/dist/resources.d.ts +28 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +53 -0
- package/dist/resources.js.map +1 -0
- package/dist/system.d.ts +14 -0
- package/dist/system.d.ts.map +1 -0
- package/dist/system.js +25 -0
- package/dist/system.js.map +1 -0
- package/dist/systems/animation-system.d.ts +8 -0
- package/dist/systems/animation-system.d.ts.map +1 -0
- package/dist/systems/animation-system.js +77 -0
- package/dist/systems/animation-system.js.map +1 -0
- package/dist/systems/attack-system.d.ts +17 -0
- package/dist/systems/attack-system.d.ts.map +1 -0
- package/dist/systems/attack-system.js +94 -0
- package/dist/systems/attack-system.js.map +1 -0
- package/dist/systems/damage-system.d.ts +18 -0
- package/dist/systems/damage-system.d.ts.map +1 -0
- package/dist/systems/damage-system.js +77 -0
- package/dist/systems/damage-system.js.map +1 -0
- package/dist/systems/input-system.d.ts +7 -0
- package/dist/systems/input-system.d.ts.map +1 -0
- package/dist/systems/input-system.js +27 -0
- package/dist/systems/input-system.js.map +1 -0
- package/dist/systems/interaction-system.d.ts +23 -0
- package/dist/systems/interaction-system.d.ts.map +1 -0
- package/dist/systems/interaction-system.js +120 -0
- package/dist/systems/interaction-system.js.map +1 -0
- package/dist/systems/particle-emitter-system.d.ts +8 -0
- package/dist/systems/particle-emitter-system.d.ts.map +1 -0
- package/dist/systems/particle-emitter-system.js +161 -0
- package/dist/systems/particle-emitter-system.js.map +1 -0
- package/dist/systems/particle-render-system.d.ts +7 -0
- package/dist/systems/particle-render-system.d.ts.map +1 -0
- package/dist/systems/particle-render-system.js +53 -0
- package/dist/systems/particle-render-system.js.map +1 -0
- package/dist/systems/particle-simulation-system.d.ts +8 -0
- package/dist/systems/particle-simulation-system.d.ts.map +1 -0
- package/dist/systems/particle-simulation-system.js +45 -0
- package/dist/systems/particle-simulation-system.js.map +1 -0
- package/dist/systems/projectile-render-system.d.ts +7 -0
- package/dist/systems/projectile-render-system.d.ts.map +1 -0
- package/dist/systems/projectile-render-system.js +35 -0
- package/dist/systems/projectile-render-system.js.map +1 -0
- package/dist/systems/projectile-system.d.ts +7 -0
- package/dist/systems/projectile-system.d.ts.map +1 -0
- package/dist/systems/projectile-system.js +114 -0
- package/dist/systems/projectile-system.js.map +1 -0
- package/dist/systems/pursue-system.d.ts +7 -0
- package/dist/systems/pursue-system.d.ts.map +1 -0
- package/dist/systems/pursue-system.js +83 -0
- package/dist/systems/pursue-system.js.map +1 -0
- package/dist/systems/ranged-attack-system.d.ts +7 -0
- package/dist/systems/ranged-attack-system.d.ts.map +1 -0
- package/dist/systems/ranged-attack-system.js +98 -0
- package/dist/systems/ranged-attack-system.js.map +1 -0
- package/dist/systems/sprite-render-system.d.ts +9 -0
- package/dist/systems/sprite-render-system.d.ts.map +1 -0
- package/dist/systems/sprite-render-system.js +108 -0
- package/dist/systems/sprite-render-system.js.map +1 -0
- package/dist/systems/veil-budget-system.d.ts +7 -0
- package/dist/systems/veil-budget-system.d.ts.map +1 -0
- package/dist/systems/veil-budget-system.js +37 -0
- package/dist/systems/veil-budget-system.js.map +1 -0
- package/dist/util/color.d.ts +19 -0
- package/dist/util/color.d.ts.map +1 -0
- package/dist/util/color.js +45 -0
- package/dist/util/color.js.map +1 -0
- package/dist/util/math.d.ts +26 -0
- package/dist/util/math.d.ts.map +1 -0
- package/dist/util/math.js +47 -0
- package/dist/util/math.js.map +1 -0
- package/dist/util/typed-arrays.d.ts +7 -0
- package/dist/util/typed-arrays.d.ts.map +1 -0
- package/dist/util/typed-arrays.js +42 -0
- package/dist/util/typed-arrays.js.map +1 -0
- package/dist/vfx/particle-pool.d.ts +61 -0
- package/dist/vfx/particle-pool.d.ts.map +1 -0
- package/dist/vfx/particle-pool.js +204 -0
- package/dist/vfx/particle-pool.js.map +1 -0
- package/dist/vfx/projectile-pool.d.ts +56 -0
- package/dist/vfx/projectile-pool.d.ts.map +1 -0
- package/dist/vfx/projectile-pool.js +157 -0
- package/dist/vfx/projectile-pool.js.map +1 -0
- package/dist/world.d.ts +23 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +101 -0
- package/dist/world.js.map +1 -0
- package/dist/zone/zone-catalog.d.ts +17 -0
- package/dist/zone/zone-catalog.d.ts.map +1 -0
- package/dist/zone/zone-catalog.js +116 -0
- package/dist/zone/zone-catalog.js.map +1 -0
- package/dist/zone/zone-state.d.ts +18 -0
- package/dist/zone/zone-state.d.ts.map +1 -0
- package/dist/zone/zone-state.js +52 -0
- package/dist/zone/zone-state.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// Loom Engine - sprite-sheet loader.
|
|
2
|
+
//
|
|
3
|
+
// Loads a PNG + JSON manifest pair from a URL and returns the
|
|
4
|
+
// {image, frames} shape that IGraphicsDevice.registerAtlas accepts
|
|
5
|
+
// directly. The engine never decides where assets come from; the
|
|
6
|
+
// caller passes a manifest URL and the loader fetches both halves.
|
|
7
|
+
//
|
|
8
|
+
// Manifest schema (see assets/knight/walk.json for an example):
|
|
9
|
+
// {
|
|
10
|
+
// "name": "knight-walk",
|
|
11
|
+
// "image": "walk.png", // sibling-relative URL of the PNG
|
|
12
|
+
// "frames": [
|
|
13
|
+
// { "x":0, "y":0, "w":16, "h":32, "name":"walk_pass_a", "duration_ms":140 },
|
|
14
|
+
// ...
|
|
15
|
+
// ],
|
|
16
|
+
// "anchor": { "x": 8, "y": 32 },
|
|
17
|
+
// "fps": 8
|
|
18
|
+
// }
|
|
19
|
+
//
|
|
20
|
+
// PRIOR-ART.md cites the Aseprite "JSON-Array" export and the
|
|
21
|
+
// TexturePacker "JSON-array" format as inspiration for the shape.
|
|
22
|
+
// We do not parse either format directly; we use a small superset
|
|
23
|
+
// (per-frame name + duration_ms + sheet anchor + fps) tailored to
|
|
24
|
+
// the engine's animation needs.
|
|
25
|
+
// Errors thrown by the loader. All carry a `kind` so callers can
|
|
26
|
+
// branch on the failure mode without parsing message strings.
|
|
27
|
+
export class SpriteSheetLoadError extends Error {
|
|
28
|
+
kind;
|
|
29
|
+
url;
|
|
30
|
+
constructor(kind, url, message, options) {
|
|
31
|
+
super(`SpriteSheetLoadError[${kind}] ${url}: ${message}`, options);
|
|
32
|
+
this.name = 'SpriteSheetLoadError';
|
|
33
|
+
this.kind = kind;
|
|
34
|
+
this.url = url;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Default browser-side image decoder. Creates an <img>, sets src to
|
|
38
|
+
// a blob: URL, and resolves on load. Throws SpriteSheetLoadError
|
|
39
|
+
// with kind='decode-image' on failure.
|
|
40
|
+
function defaultDecodeImage(bytes, url) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const blob = new Blob([bytes], { type: 'image/png' });
|
|
43
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
44
|
+
const img = new Image();
|
|
45
|
+
img.onload = () => {
|
|
46
|
+
URL.revokeObjectURL(objectUrl);
|
|
47
|
+
resolve(img);
|
|
48
|
+
};
|
|
49
|
+
img.onerror = (err) => {
|
|
50
|
+
URL.revokeObjectURL(objectUrl);
|
|
51
|
+
reject(new SpriteSheetLoadError('decode-image', url, 'Image element failed to decode the PNG bytes', { cause: err }));
|
|
52
|
+
};
|
|
53
|
+
img.src = objectUrl;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Resolve a sibling-relative image path against the manifest URL.
|
|
57
|
+
// "walk.png" alongside ".../knight/walk.json" -> ".../knight/walk.png".
|
|
58
|
+
// Absolute URLs and "/abs/path" pass through unchanged.
|
|
59
|
+
function resolveImageUrl(manifestUrl, imagePath) {
|
|
60
|
+
// URL constructor handles both absolute and relative inputs as long
|
|
61
|
+
// as we have a base. In Node (no DOM, no document) the manifestUrl
|
|
62
|
+
// must be absolute (file://, http(s)://); in the browser the
|
|
63
|
+
// Document base does the work.
|
|
64
|
+
try {
|
|
65
|
+
return new URL(imagePath, manifestUrl).toString();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Fallback: simple sibling-replace. Strip the manifest filename
|
|
69
|
+
// and append the image filename. This handles bare relative paths
|
|
70
|
+
// when no URL constructor base is reachable.
|
|
71
|
+
const slash = manifestUrl.lastIndexOf('/');
|
|
72
|
+
if (slash < 0)
|
|
73
|
+
return imagePath;
|
|
74
|
+
return manifestUrl.slice(0, slash + 1) + imagePath;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Validate a parsed JSON object against the manifest schema. Returns
|
|
78
|
+
// the typed manifest or throws SpriteSheetLoadError. We do not pull
|
|
79
|
+
// in a schema library; the engine ships zero runtime deps.
|
|
80
|
+
function validateManifest(raw, url) {
|
|
81
|
+
if (!raw || typeof raw !== 'object') {
|
|
82
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, 'manifest is not an object');
|
|
83
|
+
}
|
|
84
|
+
const m = raw;
|
|
85
|
+
if (typeof m['name'] !== 'string' || m['name'].length === 0) {
|
|
86
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, 'name must be a non-empty string');
|
|
87
|
+
}
|
|
88
|
+
if (typeof m['image'] !== 'string' || m['image'].length === 0) {
|
|
89
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, 'image must be a non-empty string');
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(m['frames']) || m['frames'].length === 0) {
|
|
92
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, 'frames must be a non-empty array');
|
|
93
|
+
}
|
|
94
|
+
const frames = [];
|
|
95
|
+
for (let i = 0; i < m['frames'].length; i++) {
|
|
96
|
+
const fRaw = m['frames'][i];
|
|
97
|
+
if (!fRaw || typeof fRaw !== 'object') {
|
|
98
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `frame[${i}] is not an object`);
|
|
99
|
+
}
|
|
100
|
+
const f = fRaw;
|
|
101
|
+
const x = f['x'];
|
|
102
|
+
const y = f['y'];
|
|
103
|
+
const w = f['w'];
|
|
104
|
+
const h = f['h'];
|
|
105
|
+
if (typeof x !== 'number' ||
|
|
106
|
+
typeof y !== 'number' ||
|
|
107
|
+
typeof w !== 'number' ||
|
|
108
|
+
typeof h !== 'number') {
|
|
109
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `frame[${i}] must have numeric x, y, w, h`);
|
|
110
|
+
}
|
|
111
|
+
if (w <= 0 || h <= 0) {
|
|
112
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `frame[${i}] w and h must be positive`);
|
|
113
|
+
}
|
|
114
|
+
const frame = { x, y, w, h };
|
|
115
|
+
if (typeof f['name'] === 'string')
|
|
116
|
+
frame.name = f['name'];
|
|
117
|
+
if (typeof f['duration_ms'] === 'number')
|
|
118
|
+
frame.duration_ms = f['duration_ms'];
|
|
119
|
+
frames.push(frame);
|
|
120
|
+
}
|
|
121
|
+
// Anchor defaults to bottom-center of the first frame if not given.
|
|
122
|
+
let anchor;
|
|
123
|
+
const aRaw = m['anchor'];
|
|
124
|
+
if (aRaw && typeof aRaw === 'object') {
|
|
125
|
+
const a = aRaw;
|
|
126
|
+
if (typeof a['x'] !== 'number' || typeof a['y'] !== 'number') {
|
|
127
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, 'anchor.x and anchor.y must be numeric when anchor is present');
|
|
128
|
+
}
|
|
129
|
+
anchor = { x: a['x'], y: a['y'] };
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const f0 = frames[0];
|
|
133
|
+
anchor = { x: f0.w / 2, y: f0.h };
|
|
134
|
+
}
|
|
135
|
+
// fps optional, defaults to 8 (matches the v1 walk-cycle cadence).
|
|
136
|
+
let fps = 8;
|
|
137
|
+
if (m['fps'] !== undefined) {
|
|
138
|
+
if (typeof m['fps'] !== 'number' || m['fps'] <= 0) {
|
|
139
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, 'fps must be a positive number');
|
|
140
|
+
}
|
|
141
|
+
fps = m['fps'];
|
|
142
|
+
}
|
|
143
|
+
// Clips optional. When absent, synthesize a 'default' clip that
|
|
144
|
+
// walks all frames in order, looping. When present, validate that
|
|
145
|
+
// each clip has a non-empty frames[] of integer indices in range
|
|
146
|
+
// and a boolean loop flag.
|
|
147
|
+
let clips;
|
|
148
|
+
if (m['clips'] === undefined) {
|
|
149
|
+
const defaultFrames = [];
|
|
150
|
+
for (let i = 0; i < frames.length; i++)
|
|
151
|
+
defaultFrames.push(i);
|
|
152
|
+
clips = [{ name: 'default', frames: defaultFrames, loop: true }];
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (!Array.isArray(m['clips'])) {
|
|
156
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, 'clips must be an array when present');
|
|
157
|
+
}
|
|
158
|
+
clips = [];
|
|
159
|
+
for (let ci = 0; ci < m['clips'].length; ci++) {
|
|
160
|
+
const cRaw = m['clips'][ci];
|
|
161
|
+
if (!cRaw || typeof cRaw !== 'object') {
|
|
162
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `clips[${ci}] is not an object`);
|
|
163
|
+
}
|
|
164
|
+
const c = cRaw;
|
|
165
|
+
if (typeof c['name'] !== 'string' || c['name'].length === 0) {
|
|
166
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `clips[${ci}].name must be a non-empty string`);
|
|
167
|
+
}
|
|
168
|
+
if (!Array.isArray(c['frames']) || c['frames'].length === 0) {
|
|
169
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `clips[${ci}].frames must be a non-empty array`);
|
|
170
|
+
}
|
|
171
|
+
const clipFrames = [];
|
|
172
|
+
for (let fi = 0; fi < c['frames'].length; fi++) {
|
|
173
|
+
const v = c['frames'][fi];
|
|
174
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0 || v >= frames.length) {
|
|
175
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `clips[${ci}].frames[${fi}] must be an integer in [0, ${frames.length})`);
|
|
176
|
+
}
|
|
177
|
+
clipFrames.push(v);
|
|
178
|
+
}
|
|
179
|
+
if (typeof c['loop'] !== 'boolean') {
|
|
180
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `clips[${ci}].loop must be boolean`);
|
|
181
|
+
}
|
|
182
|
+
const clip = { name: c['name'], frames: clipFrames, loop: c['loop'] };
|
|
183
|
+
if (c['fps'] !== undefined) {
|
|
184
|
+
if (typeof c['fps'] !== 'number' || c['fps'] <= 0) {
|
|
185
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `clips[${ci}].fps must be a positive number when present`);
|
|
186
|
+
}
|
|
187
|
+
clip.fps = c['fps'];
|
|
188
|
+
}
|
|
189
|
+
if (c['durations_ms'] !== undefined) {
|
|
190
|
+
if (!Array.isArray(c['durations_ms']) || c['durations_ms'].length !== clipFrames.length) {
|
|
191
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `clips[${ci}].durations_ms must be an array of length ${clipFrames.length} when present`);
|
|
192
|
+
}
|
|
193
|
+
const durs = [];
|
|
194
|
+
for (let di = 0; di < c['durations_ms'].length; di++) {
|
|
195
|
+
const d = c['durations_ms'][di];
|
|
196
|
+
if (typeof d !== 'number' || d <= 0) {
|
|
197
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, `clips[${ci}].durations_ms[${di}] must be a positive number`);
|
|
198
|
+
}
|
|
199
|
+
durs.push(d);
|
|
200
|
+
}
|
|
201
|
+
clip.durations_ms = durs;
|
|
202
|
+
}
|
|
203
|
+
clips.push(clip);
|
|
204
|
+
}
|
|
205
|
+
if (clips.length === 0) {
|
|
206
|
+
throw new SpriteSheetLoadError('invalid-manifest', url, 'clips must be non-empty when present');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
name: m['name'],
|
|
211
|
+
image: m['image'],
|
|
212
|
+
frames,
|
|
213
|
+
anchor,
|
|
214
|
+
fps,
|
|
215
|
+
clips,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// Load a sprite sheet from a manifest URL. Fetches the JSON, then
|
|
219
|
+
// the PNG referenced by the manifest's "image" field (resolved
|
|
220
|
+
// sibling-relative to the manifest URL), then decodes the image and
|
|
221
|
+
// returns a LoadedSpriteSheet.
|
|
222
|
+
//
|
|
223
|
+
// The returned `atlas` is shaped exactly for IGraphicsDevice.registerAtlas:
|
|
224
|
+
// const sheet = await loadSpriteSheet('/assets/knight/walk.json');
|
|
225
|
+
// const handle = device.registerAtlas(sheet.atlas);
|
|
226
|
+
export async function loadSpriteSheet(manifestUrl, options = {}) {
|
|
227
|
+
const fetchImpl = options.fetchImpl ?? (typeof fetch !== 'undefined' ? fetch : undefined);
|
|
228
|
+
if (!fetchImpl) {
|
|
229
|
+
throw new SpriteSheetLoadError('fetch-manifest', manifestUrl, 'no fetch implementation available; pass options.fetchImpl');
|
|
230
|
+
}
|
|
231
|
+
const decodeImage = options.decodeImage ?? defaultDecodeImage;
|
|
232
|
+
// 1. Fetch + parse manifest.
|
|
233
|
+
let manifestResp;
|
|
234
|
+
try {
|
|
235
|
+
manifestResp = await fetchImpl(manifestUrl);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
throw new SpriteSheetLoadError('fetch-manifest', manifestUrl, 'network error', { cause: err });
|
|
239
|
+
}
|
|
240
|
+
if (!manifestResp.ok) {
|
|
241
|
+
throw new SpriteSheetLoadError('fetch-manifest', manifestUrl, `HTTP ${manifestResp.status} ${manifestResp.statusText}`);
|
|
242
|
+
}
|
|
243
|
+
let raw;
|
|
244
|
+
try {
|
|
245
|
+
raw = await manifestResp.json();
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
throw new SpriteSheetLoadError('parse-manifest', manifestUrl, 'response is not valid JSON', { cause: err });
|
|
249
|
+
}
|
|
250
|
+
const manifest = validateManifest(raw, manifestUrl);
|
|
251
|
+
// 2. Fetch + decode image.
|
|
252
|
+
const imageUrl = resolveImageUrl(manifestUrl, manifest.image);
|
|
253
|
+
let imageResp;
|
|
254
|
+
try {
|
|
255
|
+
imageResp = await fetchImpl(imageUrl);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
throw new SpriteSheetLoadError('fetch-image', imageUrl, 'network error', { cause: err });
|
|
259
|
+
}
|
|
260
|
+
if (!imageResp.ok) {
|
|
261
|
+
throw new SpriteSheetLoadError('fetch-image', imageUrl, `HTTP ${imageResp.status} ${imageResp.statusText}`);
|
|
262
|
+
}
|
|
263
|
+
const bytes = await imageResp.arrayBuffer();
|
|
264
|
+
const image = await decodeImage(bytes, imageUrl);
|
|
265
|
+
// 3. Compose AtlasDescriptor (only fields the device reads).
|
|
266
|
+
const atlas = {
|
|
267
|
+
image,
|
|
268
|
+
frames: manifest.frames.map((f) => ({ x: f.x, y: f.y, w: f.w, h: f.h })),
|
|
269
|
+
name: manifest.name,
|
|
270
|
+
};
|
|
271
|
+
return { manifest, image, atlas };
|
|
272
|
+
}
|
|
273
|
+
// Compute the active frame index for a time-driven walk cycle. Use
|
|
274
|
+
// per-frame duration_ms if every frame has it, otherwise fall back
|
|
275
|
+
// to manifest.fps. Returns an integer in [0, frames.length).
|
|
276
|
+
//
|
|
277
|
+
// `now` is a monotonic millisecond clock (typically performance.now()
|
|
278
|
+
// in the browser or process.hrtime() bigint -> ms in Node). `start`
|
|
279
|
+
// is the t0 the caller stored when the animation began.
|
|
280
|
+
export function computeFrameIndex(manifest, now, start) {
|
|
281
|
+
const n = manifest.frames.length;
|
|
282
|
+
if (n <= 1)
|
|
283
|
+
return 0;
|
|
284
|
+
// Sum per-frame durations if available. Mixed manifests (some
|
|
285
|
+
// frames with duration_ms, some without) fall back to fps.
|
|
286
|
+
let totalDuration = 0;
|
|
287
|
+
let allHaveDuration = true;
|
|
288
|
+
for (let i = 0; i < n; i++) {
|
|
289
|
+
const d = manifest.frames[i]?.duration_ms;
|
|
290
|
+
if (typeof d === 'number' && d > 0) {
|
|
291
|
+
totalDuration += d;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
allHaveDuration = false;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const elapsed = Math.max(0, now - start);
|
|
299
|
+
if (allHaveDuration) {
|
|
300
|
+
const t = elapsed % totalDuration;
|
|
301
|
+
let acc = 0;
|
|
302
|
+
for (let i = 0; i < n; i++) {
|
|
303
|
+
acc += manifest.frames[i].duration_ms;
|
|
304
|
+
if (t < acc)
|
|
305
|
+
return i;
|
|
306
|
+
}
|
|
307
|
+
return n - 1; // safety
|
|
308
|
+
}
|
|
309
|
+
// Uniform-fps fallback.
|
|
310
|
+
const frameMs = 1000 / manifest.fps;
|
|
311
|
+
return Math.floor(elapsed / frameMs) % n;
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=sprite-sheet-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sprite-sheet-loader.js","sourceRoot":"","sources":["../../src/asset/sprite-sheet-loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type BusPriority = 'essential' | 'ambient';
|
|
2
|
+
export interface BusOptions {
|
|
3
|
+
initialGain?: number;
|
|
4
|
+
priority?: BusPriority;
|
|
5
|
+
}
|
|
6
|
+
export declare const AUDIO_BUDGET_AMBIENT_FLOOR = 0.25;
|
|
7
|
+
export declare const AUDIO_BUDGET_ESSENTIAL_FLOOR = 0.05;
|
|
8
|
+
export declare class AudioBus {
|
|
9
|
+
readonly ctx: AudioContext;
|
|
10
|
+
private master;
|
|
11
|
+
private buses;
|
|
12
|
+
private masterGain;
|
|
13
|
+
private currentBudget;
|
|
14
|
+
private suspended;
|
|
15
|
+
private constructor();
|
|
16
|
+
static create(ctx?: AudioContext): AudioBus;
|
|
17
|
+
unlock(): Promise<void>;
|
|
18
|
+
isUnlocked(): boolean;
|
|
19
|
+
input(name: string): AudioNode;
|
|
20
|
+
hasBus(name: string): boolean;
|
|
21
|
+
addBus(name: string, opts?: BusOptions): void;
|
|
22
|
+
removeBus(name: string): void;
|
|
23
|
+
setMasterGain(gain: number): void;
|
|
24
|
+
getMasterGain(): number;
|
|
25
|
+
setBusGain(name: string, gain: number): void;
|
|
26
|
+
getBusGain(name: string): number;
|
|
27
|
+
setBusMuted(name: string, muted: boolean): void;
|
|
28
|
+
isBusMuted(name: string): boolean;
|
|
29
|
+
setAudioBudget(budget: number): void;
|
|
30
|
+
getAudioBudget(): number;
|
|
31
|
+
private applyBudgetToBus;
|
|
32
|
+
playOneShot(busName: string, buffer: AudioBuffer, options?: {
|
|
33
|
+
rate?: number;
|
|
34
|
+
gain?: number;
|
|
35
|
+
}): AudioBufferSourceNode | null;
|
|
36
|
+
playTone(busName: string, freq: number, durationMs: number, options?: {
|
|
37
|
+
gain?: number;
|
|
38
|
+
type?: OscillatorType;
|
|
39
|
+
}): void;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|
|
42
|
+
export declare const RESOURCE_AUDIO_BUS = "audio_bus";
|
|
43
|
+
//# sourceMappingURL=audio-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-bus.d.ts","sourceRoot":"","sources":["../../src/audio/audio-bus.ts"],"names":[],"mappings":"AAkCA,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,SAAS,CAAC;AAElD,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB;AAqBD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,qBAAa,QAAQ;IACnB,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,SAAS,CAAiB;IAElC,OAAO;IAcP,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,YAAY,GAAG,QAAQ;IAarC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAe7B,UAAU,IAAI,OAAO;IAOrB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS;IAQ9B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI7B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,UAAe,GAAG,IAAI;IAejD,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAO7B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAKjC,aAAa,IAAI,MAAM;IAIvB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAO5C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIhC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAO/C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAOjC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAUpC,cAAc,IAAI,MAAM;IAIxB,OAAO,CAAC,gBAAgB;IAmBxB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,qBAAqB,GAAG,IAAI;IAoB/H,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,cAAc,CAAA;KAAO,GAAG,IAAI;IAuBzH,OAAO,IAAI,IAAI;CAOhB;AAID,eAAO,MAAM,kBAAkB,cAAc,CAAC"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// AudioBus - simple Web Audio mixer for the Loom Engine.
|
|
2
|
+
//
|
|
3
|
+
// Architecture:
|
|
4
|
+
// master (GainNode, output -> destination)
|
|
5
|
+
// |- 'sfx' sub-bus
|
|
6
|
+
// |- 'music' sub-bus
|
|
7
|
+
// |- 'voice' sub-bus
|
|
8
|
+
// |- 'ui' sub-bus
|
|
9
|
+
//
|
|
10
|
+
// Sub-buses are GainNodes that route into master. Sound-source code
|
|
11
|
+
// connects an AudioNode to bus.input(name) and the bus handles routing
|
|
12
|
+
// + per-bus gain + per-bus mute. New named buses can be created via
|
|
13
|
+
// addBus(name, opts).
|
|
14
|
+
//
|
|
15
|
+
// Lazy unlock: browsers block AudioContext until the first user
|
|
16
|
+
// gesture. AudioBus.create() does NOT auto-resume the context;
|
|
17
|
+
// callers must call unlock() inside a click / touchstart / keydown
|
|
18
|
+
// handler or use a UI prompt. Calling play* before unlock returns
|
|
19
|
+
// silently with no error - production builds want a single warning
|
|
20
|
+
// in the console; v1 keeps it quiet to avoid spam.
|
|
21
|
+
//
|
|
22
|
+
// VE-budget gating per LOOM-ENGINE-SPEC.md Section 3 / Phase 5: the
|
|
23
|
+
// caller passes the VeilBudget audioBudget into setMasterAudioBudget
|
|
24
|
+
// each tick (or on Director updates). When the budget drops below
|
|
25
|
+
// the priority floor for a bus, that bus mutes. Default tier order:
|
|
26
|
+
// essential (always plays): sfx, voice
|
|
27
|
+
// ambient (mutes under load): music, ui
|
|
28
|
+
//
|
|
29
|
+
// Inspirations (per PRIOR-ART.md):
|
|
30
|
+
// Web Audio API W3C spec - public technique, no patent IP
|
|
31
|
+
// Gain bus pattern - canonical (FMOD, Wwise, Web Audio docs)
|
|
32
|
+
// Priority-tiered ducking - common in game audio engines, not
|
|
33
|
+
// patented as a pattern; Loom-specific is the VE-budget driver
|
|
34
|
+
const DEFAULT_BUSES = [
|
|
35
|
+
{ name: 'sfx', opts: { initialGain: 1.0, priority: 'essential' } },
|
|
36
|
+
{ name: 'music', opts: { initialGain: 0.6, priority: 'ambient' } },
|
|
37
|
+
{ name: 'voice', opts: { initialGain: 1.0, priority: 'essential' } },
|
|
38
|
+
{ name: 'ui', opts: { initialGain: 0.8, priority: 'ambient' } },
|
|
39
|
+
];
|
|
40
|
+
// Budget thresholds. When VeilBudget.audioBudget drops below the
|
|
41
|
+
// 'ambient' threshold, ambient buses mute. When it drops below
|
|
42
|
+
// 'essential', everything mutes (engine still runs in silence).
|
|
43
|
+
// These are arbitrary scalars; the Director's budget value should be
|
|
44
|
+
// in [0, 1] where 1 is "all good".
|
|
45
|
+
export const AUDIO_BUDGET_AMBIENT_FLOOR = 0.25;
|
|
46
|
+
export const AUDIO_BUDGET_ESSENTIAL_FLOOR = 0.05;
|
|
47
|
+
export class AudioBus {
|
|
48
|
+
ctx;
|
|
49
|
+
master;
|
|
50
|
+
buses = new Map();
|
|
51
|
+
masterGain = 1;
|
|
52
|
+
currentBudget = 1.0;
|
|
53
|
+
suspended = true;
|
|
54
|
+
constructor(ctx) {
|
|
55
|
+
this.ctx = ctx;
|
|
56
|
+
this.master = ctx.createGain();
|
|
57
|
+
this.master.gain.value = this.masterGain;
|
|
58
|
+
this.master.connect(ctx.destination);
|
|
59
|
+
for (const def of DEFAULT_BUSES) {
|
|
60
|
+
this.addBus(def.name, def.opts);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Construct from an existing AudioContext (tests pass mocks) or
|
|
64
|
+
// let the bus create one. Browser autoplay policy means the new
|
|
65
|
+
// context starts suspended.
|
|
66
|
+
static create(ctx) {
|
|
67
|
+
if (ctx) {
|
|
68
|
+
return new AudioBus(ctx);
|
|
69
|
+
}
|
|
70
|
+
if (typeof AudioContext === 'undefined') {
|
|
71
|
+
throw new Error('AudioBus.create: AudioContext is unavailable in this environment');
|
|
72
|
+
}
|
|
73
|
+
return new AudioBus(new AudioContext());
|
|
74
|
+
}
|
|
75
|
+
// Resume the AudioContext after a user gesture. Returns a promise
|
|
76
|
+
// that settles when the context is running. Calling more than once
|
|
77
|
+
// is safe.
|
|
78
|
+
async unlock() {
|
|
79
|
+
if (this.ctx.state === 'running') {
|
|
80
|
+
this.suspended = false;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
await this.ctx.resume();
|
|
85
|
+
this.suspended = this.ctx.state !== 'running';
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Already-suspended contexts that fail resume are usually a
|
|
89
|
+
// policy issue; nothing the engine can do beyond surface it.
|
|
90
|
+
this.suspended = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
isUnlocked() {
|
|
94
|
+
return !this.suspended && this.ctx.state === 'running';
|
|
95
|
+
}
|
|
96
|
+
// Public: a node a sound source connects to so the bus routes its
|
|
97
|
+
// output through master. Returns the bus's input GainNode; users
|
|
98
|
+
// call audioSource.connect(bus.input('sfx')).
|
|
99
|
+
input(name) {
|
|
100
|
+
const entry = this.buses.get(name);
|
|
101
|
+
if (!entry) {
|
|
102
|
+
throw new Error('AudioBus.input: unknown bus "' + name + '"');
|
|
103
|
+
}
|
|
104
|
+
return entry.node;
|
|
105
|
+
}
|
|
106
|
+
hasBus(name) {
|
|
107
|
+
return this.buses.has(name);
|
|
108
|
+
}
|
|
109
|
+
addBus(name, opts = {}) {
|
|
110
|
+
if (this.buses.has(name))
|
|
111
|
+
return; // idempotent
|
|
112
|
+
const node = this.ctx.createGain();
|
|
113
|
+
const initial = opts.initialGain ?? 1.0;
|
|
114
|
+
node.gain.value = initial;
|
|
115
|
+
node.connect(this.master);
|
|
116
|
+
this.buses.set(name, {
|
|
117
|
+
node,
|
|
118
|
+
baseGain: initial,
|
|
119
|
+
muted: false,
|
|
120
|
+
priority: opts.priority ?? 'ambient',
|
|
121
|
+
});
|
|
122
|
+
this.applyBudgetToBus(name);
|
|
123
|
+
}
|
|
124
|
+
removeBus(name) {
|
|
125
|
+
const entry = this.buses.get(name);
|
|
126
|
+
if (!entry)
|
|
127
|
+
return;
|
|
128
|
+
entry.node.disconnect();
|
|
129
|
+
this.buses.delete(name);
|
|
130
|
+
}
|
|
131
|
+
setMasterGain(gain) {
|
|
132
|
+
this.masterGain = Math.max(0, gain);
|
|
133
|
+
this.master.gain.value = this.masterGain;
|
|
134
|
+
}
|
|
135
|
+
getMasterGain() {
|
|
136
|
+
return this.masterGain;
|
|
137
|
+
}
|
|
138
|
+
setBusGain(name, gain) {
|
|
139
|
+
const entry = this.buses.get(name);
|
|
140
|
+
if (!entry)
|
|
141
|
+
return;
|
|
142
|
+
entry.baseGain = Math.max(0, gain);
|
|
143
|
+
this.applyBudgetToBus(name);
|
|
144
|
+
}
|
|
145
|
+
getBusGain(name) {
|
|
146
|
+
return this.buses.get(name)?.baseGain ?? 0;
|
|
147
|
+
}
|
|
148
|
+
setBusMuted(name, muted) {
|
|
149
|
+
const entry = this.buses.get(name);
|
|
150
|
+
if (!entry)
|
|
151
|
+
return;
|
|
152
|
+
entry.muted = muted;
|
|
153
|
+
this.applyBudgetToBus(name);
|
|
154
|
+
}
|
|
155
|
+
isBusMuted(name) {
|
|
156
|
+
return this.buses.get(name)?.muted ?? false;
|
|
157
|
+
}
|
|
158
|
+
// Apply the latest VeilBudget audioBudget. Caller pushes this from
|
|
159
|
+
// the resource each tick (or on Director updates). Idempotent;
|
|
160
|
+
// re-apply with the same value is cheap.
|
|
161
|
+
setAudioBudget(budget) {
|
|
162
|
+
if (Number.isNaN(budget) || budget < 0)
|
|
163
|
+
budget = 0;
|
|
164
|
+
if (budget > 1)
|
|
165
|
+
budget = 1;
|
|
166
|
+
if (budget === this.currentBudget)
|
|
167
|
+
return;
|
|
168
|
+
this.currentBudget = budget;
|
|
169
|
+
for (const name of this.buses.keys()) {
|
|
170
|
+
this.applyBudgetToBus(name);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
getAudioBudget() {
|
|
174
|
+
return this.currentBudget;
|
|
175
|
+
}
|
|
176
|
+
applyBudgetToBus(name) {
|
|
177
|
+
const entry = this.buses.get(name);
|
|
178
|
+
if (!entry)
|
|
179
|
+
return;
|
|
180
|
+
let effective = entry.baseGain;
|
|
181
|
+
if (entry.muted)
|
|
182
|
+
effective = 0;
|
|
183
|
+
if (this.currentBudget < AUDIO_BUDGET_ESSENTIAL_FLOOR) {
|
|
184
|
+
effective = 0;
|
|
185
|
+
}
|
|
186
|
+
else if (entry.priority === 'ambient' &&
|
|
187
|
+
this.currentBudget < AUDIO_BUDGET_AMBIENT_FLOOR) {
|
|
188
|
+
effective = 0;
|
|
189
|
+
}
|
|
190
|
+
entry.node.gain.value = effective;
|
|
191
|
+
}
|
|
192
|
+
// Convenience: play a one-shot AudioBuffer through a named bus.
|
|
193
|
+
// Returns the AudioBufferSourceNode so callers can stop / track it.
|
|
194
|
+
// Returns null if the bus or context isn't ready.
|
|
195
|
+
playOneShot(busName, buffer, options = {}) {
|
|
196
|
+
const entry = this.buses.get(busName);
|
|
197
|
+
if (!entry)
|
|
198
|
+
return null;
|
|
199
|
+
if (!this.isUnlocked())
|
|
200
|
+
return null;
|
|
201
|
+
const src = this.ctx.createBufferSource();
|
|
202
|
+
src.buffer = buffer;
|
|
203
|
+
src.playbackRate.value = options.rate ?? 1;
|
|
204
|
+
if (options.gain !== undefined) {
|
|
205
|
+
const g = this.ctx.createGain();
|
|
206
|
+
g.gain.value = options.gain;
|
|
207
|
+
src.connect(g).connect(entry.node);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
src.connect(entry.node);
|
|
211
|
+
}
|
|
212
|
+
src.start();
|
|
213
|
+
return src;
|
|
214
|
+
}
|
|
215
|
+
// Convenience: short tone via OscillatorNode. Useful for code-only
|
|
216
|
+
// demos and UI feedback when no sound assets are loaded yet.
|
|
217
|
+
playTone(busName, freq, durationMs, options = {}) {
|
|
218
|
+
const entry = this.buses.get(busName);
|
|
219
|
+
if (!entry)
|
|
220
|
+
return;
|
|
221
|
+
if (!this.isUnlocked())
|
|
222
|
+
return;
|
|
223
|
+
const osc = this.ctx.createOscillator();
|
|
224
|
+
const gain = this.ctx.createGain();
|
|
225
|
+
osc.type = options.type ?? 'sine';
|
|
226
|
+
osc.frequency.value = freq;
|
|
227
|
+
const peakGain = options.gain ?? 0.2;
|
|
228
|
+
// Tiny attack / release envelope so the tone doesn't click.
|
|
229
|
+
const now = this.ctx.currentTime;
|
|
230
|
+
const dur = Math.max(0.01, durationMs / 1000);
|
|
231
|
+
gain.gain.setValueAtTime(0, now);
|
|
232
|
+
gain.gain.linearRampToValueAtTime(peakGain, now + 0.005);
|
|
233
|
+
gain.gain.linearRampToValueAtTime(peakGain, now + dur - 0.02);
|
|
234
|
+
gain.gain.linearRampToValueAtTime(0, now + dur);
|
|
235
|
+
osc.connect(gain).connect(entry.node);
|
|
236
|
+
osc.start(now);
|
|
237
|
+
osc.stop(now + dur + 0.05);
|
|
238
|
+
}
|
|
239
|
+
// Tear down. Useful in tests; production demo lives the lifetime of
|
|
240
|
+
// the page.
|
|
241
|
+
dispose() {
|
|
242
|
+
for (const entry of this.buses.values()) {
|
|
243
|
+
try {
|
|
244
|
+
entry.node.disconnect();
|
|
245
|
+
}
|
|
246
|
+
catch { /* ignore */ }
|
|
247
|
+
}
|
|
248
|
+
this.buses.clear();
|
|
249
|
+
try {
|
|
250
|
+
this.master.disconnect();
|
|
251
|
+
}
|
|
252
|
+
catch { /* ignore */ }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Resource key for the world's resource registry. Engine.create
|
|
256
|
+
// registers an AudioBus instance under this key.
|
|
257
|
+
export const RESOURCE_AUDIO_BUS = 'audio_bus';
|
|
258
|
+
//# sourceMappingURL=audio-bus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-bus.js","sourceRoot":"","sources":["../../src/audio/audio-bus.ts"],"names":[],"mappings":"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"}
|