A game that stays silent is only half a game, so a sound is an asset like any other (#45)
game-maker / suite (push) Successful in 12m1s
game-maker / suite (push) Successful in 12m1s
Sounds follow the road fonts already paved: the file goes to the server content-addressed under its sha256, the project keeps a named asset that points at it, and nothing is ever re-encoded. The format is read from the file's own signature, not from what the browser claims - the same .m4a arrives as audio/mp4, audio/x-m4a or as nothing at all. Playing is two nodes. "Play sound" takes the asset and may raise or lower its volume; every call is its own voice, so two jumps in a row are two jumps heard. Its loop input is a choice of three - from the asset, on, off - because a checkbox has to spend one of its two positions on "not set" and would lose a case: an asset that loops could then never be played once. Below the graph core stays browser-free: NodeContext hands out an asset id, BehaviorSystem resolves it against the project and folds the asset's own volume and loop into the call, and only viewport/SoundLibrary knows about Web Audio. A run without a sink plays nothing instead of throwing. The CI browser is muted, so the ear is no witness: the debug hook counts started and stopped voices, and step 34 reads that counter.
This commit is contained in:
1 parent
7babf88636
commit
5671a38044
47 files changed
+2066
-42
No files matched your search
+11
-1
@@ -29,6 +29,7 @@ import type { Position, PropertyBag, Vec2 } from '../src/core/model/Properties';
|
||||
import { createEmptyProject, findScene, type Project } from '../src/core/model/Project';
|
||||
import type { SceneObject } from '../src/core/model/SceneObject';
|
||||
import { deserializeProject, serializeProject } from '../src/core/store/serialize';
|
||||
import type { AudioSink } from '../src/runtime/systems/Audio';
|
||||
import { BehaviorSystem } from '../src/runtime/systems/BehaviorSystem';
|
||||
import { CollisionSystem } from '../src/runtime/systems/CollisionSystem';
|
||||
import { AnimationSystem } from '../src/runtime/systems/AnimationSystem';
|
||||
@@ -193,6 +194,9 @@ export type FakeContext = NodeContext & {
|
||||
logs: string[];
|
||||
stopped: boolean;
|
||||
changes: { machineId: string; from: string; to: string }[];
|
||||
/** What the context was asked to play and to silence, in order – nothing here makes a noise. */
|
||||
played: { assetId: string; volume: number; loop: boolean | undefined }[];
|
||||
silenced: string[];
|
||||
};
|
||||
|
||||
/** A NodeContext double: self/owner handles, `speed = 100`, dt 0.5, moves applied directly, `log` and `stop` recorded. */
|
||||
@@ -221,6 +225,10 @@ export function fakeContext(overrides: Partial<NodeContext> = {}): FakeContext {
|
||||
return { blockedX: false, blockedY: false };
|
||||
},
|
||||
log: (message) => logs.push(message),
|
||||
played: [],
|
||||
silenced: [],
|
||||
playSound: (assetId, volume, loop) => ctx.played.push({ assetId, volume, loop }),
|
||||
stopSound: (assetId) => ctx.silenced.push(assetId),
|
||||
spawn: () => null,
|
||||
destroy: () => {},
|
||||
stop: () => {
|
||||
@@ -259,6 +267,8 @@ export interface WorldOptions {
|
||||
collisions?: boolean;
|
||||
animations?: boolean;
|
||||
behaviors?: boolean;
|
||||
/** Where `Play sound` ends up; without one the run plays to nobody (see `SILENT_AUDIO`). */
|
||||
audio?: AudioSink;
|
||||
}
|
||||
|
||||
export interface StartedWorld {
|
||||
@@ -285,7 +295,7 @@ export function startWorld(fixture: SceneFixture, options: WorldOptions = {}): S
|
||||
if (collisions) world.addSystem(collisions);
|
||||
if (animations) world.addSystem(animations);
|
||||
const behaviors = options.behaviors
|
||||
? new BehaviorSystem(fixture.project, input, { collisions, animations })
|
||||
? new BehaviorSystem(fixture.project, input, { collisions, animations, audio: options.audio })
|
||||
: null;
|
||||
if (behaviors) world.addSystem(behaviors);
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user