@moku-labs/game 0.4.3 → 0.4.4

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.
@@ -1,4 +1,4 @@
1
- import { $ as Ease, I as AnyComponentValue, Kt as Json, L as ComponentHandle, Nt as TypeTag, P as AnyComponent, Rt as Api, V as Entity, ct as TrackSegment, it as NumericFields, k as Api$2, m as Api$3, rt as MotionHandle, st as TrackOptions, ut as Api$1, vn as Require, z as ComponentType } from "./types-Dy1xB68L.mjs";
1
+ import { $ as Ease, Gt as Events$2, I as AnyComponentValue, K as Owner, Kt as Json, L as ComponentHandle, Nt as TypeTag, P as AnyComponent, Pt as Descriptor, Rt as Api, Ut as Api$5, V as Entity, ct as TrackSegment, et as LayerSort, it as NumericFields, k as Api$2, m as Api$3, o as Events$4, rt as MotionHandle, sn as Events$3, st as TrackOptions, t as Api$4, tt as LayerSpec, ut as Api$1, vn as Require, z as ComponentType } from "./types-Dy1xB68L.mjs";
2
2
  import { D as TextApi, H as InputApi, I as I18nApi, c as UiNode, i as Finding, n as ElementChange, o as JsxState, r as ElementMotion, v as StylesState } from "./types-VErbiEXy.mjs";
3
3
  import { Log } from "@moku-labs/common/browser";
4
4
  import { PluginCtx } from "@moku-labs/core";
@@ -22,8 +22,8 @@ type LayoutState = {
22
22
  order: number;
23
23
  cleanups: Array<() => void>;
24
24
  };
25
- declare namespace types_d_exports$1 {
26
- export { Config$1 as Config, Deps$1 as Deps, ElementChange, ElementMotion, Finding, FocusRing, KernelSlice$1 as KernelSlice, State$1 as State, TextInputLook, UiApi, UiCtx, UiNode };
25
+ declare namespace types_d_exports$3 {
26
+ export { Config$3 as Config, Deps$3 as Deps, ElementChange, ElementMotion, Finding, FocusRing, KernelSlice$3 as KernelSlice, State$3 as State, TextInputLook, UiApi, UiCtx, UiNode };
27
27
  }
28
28
  /**
29
29
  * The look of the keyboard focus ring: a dashed ring with a solid halo under it, drawn around the
@@ -96,7 +96,7 @@ type TextInputLook = {
96
96
  * });
97
97
  * ```
98
98
  */
99
- type Config$1 = {
99
+ type Config$3 = {
100
100
  /** Smallest tap target `lint()` accepts, in CSS px (points). */tapTargetPt: number; /** `when` flags: `tall` when height / width is at least `tall`, `wide` the other way round. */
101
101
  breakpoints: {
102
102
  tall: number;
@@ -116,7 +116,7 @@ type Config$1 = {
116
116
  /**
117
117
  * ui plugin state: one branch per module.
118
118
  */
119
- type State$1 = {
119
+ type State$3 = {
120
120
  jsx: JsxState;
121
121
  styles: StylesState;
122
122
  layout: LayoutState;
@@ -125,7 +125,7 @@ type State$1 = {
125
125
  * Resolved dependency APIs. `anim` is not here: its edge is validation only, and its work
126
126
  * reaches `ui` through the tween driver behind every `ViewHandle`.
127
127
  */
128
- type Deps$1 = {
128
+ type Deps$3 = {
129
129
  time: Api;
130
130
  flow: Api$1;
131
131
  world: Api$2;
@@ -139,7 +139,7 @@ type Deps$1 = {
139
139
  *
140
140
  * `ui` owns no event, so `emit` is the kernel's and never called here.
141
141
  */
142
- type KernelSlice$1 = PluginCtx<Config$1, State$1> & {
142
+ type KernelSlice$3 = PluginCtx<Config$3, State$3> & {
143
143
  readonly global: object;
144
144
  readonly log: Log.LogApi;
145
145
  readonly require: Require;
@@ -147,8 +147,8 @@ type KernelSlice$1 = PluginCtx<Config$1, State$1> & {
147
147
  /**
148
148
  * Domain context shared by the three modules: the kernel slice plus the resolved deps.
149
149
  */
150
- type UiCtx = KernelSlice$1 & {
151
- readonly deps: Deps$1;
150
+ type UiCtx = KernelSlice$3 & {
151
+ readonly deps: Deps$3;
152
152
  };
153
153
  /**
154
154
  * ui plugin API, `app.ui`. The screen is one more projection, so three members read it the way a
@@ -865,8 +865,8 @@ type RunningTimeline = {
865
865
  readonly resolve: () => void;
866
866
  ended: boolean;
867
867
  };
868
- declare namespace types_d_exports {
869
- export { AnimApi, AnimCtx, AnimationDefinition, AnimationSpec, AnyAnimationDefinition, BuildTools, Config, Cursor, Deps, ErasedFields, Events, ExternalPlayer, FrameLoop, FramesValue, FxStep, HapticDescriptor, HapticKind, KernelSlice, MarkListener, MotionKeyframe, PlayDescriptor, PlayHandle, Pose, RunningTimeline, SfxDescriptor, SlotRecord, SlotTags, SlotValue, SlotValues, State, Step, StepMotion, Target, TimelineRuntime, Track };
868
+ declare namespace types_d_exports$2 {
869
+ export { AnimApi, AnimCtx, AnimationDefinition, AnimationSpec, AnyAnimationDefinition, BuildTools, Config$2 as Config, Cursor, Deps$2 as Deps, ErasedFields, Events$1 as Events, ExternalPlayer, FrameLoop, FramesValue, FxStep, HapticDescriptor, HapticKind, KernelSlice$2 as KernelSlice, MarkListener, MotionKeyframe, PlayDescriptor, PlayHandle, Pose, RunningTimeline, SfxDescriptor, SlotRecord, SlotTags, SlotValue, SlotValues, State$2 as State, Step, StepMotion, Target, TimelineRuntime, Track };
870
870
  }
871
871
  /**
872
872
  * What a step and a slot are pointed at: the projection key of a live view or of an element a
@@ -1034,7 +1034,7 @@ type MarkListener = (animation: string, mark: string) => void;
1034
1034
  * }); // a reached mark("done") step logs { mark: "done" }
1035
1035
  * ```
1036
1036
  */
1037
- type Events = {
1037
+ type Events$1 = {
1038
1038
  /** A `mark` step was reached, or jumped by `finish()`. */"anim:mark": {
1039
1039
  animation: string;
1040
1040
  mark: string;
@@ -1118,7 +1118,7 @@ type FrameLoop = {
1118
1118
  * createApp({ plugins: [...screen], pluginConfigs: { anim: { maxTracks: 500, reducedMotion: true } } });
1119
1119
  * ```
1120
1120
  */
1121
- type Config = {
1121
+ type Config$2 = {
1122
1122
  /** Dev guard: one warning each time the running track count rises past this number. */maxTracks: number;
1123
1123
  /**
1124
1124
  * Start value of reduced motion: every track but a loop takes 0 ms and every loop stands on
@@ -1130,7 +1130,7 @@ type Config = {
1130
1130
  * anim plugin state: the one track table, the field bookkeeping of the retarget policy and the
1131
1131
  * offsets accumulator, the running timelines and the animation registry.
1132
1132
  */
1133
- type State = {
1133
+ type State$2 = {
1134
1134
  /** The one track table. Insertion order is advance order. */tracks: Map<number, Track>; /** Ids of tracks and timelines. */
1135
1135
  nextId: number; /** `"<entity>:<component>:<field>"` to the id of the absolute track that drives the field. */
1136
1136
  owner: Map<string, number>; /** Per field key, the delta each additive track contributes. */
@@ -1278,7 +1278,7 @@ type AnimApi = {
1278
1278
  * of `at` with the pure `rootPoseOf` of `renderer/sync/pose.ts`, and turns the target of a
1279
1279
  * `tween` in `space: "root"` local with the pure `localPoseOf` of the same module.
1280
1280
  */
1281
- type Deps = {
1281
+ type Deps$2 = {
1282
1282
  time: Api;
1283
1283
  flow: Api$1;
1284
1284
  world: Api$2;
@@ -1290,7 +1290,7 @@ type Deps = {
1290
1290
  * row 8), so the own events reach the context the kernel hands the factories and `emit` is the
1291
1291
  * kernel's own, anim-typed one. No member of the context is cast.
1292
1292
  */
1293
- type KernelSlice = PluginCtx<Config, State, Events> & {
1293
+ type KernelSlice$2 = PluginCtx<Config$2, State$2, Events$1> & {
1294
1294
  readonly global: object;
1295
1295
  readonly log: Log.LogApi;
1296
1296
  readonly require: Require;
@@ -1298,8 +1298,659 @@ type KernelSlice = PluginCtx<Config, State, Events> & {
1298
1298
  /**
1299
1299
  * Domain context of the anim plugin: the kernel slice and the three resolved dependencies.
1300
1300
  */
1301
- type AnimCtx = KernelSlice & {
1301
+ type AnimCtx = KernelSlice$2 & {
1302
+ readonly deps: Deps$2;
1303
+ };
1304
+ declare namespace types_d_exports$1 {
1305
+ export { AnySceneProjection, CheckedProjections, Config$1 as Config, DefineScene, Deps$1 as Deps, Events, KernelSlice$1 as KernelSlice, LayerMap, RunContext, SceneDefinition, SceneLayer, SceneOwner, SceneProjection, SceneSpec, ScenesApi, ScenesCtx, SortRule, State$1 as State, UiLayer };
1306
+ }
1307
+ /**
1308
+ * How a layer orders what is drawn in it. The scene's name for the rule `world` stores.
1309
+ *
1310
+ * @example
1311
+ * ```ts
1312
+ * const sort: SortRule = "y";
1313
+ * ```
1314
+ */
1315
+ type SortRule = LayerSort;
1316
+ /**
1317
+ * What a scene declares about its layers: the key is the layer name, the order of the keys is
1318
+ * draw order, bottom first. An omitted `sort` means `"none"`.
1319
+ *
1320
+ * @example
1321
+ * ```ts
1322
+ * const layers: LayerMap = { background: {}, cells: {}, items: { sort: "y" }, lifted: {} };
1323
+ * ```
1324
+ */
1325
+ type LayerMap = Record<string, {
1326
+ sort?: SortRule;
1327
+ }>;
1328
+ /**
1329
+ * The name of the layer every scene gets on top of the layers it declared, where popups and the
1330
+ * HUD are mounted. A scene that declares it keeps the place it wrote it in.
1331
+ *
1332
+ * @example
1333
+ * ```ts
1334
+ * const layer: UiLayer = "ui";
1335
+ * ```
1336
+ */
1337
+ type UiLayer = "ui";
1338
+ /**
1339
+ * The layer names the projections of a scene may use: the keys the scene declared and the
1340
+ * appended `ui`, which is there whether the scene wrote it or not.
1341
+ *
1342
+ * @example
1343
+ * ```ts
1344
+ * const layer: SceneLayer<{ cells: Record<never, never> }> = "ui"; // or "cells"
1345
+ * ```
1346
+ */
1347
+ type SceneLayer<Layers extends LayerMap> = (keyof Layers & string) | UiLayer;
1348
+ /**
1349
+ * Any projection as a scene reads it. `projection()` widens `lift` to `string` when the author
1350
+ * declared none, so this is the loosest shape `defineScene` accepts.
1351
+ *
1352
+ * @example
1353
+ * ```ts
1354
+ * const spec: AnySceneProjection = { name: "board.items", layer: "items", lift: "lifted" };
1355
+ * ```
1356
+ */
1357
+ type AnySceneProjection = {
1358
+ readonly name: string;
1359
+ readonly layer: string;
1360
+ readonly lift?: string;
1361
+ };
1362
+ /**
1363
+ * The three fields of a `ProjectionSpec` a scene reads, with both layers checked against the
1364
+ * layers the scene declares. It is what a wrong `layer` or `lift` is reported against.
1365
+ *
1366
+ * @example
1367
+ * ```ts
1368
+ * const spec: SceneProjection<"items" | "lifted"> = { name: "board.items", layer: "items" };
1369
+ * ```
1370
+ */
1371
+ type SceneProjection<Layer extends string> = {
1372
+ readonly name: string;
1373
+ readonly layer: Layer;
1374
+ readonly lift?: Layer;
1375
+ };
1376
+ /**
1377
+ * Checks the `lift` of one projection. A projection built without a `lift` carries the widened
1378
+ * `string` of `projection()`, which is the one lift that is always accepted.
1379
+ */
1380
+ type CheckedLift<Layer extends string, Spec> = Spec extends {
1381
+ readonly lift?: infer Lift;
1382
+ } ? string extends Exclude<Lift, undefined> ? Spec : Exclude<Lift, undefined> extends Layer ? Spec : SceneProjection<Layer> : Spec;
1383
+ /**
1384
+ * Checks one projection of a scene: first its `layer`, then its `lift`. A projection that fails
1385
+ * becomes `SceneProjection<Layer>`, so the compiler prints the layer names it accepts.
1386
+ */
1387
+ type CheckedProjection<Layer extends string, Spec> = Spec extends {
1388
+ readonly layer: infer Declared;
1389
+ } ? Declared extends Layer ? CheckedLift<Layer, Spec> : SceneProjection<Layer> : SceneProjection<Layer>;
1390
+ /**
1391
+ * Checking type: validates the projections of a scene one by one against its layer names, so the
1392
+ * error sits on the projection that is wrong and not on the whole list.
1393
+ *
1394
+ * @example
1395
+ * ```ts
1396
+ * type Checked = CheckedProjections<"items", readonly [{ name: "board.items"; layer: "items" }]>;
1397
+ * ```
1398
+ */
1399
+ type CheckedProjections<Layer extends string, List> = { readonly [Index in keyof List]: CheckedProjection<Layer, List[Index]> };
1400
+ /**
1401
+ * What the author passes to `defineScene`. The layer names come from the keys of `layers` plus
1402
+ * the appended `ui`; `NoInfer` keeps `projections` out of that inference, so a wrong name is
1403
+ * reported on the projection and never widens the layer union. `music` is an asset key of the
1404
+ * game, narrowed to its audio keys once the scanner splits them.
1405
+ *
1406
+ * @example
1407
+ * ```ts
1408
+ * const spec: SceneSpec<{ items: Record<never, never> }, readonly [], string, "board"> = {
1409
+ * bundle: "board",
1410
+ * layers: { items: {} },
1411
+ * projections: []
1412
+ * };
1413
+ * ```
1414
+ */
1415
+ type SceneSpec<Layers extends LayerMap, Projections extends readonly AnySceneProjection[], Asset extends string, Bundle extends string> = {
1416
+ bundle: Bundle;
1417
+ layers: Layers;
1418
+ projections: Projections & CheckedProjections<NoInfer<SceneLayer<Layers>>, NoInfer<Projections>>;
1419
+ music?: Asset;
1420
+ };
1421
+ /**
1422
+ * The signature of `defineScene` bound to the asset and bundle keys of one game.
1423
+ * `defineGame<Types>()` returns it; the loose export accepts any string.
1424
+ *
1425
+ * @example
1426
+ * ```ts
1427
+ * const defineGameScene: DefineScene<"board.music", "board"> = defineScene;
1428
+ * ```
1429
+ */
1430
+ type DefineScene<Asset extends string, Bundle extends string> = <Layers extends LayerMap, const Projections extends readonly AnySceneProjection[]>(id: string, scene: SceneSpec<Layers, Projections, Asset, Bundle>) => SceneDefinition;
1431
+ /**
1432
+ * A scene as `defineScene` returns it: plain frozen data, ready for the `scenes` key of a feature.
1433
+ *
1434
+ * @example
1435
+ * ```ts
1436
+ * // What defineScene("board", { bundle: "board", layers: { items: { sort: "y" } }, projections: [] })
1437
+ * // returns: { id: "board", bundle: "board", music: undefined,
1438
+ * // layers: [{ name: "items", sort: "y" }, { name: "ui", sort: "none" }], projections: [] }
1439
+ * ```
1440
+ */
1441
+ type SceneDefinition = {
1442
+ readonly id: string;
1443
+ readonly bundle: string;
1444
+ readonly music: string | undefined; /** Draw order, bottom first. */
1445
+ readonly layers: readonly LayerSpec[]; /** Names of the projections the scene mounts. */
1446
+ readonly projections: readonly string[];
1447
+ };
1448
+ /**
1449
+ * The one owner every mount of this plugin carries. One is enough: one scene is mounted at a
1450
+ * time, and `unmount` is what flushes.
1451
+ *
1452
+ * @example
1453
+ * ```ts
1454
+ * const owner: SceneOwner = { kind: "plugin", name: "scenes" };
1455
+ * ```
1456
+ */
1457
+ type SceneOwner = Owner & {
1458
+ kind: "plugin";
1459
+ name: "scenes";
1460
+ };
1461
+ /**
1462
+ * scenes plugin config: none. A scene is data in a feature, and a transition effect is V3 work
1463
+ * of `anim`, so a key here would have no reader.
1464
+ *
1465
+ * @example
1466
+ * ```ts
1467
+ * const config: Config = {};
1468
+ * ```
1469
+ */
1470
+ type Config$1 = Record<string, never>;
1471
+ /**
1472
+ * scenes plugin state.
1473
+ */
1474
+ type State$1 = {
1475
+ /** Scene id to its declaration, filled in `onStart` from `flow.features`. */scenes: Map<string, SceneDefinition>; /** Id of the scene that is mounted now. */
1476
+ current: string | undefined; /** Fast mode: the scene a transit node named, applied at the next rest node. */
1477
+ pending: string | undefined;
1478
+ owner: SceneOwner; /** The remover of the `onEnter` callback: set in `onStart`, run in `onStop`. */
1479
+ teardown: (() => void) | undefined;
1480
+ };
1481
+ /**
1482
+ * scenes plugin events.
1483
+ *
1484
+ * @example
1485
+ * ```ts
1486
+ * // A game plugin starts the music of the scene the graph switched to.
1487
+ * createPlugin("music", {
1488
+ * depends: [scenesPlugin],
1489
+ * hooks: ctx => ({ "scenes:changed": ({ music }) => ctx.log.info("scene music", { music }) })
1490
+ * }); // entering the board logs { music: "board.theme" }
1491
+ * ```
1492
+ */
1493
+ type Events = {
1494
+ /** The mounted scene changed. */"scenes:changed": {
1495
+ from: string | undefined;
1496
+ to: string;
1497
+ music: string | undefined;
1498
+ };
1499
+ };
1500
+ /**
1501
+ * scenes plugin API, `app.scenes`. A scene is switched by the graph, never by a call, so the
1502
+ * plugin answers one question and takes no orders.
1503
+ *
1504
+ * @example
1505
+ * ```ts
1506
+ * // A headless test walks from the menu onto the board and asks what is mounted.
1507
+ * await app.flow.walk([{ at: "home", intent: "play" }]);
1508
+ * app.scenes.current(); // "board"
1509
+ * ```
1510
+ */
1511
+ type ScenesApi = {
1512
+ /**
1513
+ * The id of the mounted scene.
1514
+ *
1515
+ * @returns The scene id, `undefined` before the first switch and after the app stopped.
1516
+ * @example
1517
+ * ```ts
1518
+ * // A dev overlay writes the scene next to the frame counter.
1519
+ * app.scenes.current(); // "board", or undefined while the first bundle still loads
1520
+ * ```
1521
+ */
1522
+ current(): string | undefined;
1523
+ };
1524
+ /**
1525
+ * Resolved dependency APIs.
1526
+ */
1527
+ type Deps$1 = {
1528
+ flow: Api$1;
1529
+ world: Api$2;
1530
+ assets: Api$4;
1531
+ time: Api;
1532
+ };
1533
+ /**
1534
+ * What the kernel context offers before the deps are attached.
1535
+ *
1536
+ * `index.ts` writes `events` with an annotated `register` (core spec `14-EVENT-REGISTRATION.md`
1537
+ * row 8), so the own event reaches the context the kernel hands the factories and `emit` is the
1538
+ * kernel's own, scenes-typed one. No member of the context is cast.
1539
+ */
1540
+ type KernelSlice$1 = PluginCtx<Config$1, State$1, Events> & {
1541
+ readonly global: object;
1542
+ readonly log: Log.LogApi;
1543
+ readonly require: Require;
1544
+ };
1545
+ /**
1546
+ * Domain context shared by the files of the plugin.
1547
+ */
1548
+ type ScenesCtx = KernelSlice$1 & {
1549
+ readonly deps: Deps$1;
1550
+ };
1551
+ /**
1552
+ * The mode and the abort signal `flow.onEnter` hands every callback.
1553
+ *
1554
+ * @example
1555
+ * ```ts
1556
+ * const run: RunContext = { mode: "live", signal: new AbortController().signal };
1557
+ * ```
1558
+ */
1559
+ type RunContext = {
1560
+ mode: "live" | "fast";
1561
+ signal: AbortSignal;
1562
+ };
1563
+ declare namespace types_d_exports {
1564
+ export { AudioApi, AudioContextLike, AudioCtx, AudioSessionLike, AudioSessionType, BundleUnloaded, Bus, BusState, Config, Deps, KernelSlice, LifecycleChanged, ModelCommitted, MusicDescriptor, MusicMode, MusicOptions, MusicRequest, MusicSwitch, MusicTrack, RetiringTrack, SceneChanged, SfxRequest, SoundEntry, State, StreamTrack, Volumes };
1565
+ }
1566
+ /**
1567
+ * The three volume buses. `master` carries the whole game, `music` the scene track, `sfx` every
1568
+ * sound a node or a timeline fires.
1569
+ *
1570
+ * @example
1571
+ * ```ts
1572
+ * const bus: Bus = "music";
1573
+ * ```
1574
+ */
1575
+ type Bus = "master" | "music" | "sfx";
1576
+ /**
1577
+ * The part of `AudioContext` the plugin uses. A test passes its own through `config.context`, so
1578
+ * nothing in the unit tests touches a sound card. `state` adds Safari's `"interrupted"`, which a
1579
+ * phone call or the lock screen puts a context into.
1580
+ *
1581
+ * @example
1582
+ * ```ts
1583
+ * // A dev tool builds the engine's context itself and hands it to the plugin.
1584
+ * const context: AudioContextLike = new AudioContext();
1585
+ * createApp({ plugins: [...screen, audioPlugin], pluginConfigs: { audio: { context: () => context } } });
1586
+ * ```
1587
+ */
1588
+ type AudioContextLike = Pick<AudioContext, "destination" | "currentTime" | "createGain" | "createBufferSource" | "decodeAudioData" | "createMediaElementSource" | "resume" | "close"> & {
1589
+ readonly state: AudioContext["state"] | "interrupted";
1590
+ };
1591
+ /**
1592
+ * What `music` accepts besides the key. An absent `fadeMs` means `config.musicFadeMs` at play time.
1593
+ *
1594
+ * @example
1595
+ * ```ts
1596
+ * const options: MusicOptions = { fadeMs: 200 };
1597
+ * ```
1598
+ */
1599
+ type MusicOptions = {
1600
+ fadeMs?: number;
1601
+ };
1602
+ /**
1603
+ * `music` bound to the audio asset keys of one game. `defineGame` returns it through `audioFor`;
1604
+ * the loose export accepts any string.
1605
+ *
1606
+ * @example
1607
+ * ```ts
1608
+ * const gameMusic: MusicDescriptor<"board.theme"> = music;
1609
+ * ```
1610
+ */
1611
+ type MusicDescriptor<Asset extends string> = (key: Asset | null, options?: MusicOptions) => Descriptor;
1612
+ /**
1613
+ * Reads the player's volume choice out of the committed player state. Every bus it names is
1614
+ * applied; the ones it leaves out keep the value they had.
1615
+ *
1616
+ * @example
1617
+ * ```ts
1618
+ * const volumes: Volumes = player => (player as { settings: { audio: { music: number } } }).settings.audio;
1619
+ * ```
1620
+ */
1621
+ type Volumes = (player: Json) => Partial<Record<Bus, number>>;
1622
+ /**
1623
+ * How a music track is played. `"decode"`: one AudioBuffer per track, gapless, about 58 MB per
1624
+ * 150 s at 48 kHz. `"stream"`: an `<audio>` element on a `blob:` URL of the bytes `assets` holds,
1625
+ * about 12 MB, and a gap at every loop: 5 to 49 ms in Chromium, about 0.4 s in WebKit. A tight
1626
+ * loop keeps `"decode"`; a long track that tolerates a seam takes `"stream"`.
1627
+ *
1628
+ * @example
1629
+ * ```ts
1630
+ * const mode: MusicMode = "stream";
1631
+ * ```
1632
+ */
1633
+ type MusicMode = "decode" | "stream";
1634
+ /**
1635
+ * What the page asks iOS for. `"ambient"`: mixes with other apps, the silent switch mutes it.
1636
+ * `"playback"`: stops other apps, plays through the switch. `"auto"`: WebKit picks.
1637
+ *
1638
+ * @example
1639
+ * ```ts
1640
+ * const session: AudioSessionType = "playback";
1641
+ * ```
1642
+ */
1643
+ type AudioSessionType = "ambient" | "playback" | "auto";
1644
+ /**
1645
+ * The part of `navigator.audioSession` the plugin writes. `lib.dom` has no type for it, and only
1646
+ * WebKit has the object at all.
1647
+ *
1648
+ * @example
1649
+ * ```ts
1650
+ * // What Safari answers before the plugin started: no override.
1651
+ * const session: AudioSessionLike = { type: "auto" };
1652
+ * ```
1653
+ */
1654
+ type AudioSessionLike = {
1655
+ type: string;
1656
+ };
1657
+ /**
1658
+ * One music track played by an element at `music: "stream"`: the element, its source node in the
1659
+ * graph, the track gain every level goes through, and the `blob:` URL the element reads.
1660
+ *
1661
+ * @example
1662
+ * ```ts
1663
+ * // `state.music.stream` once "board.theme" plays at `music: "stream"`.
1664
+ * const { element, url }: StreamTrack = track;
1665
+ * element.loop; // true
1666
+ * url.startsWith("blob:"); // true
1667
+ * ```
1668
+ */
1669
+ type StreamTrack = {
1670
+ element: HTMLAudioElement;
1671
+ node: MediaElementAudioSourceNode;
1672
+ gain: GainNode;
1673
+ url: string;
1674
+ };
1675
+ /**
1676
+ * A streamed track that fades out. Its timer frees the element when the fade ends: an element has
1677
+ * no `stop(when)` on the context clock.
1678
+ *
1679
+ * @example
1680
+ * ```ts
1681
+ * // A switch at `musicFadeMs: 600` keeps the old track here for 600 ms.
1682
+ * const entry: RetiringTrack = { stream: oldTrack, timer: setTimeout(free, 600) };
1683
+ * ```
1684
+ */
1685
+ type RetiringTrack = {
1686
+ stream: StreamTrack;
1687
+ timer: ReturnType<typeof setTimeout>;
1688
+ };
1689
+ /**
1690
+ * audio plugin config.
1691
+ *
1692
+ * @example
1693
+ * ```ts
1694
+ * createApp({
1695
+ * plugins: [...screen, audioPlugin],
1696
+ * pluginConfigs: {
1697
+ * audio: { musicFadeMs: 400, music: "stream", session: "playback", volumes: player => player.settings.audio }
1698
+ * }
1699
+ * });
1700
+ * ```
1701
+ */
1702
+ type Config = {
1703
+ /** Start gain of each bus, 0..1. Overridden per commit when `volumes` is set. */buses: {
1704
+ master: number;
1705
+ music: number;
1706
+ sfx: number;
1707
+ }; /** Cross-fade of a music switch, in real milliseconds of the context clock. */
1708
+ musicFadeMs: number; /** Reads the player's choice from the committed player. `undefined`: the buses stay at `buses`. */
1709
+ volumes: Volumes | undefined; /** Context factory, the test seam. `undefined`: `new AudioContext()` when the global exists, otherwise headless. */
1710
+ context: (() => AudioContextLike) | undefined; /** How many started sounds `journal()` keeps, newest last. `0` turns the journal off. */
1711
+ journal: number; /** How music plays. `"stream"` needs MP3 or AAC: WebM Opus is silent through a media element in WebKit (bug 276813). Sounds always decode. */
1712
+ music: MusicMode; /** Set on `navigator.audioSession.type` in `onStart`, before the context exists. No-op where the API is missing (every Chromium, Firefox). */
1713
+ session: AudioSessionType;
1714
+ };
1715
+ /**
1716
+ * One sound that started: a play of an `sfx` or a music track that began. `at` is the elapsed
1717
+ * game time in milliseconds, `app.time.snapshot().elapsed` at the start.
1718
+ *
1719
+ * @example
1720
+ * ```ts
1721
+ * const entry: SoundEntry = { key: "ui.click", bus: "sfx", kind: "sfx", at: 1600 };
1722
+ * ```
1723
+ */
1724
+ type SoundEntry = {
1725
+ readonly key: string;
1726
+ readonly bus: Bus;
1727
+ readonly kind: "sfx" | "music";
1728
+ readonly at: number;
1729
+ };
1730
+ /**
1731
+ * What the plugin keeps for one bus: its gain node while a context exists, the volume a game or
1732
+ * the player chose, and whether it is muted.
1733
+ *
1734
+ * @example
1735
+ * ```ts
1736
+ * const entry: BusState = { gain: undefined, volume: 0.6, muted: false };
1737
+ * ```
1738
+ */
1739
+ type BusState = {
1740
+ gain: GainNode | undefined;
1741
+ volume: number;
1742
+ muted: boolean;
1743
+ };
1744
+ /**
1745
+ * The music that is playing, or the key remembered until the first touch unlocks the context.
1746
+ * `source` plays a decoded track, `stream` a streamed one; they are never both set. Both
1747
+ * `undefined`: the key is only remembered (a locked context, or a refused `play()`).
1748
+ *
1749
+ * @example
1750
+ * ```ts
1751
+ * const track: MusicTrack = { key: "board.theme", gain: undefined, source: undefined, stream: undefined };
1752
+ * ```
1753
+ */
1754
+ type MusicTrack = {
1755
+ key: string;
1756
+ gain: GainNode | undefined;
1757
+ source: AudioBufferSourceNode | undefined;
1758
+ stream: StreamTrack | undefined;
1759
+ };
1760
+ /**
1761
+ * A music switch that waits for its buffer to decode. Its identity is the token: a later request
1762
+ * replaces it, and the replaced switch neither starts nor fades anything when its buffer arrives.
1763
+ *
1764
+ * @example
1765
+ * ```ts
1766
+ * const waiting: MusicSwitch = { key: "ui.theme" };
1767
+ * ```
1768
+ */
1769
+ type MusicSwitch = {
1770
+ key: string;
1771
+ };
1772
+ /**
1773
+ * audio plugin state.
1774
+ */
1775
+ type State = {
1776
+ /** `undefined` means headless: no graph, no sound, every member still keeps its value. */context: AudioContextLike | undefined;
1777
+ buses: Record<Bus, BusState>; /** A lifecycle push is in force: every bus is held at zero. */
1778
+ paused: boolean;
1779
+ unlocked: boolean; /** A gesture called `resume()` and it has not settled yet. */
1780
+ resuming: boolean; /** Sounds fired while `resuming`, one per key; played when the context runs, dropped when it does not. */
1781
+ pendingSfx: Map<string, SfxRequest>; /** Per key, evicted per key on `assets:bundle-unloaded`. */
1782
+ decoded: Map<string, Promise<AudioBuffer>>; /** Asset keys that already got their one warning. */
1783
+ warned: Set<string>; /** `config.volumes` threw once and was reported; it is not reported again this run. */
1784
+ warnedVolumes: boolean;
1785
+ music: MusicTrack | undefined; /** The latest switch still decoding; `undefined` once it started or a later request replaced it. */
1786
+ musicPending: MusicSwitch | undefined; /** Removes the two window listeners of the unlock. */
1787
+ unlock: (() => void) | undefined; /** The removers of the two fx handlers. */
1788
+ removers: Array<() => void>; /** The `navigator.audioSession` that took `config.session`; `stop` writes `"auto"` back to it. */
1789
+ session: AudioSessionLike | undefined; /** Streamed tracks that fade out, each freed by its timer. */
1790
+ retiring: Set<RetiringTrack>;
1791
+ /**
1792
+ * The sounds that started, oldest first, at most `config.journal` of them. Frozen: a write
1793
+ * replaces the whole list, so `journal()` hands it out without a copy.
1794
+ */
1795
+ journal: readonly SoundEntry[];
1796
+ };
1797
+ /**
1798
+ * audio plugin API, `app.audio`. Three buses and the unlock flag: what a game changes about sound
1799
+ * it commits to the player state, which this plugin applies on the next commit.
1800
+ *
1801
+ * @example
1802
+ * ```ts
1803
+ * // A dev overlay pulls the music down and reads the value back.
1804
+ * app.audio.setVolume("music", 0.2);
1805
+ * app.audio.volume("music"); // 0.2
1806
+ * ```
1807
+ */
1808
+ type AudioApi = {
1809
+ /**
1810
+ * Sets the volume of one bus and schedules the new gain at once. The value is clamped to 0..1
1811
+ * and `NaN` becomes 0. Nothing is persisted: a game stores the choice in the player state and
1812
+ * lets `config.volumes` apply it, so the next commit overwrites what this call set.
1813
+ *
1814
+ * @param bus - The bus to change.
1815
+ * @param value - The new volume, 0..1.
1816
+ * @throws {Error} When the bus does not exist.
1817
+ * @example
1818
+ * ```ts
1819
+ * // A test turns the music off before it walks the graph, so nothing rings.
1820
+ * app.audio.setVolume("music", 0);
1821
+ * app.audio.volume("music"); // 0
1822
+ * ```
1823
+ */
1824
+ setVolume(bus: Bus, value: number): void;
1825
+ /**
1826
+ * The stored volume of a bus, never the live gain: a muted or paused bus still answers the
1827
+ * value the player chose.
1828
+ *
1829
+ * @param bus - The bus to read.
1830
+ * @returns The stored volume, 0..1.
1831
+ * @throws {Error} When the bus does not exist.
1832
+ * @example
1833
+ * ```ts
1834
+ * // The settings screen draws the slider of the sfx bus at its stored place.
1835
+ * app.audio.mute("sfx", true);
1836
+ * app.audio.volume("sfx"); // 1: muting does not move the slider
1837
+ * ```
1838
+ */
1839
+ volume(bus: Bus): number;
1840
+ /**
1841
+ * Mutes or unmutes one bus. The flag is independent of the pause: a bus muted here stays muted
1842
+ * when the game comes back from the background.
1843
+ *
1844
+ * @param bus - The bus to change.
1845
+ * @param on - `true` mutes it, `false` lets it play again.
1846
+ * @throws {Error} When the bus does not exist.
1847
+ * @example
1848
+ * ```ts
1849
+ * // A dev overlay silences the whole game while a profiler trace runs.
1850
+ * app.audio.mute("master", true);
1851
+ * app.audio.volume("master"); // 1, and nothing is heard
1852
+ * ```
1853
+ */
1854
+ mute(bus: Bus, on: boolean): void;
1855
+ /**
1856
+ * Tells whether one bus is muted: the stored flag `mute` wrote. A pause does not change it, and
1857
+ * a headless run answers it too.
1858
+ *
1859
+ * @param bus - The bus to read.
1860
+ * @returns True while the bus is muted.
1861
+ * @throws {Error} When the bus does not exist.
1862
+ * @example
1863
+ * ```ts
1864
+ * // The settings screen draws the Sound switch off after the player muted the game.
1865
+ * app.audio.mute("master", true);
1866
+ * app.audio.muted("master"); // true
1867
+ * ```
1868
+ */
1869
+ muted(bus: Bus): boolean;
1870
+ /**
1871
+ * Tells whether the browser let the audio context run. It is false until the first pointer
1872
+ * event on the page resumed it, and false for the whole of a headless run.
1873
+ *
1874
+ * @returns True once the context is running.
1875
+ * @example
1876
+ * ```ts
1877
+ * // A "tap to start" screen stays up until the browser allowed sound.
1878
+ * app.audio.unlocked(); // false before the first touch, true after it
1879
+ * ```
1880
+ */
1881
+ unlocked(): boolean;
1882
+ /**
1883
+ * The sounds that started, oldest first: every play of an `sfx` and every music track that
1884
+ * began, at most `config.journal` of them. A sound dropped before the unlock, a missing file
1885
+ * and a music switch to the track that already plays are not in it. Empty while
1886
+ * `config.journal` is 0, the default, and after the app stopped.
1887
+ *
1888
+ * @returns The journal, frozen; a later sound replaces the list and leaves this one as it was.
1889
+ * @example
1890
+ * ```ts
1891
+ * // A test composed with `pluginConfigs.audio = { journal: 200 }` taps "deliver" after the
1892
+ * // first touch and checks the chime was heard.
1893
+ * app.audio.journal();
1894
+ * // [{ key: "board.theme", bus: "music", kind: "music", at: 0 },
1895
+ * // { key: "orders.complete", bus: "sfx", kind: "sfx", at: 1600 }]
1896
+ * ```
1897
+ */
1898
+ journal(): readonly SoundEntry[];
1899
+ };
1900
+ /**
1901
+ * Resolved dependency APIs.
1902
+ */
1903
+ type Deps = {
1904
+ flow: Api$1;
1905
+ assets: Api$4;
1906
+ model: Api$5;
1907
+ time: Api;
1908
+ };
1909
+ /**
1910
+ * What the kernel context offers before the deps are attached.
1911
+ *
1912
+ * `audio` owns no event, so `emit` is the kernel's and never called here.
1913
+ */
1914
+ type KernelSlice = PluginCtx<Config, State> & {
1915
+ readonly global: object;
1916
+ readonly log: Log.LogApi;
1917
+ readonly require: Require;
1918
+ };
1919
+ /**
1920
+ * Domain context shared by the files of the plugin: the kernel slice plus the resolved deps.
1921
+ */
1922
+ type AudioCtx = KernelSlice & {
1302
1923
  readonly deps: Deps;
1303
1924
  };
1925
+ /**
1926
+ * Payload of the `model:committed` hook: the source of every volume a game sets.
1927
+ */
1928
+ type ModelCommitted = Events$2["model:committed"];
1929
+ /**
1930
+ * Payload of the `scenes:changed` hook: `music` is the track the next scene declared.
1931
+ */
1932
+ type SceneChanged = Events["scenes:changed"];
1933
+ /**
1934
+ * Payload of the `lifecycle:changed` hook: the pause that holds every bus at zero.
1935
+ */
1936
+ type LifecycleChanged = Events$3["lifecycle:changed"];
1937
+ /**
1938
+ * Payload of the `assets:bundle-unloaded` hook: `keys` names every decoded sound that is gone.
1939
+ */
1940
+ type BundleUnloaded = Events$4["assets:bundle-unloaded"];
1941
+ /**
1942
+ * What one play of a sound asks for: the key and the bus the sound goes to.
1943
+ */
1944
+ type SfxRequest = {
1945
+ key: string;
1946
+ bus: string;
1947
+ };
1948
+ /**
1949
+ * What one music switch asks for: the key, or `null` to fade the current track out.
1950
+ */
1951
+ type MusicRequest = {
1952
+ key: string | null;
1953
+ fadeMs: number;
1954
+ };
1304
1955
  //#endregion
1305
- export { UiApi as A, spawned as C, wait as D, use as E, Config$1 as O, spawn as S, tween as T, parallel as _, State as a, set as b, HapticKind as c, HAPTIC_KINDS as d, defineAnimation as f, mark as g, haptic as h, MotionKeyframe as i, types_d_exports$1 as j, State$1 as k, SfxDescriptor as l, frames as m, Config as n, Target as o, external as p, FramesValue as r, types_d_exports as s, AnimApi as t, Step as u, play as v, stagger as w, sfx as x, sequence as y };
1956
+ export { haptic as A, tween as B, HapticKind as C, defineAnimation as D, HAPTIC_KINDS as E, set as F, UiApi as G, wait as H, sfx as I, types_d_exports$3 as K, spawn as L, parallel as M, play as N, external as O, sequence as P, spawned as R, types_d_exports$2 as S, Step as T, Config$3 as U, use as V, State$3 as W, Config$2 as _, SoundEntry as a, State$2 as b, AnySceneProjection as c, SceneDefinition as d, SceneSpec as f, AnimApi as g, types_d_exports$1 as h, MusicOptions as i, mark as j, frames as k, DefineScene as l, State$1 as m, Config as n, State as o, ScenesApi as p, MusicDescriptor as r, types_d_exports as s, AudioApi as t, LayerMap as u, FramesValue as v, SfxDescriptor as w, Target as x, MotionKeyframe as y, stagger as z };