@moku-labs/game 0.2.0 → 0.3.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.
Files changed (35) hide show
  1. package/README.md +2 -0
  2. package/dist/assets.d.mts +181 -22
  3. package/dist/assets.mjs +1186 -160
  4. package/dist/{commands-C0BljsM5.d.mts → commands-DYO22lzi.d.mts} +22 -5
  5. package/dist/control.d.mts +3 -3
  6. package/dist/control.mjs +3 -3
  7. package/dist/{define-CjP_4H0G.d.mts → define-BKWjWgyQ.d.mts} +1 -1
  8. package/dist/dev-CQYJPmcD.mjs +1183 -0
  9. package/dist/flatten-BkXuscAV.mjs +100 -0
  10. package/dist/index.d.mts +85 -31
  11. package/dist/index.mjs +3208 -490
  12. package/dist/{session-DmAxY6Ll.mjs → input-CK4nxBO1.mjs} +41 -12
  13. package/dist/inspect.d.mts +32 -9
  14. package/dist/inspect.mjs +2 -2
  15. package/dist/jsx-dev-runtime.d.mts +1 -1
  16. package/dist/jsx-dev-runtime.mjs +1 -1
  17. package/dist/jsx-runtime.d.mts +1 -1
  18. package/dist/jsx-runtime.mjs +1 -1
  19. package/dist/{run-tlcn1w7y.mjs → run-BfuMLahc.mjs} +489 -213
  20. package/dist/{runtime-Bn5TXNxP.d.mts → runtime-Bhu442L8.d.mts} +1 -1
  21. package/dist/{runtime-DRlwxkIv.mjs → runtime-CBZgWgzx.mjs} +1 -57
  22. package/dist/{sources-BooL2MsX.mjs → sources-C1uGjz4l.mjs} +198 -21
  23. package/dist/testing.d.mts +5 -5
  24. package/dist/testing.mjs +5 -4
  25. package/dist/{types-CxZy7Pi4.d.mts → types-BnWH-75b.d.mts} +95 -21
  26. package/dist/{types-CqkrEY06.d.mts → types-CVUHPvMB.d.mts} +5 -434
  27. package/dist/{types-DmhTHRVW.d.mts → types-DtmFVCHf.d.mts} +1 -1
  28. package/dist/{types-BZ1OLf24.d.mts → types-Dy1xB68L.d.mts} +2318 -1169
  29. package/dist/{types-X9jXjka3.d.mts → types-VErbiEXy.d.mts} +867 -198
  30. package/dist/{visual-NPrDkvUk.mjs → visual-DUvvPPCt.mjs} +51 -1
  31. package/llms.txt +5 -5
  32. package/package.json +5 -5
  33. package/dist/component-DGg5DqKK.mjs +0 -44
  34. package/dist/dev-CcwFrwok.mjs +0 -413
  35. package/dist/types-C47fKNcn.d.mts +0 -695
@@ -1,4 +1,4 @@
1
- import { a as JsxChild, m as UiIntrinsicElements, t as DescriptionNode } from "./types-X9jXjka3.mjs";
1
+ import { a as JsxChild, m as UiIntrinsicElements, t as DescriptionNode } from "./types-VErbiEXy.mjs";
2
2
 
3
3
  //#region src/plugins/ui/jsx/runtime.d.ts
4
4
  /** What the compiler passes as props: the children plus whatever the tag declared. */
@@ -1,60 +1,4 @@
1
- import { n as isComponentDefinition } from "./component-DGg5DqKK.mjs";
2
- //#region src/plugins/ui/jsx/flatten.ts
3
- /** The type of the node `Fragment` builds. It never reaches a reconcile: `flatten` lifts it. */
4
- const FRAGMENT = "#fragment";
5
- /**
6
- * Builds the node a bare string or number child becomes.
7
- *
8
- * @param content - What stood between the tags.
9
- * @returns A text node carrying it.
10
- * @example
11
- * ```ts
12
- * textNode(5); // { type: "text", props: { content: "5" }, children: [] }
13
- * ```
14
- */
15
- function textNode(content) {
16
- return {
17
- type: "text",
18
- props: { content: String(content) },
19
- children: []
20
- };
21
- }
22
- /**
23
- * Tells a description node from the other things a child may be.
24
- *
25
- * @param child - One child of a tag.
26
- * @returns True when it is a node.
27
- * @example
28
- * ```ts
29
- * isNode({ type: "row", props: {}, children: [] }); // true
30
- * ```
31
- */
32
- function isNode(child) {
33
- return typeof child === "object" && child !== null && !Array.isArray(child) && "type" in child;
34
- }
35
- /**
36
- * Turns whatever stood between two tags into the children of that tag.
37
- *
38
- * @param child - One child, an array of children, or nothing.
39
- * @returns The children as nodes, fragments lifted and empty values dropped.
40
- * @example
41
- * ```ts
42
- * flatten(["a", undefined, 2]).map(node => node.props.content); // ["a", "2"]
43
- * ```
44
- */
45
- function flatten(child) {
46
- if (child === null || child === void 0 || typeof child === "boolean") return [];
47
- if (typeof child === "string" || typeof child === "number") return [textNode(child)];
48
- if (Array.isArray(child)) {
49
- const nodes = [];
50
- for (const item of child) nodes.push(...flatten(item));
51
- return nodes;
52
- }
53
- if (!isNode(child)) return [];
54
- if (child.type === "#fragment") return [...child.children];
55
- return [child];
56
- }
57
- //#endregion
1
+ import { i as isComponentDefinition, n as flatten, t as FRAGMENT } from "./flatten-BkXuscAV.mjs";
58
2
  //#region src/plugins/ui/jsx/runtime.ts
59
3
  /**
60
4
  * @file ui/jsx — the JSX runtime itself: the three factories a TypeScript build calls, the
@@ -1,5 +1,5 @@
1
- import { i as scaleByFits } from "./visual-NPrDkvUk.mjs";
2
- import { i as inputOrEmpty, n as isTainted, o as defineSource, t as cheatsOf } from "./session-DmAxY6Ll.mjs";
1
+ import { i as scaleByFits, w as Layer } from "./visual-DUvvPPCt.mjs";
2
+ import { c as defineSource, n as cheatsOf, o as readTarget, r as isTainted, t as inputOrEmpty } from "./input-CK4nxBO1.mjs";
3
3
  //#region src/plugins/flow/doors/read.ts
4
4
  /**
5
5
  * @file flow/doors — `read` and `watch`, the `/inspect` side. Neither changes the game.
@@ -171,9 +171,27 @@ const modelSource = defineSource({
171
171
  //#endregion
172
172
  //#region src/plugins/renderer/inspect.ts
173
173
  /**
174
- * @file renderer plugin — the renderer source of the `/inspect` door: the frame counters.
175
- * Production-safe: it only reads.
174
+ * @file renderer plugin — the renderer sources of the `/inspect` door: the frame counters, and what
175
+ * lies under a point of the page. Production-safe: both only read.
176
+ */
177
+ /**
178
+ * Names one entity under a point: its owner, the projection and key that address it, and the
179
+ * layer its own `Layer` names.
180
+ *
181
+ * @param world - The world API.
182
+ * @param entity - The entity hit.
183
+ * @returns What lies there, or `undefined` for an entity that despawned this frame.
176
184
  */
185
+ function underOf(world, entity) {
186
+ const owner = world.ecs.ownerOf(entity);
187
+ if (owner === void 0) return void 0;
188
+ return {
189
+ entity,
190
+ owner,
191
+ key: world.projection.keyOf(entity),
192
+ layer: world.ecs.get(entity, Layer)?.name
193
+ };
194
+ }
177
195
  /**
178
196
  * The counters of the renderer: frames per second, frame work, GPU textures and their memory,
179
197
  * views and pooled display objects, render passes, and in a dev build the draw calls.
@@ -192,11 +210,44 @@ const renderSource = defineSource({
192
210
  changes: "frame",
193
211
  read: (app) => app.renderer.stats()
194
212
  });
213
+ /**
214
+ * What lies under a point of the page, topmost first: the point in client CSS pixels, the
215
+ * coordinates `game.locate` answers and Playwright clicks, mapped through
216
+ * `viewport.toReference`, and every view whose hit box holds it, with the skip rules of a tap.
217
+ * `[]` while the renderer is inert and where nothing is drawn.
218
+ *
219
+ * @example
220
+ * ```ts
221
+ * // An agent asks what lies under the pointer on a 390 px wide phone: a shine over a board cell.
222
+ * read(app, sources.at, { x: 195, y: 108.33 });
223
+ * // [{ entity: 1048577, owner: { kind: "plugin", name: "fx" }, key: undefined, layer: undefined },
224
+ * // { entity: 1048576, owner: { kind: "projection", name: "board.items" },
225
+ * // key: { projection: "board.items", key: "c7" }, layer: "items" }]
226
+ * ```
227
+ */
228
+ const atSource = defineSource({
229
+ id: "game.at",
230
+ title: "Under a point",
231
+ input: {
232
+ x: "number",
233
+ y: "number"
234
+ },
235
+ changes: "frame",
236
+ read: (app, { x, y }) => {
237
+ const point = app.renderer.viewport.toReference(x, y);
238
+ const found = [];
239
+ for (const entity of app.renderer.sync.hitAll(point.x, point.y)) {
240
+ const under = underOf(app.world, entity);
241
+ if (under !== void 0) found.push(under);
242
+ }
243
+ return found;
244
+ }
245
+ });
195
246
  //#endregion
196
247
  //#region src/plugins/ui/inspect.ts
197
248
  /**
198
- * @file ui plugin — the ui sources of the `/inspect` door: the live screen as data and the rect
199
- * of one element on the page. Production-safe: both only read.
249
+ * @file ui plugin — the ui sources of the `/inspect` door: the live screen as data, and where an
250
+ * element or a view is on the page. Production-safe: every one only reads.
200
251
  */
201
252
  /**
202
253
  * Finds a keyed node and the nodes above it.
@@ -253,6 +304,37 @@ function onPage(renderer, rect) {
253
304
  };
254
305
  }
255
306
  /**
307
+ * Where a live ui element is on the page: its layout box, scaled by every fitted element above
308
+ * it, through the viewport.
309
+ *
310
+ * @param app - The app with ui and renderer.
311
+ * @param key - The `key` prop of the element.
312
+ * @returns The rect in CSS px, or `undefined` when no live element has the key.
313
+ */
314
+ function locateKey(app, key) {
315
+ const chain = app.ui.find(key) !== void 0 ? chainTo(app.ui.tree(), key) : void 0;
316
+ return chain === void 0 ? void 0 : onPage(app.renderer, drawnRect(chain));
317
+ }
318
+ /**
319
+ * Where a view is on the page: the box the renderer draws for the entity of its projection key,
320
+ * through the viewport.
321
+ *
322
+ * @param app - The app with ui, renderer and world.
323
+ * @param target - The projection key of the view.
324
+ * @returns The rect in CSS px, or `undefined` when the view is not live or has no drawn box.
325
+ */
326
+ function locateTarget(app, target) {
327
+ const entity = app.world.projection.entityOf(target.projection, target.key);
328
+ const bounds = entity === void 0 ? void 0 : app.renderer.sync.boundsOf(entity);
329
+ if (bounds === void 0) return void 0;
330
+ return onPage(app.renderer, {
331
+ x: bounds.x,
332
+ y: bounds.y,
333
+ w: bounds.width,
334
+ h: bounds.height
335
+ });
336
+ }
337
+ /**
256
338
  * The live screen as plain data: every root, every element with its natural rect, its style,
257
339
  * its state flags and the fit scale of a fitted one.
258
340
  *
@@ -270,32 +352,42 @@ const uiSource = defineSource({
270
352
  read: (app) => app.ui.tree()
271
353
  });
272
354
  /**
273
- * Where an element is on the page, in CSS px: its layout box, so a bare button with no fill has
274
- * one, scaled by every fitted element above it, through the viewport. In reference units while
275
- * the renderer is inert.
355
+ * Where something is on the page, in CSS px, by either of the two addresses the doors use: a ui
356
+ * element by its `key` (its layout box, scaled by every fitted element above it, so a bare button
357
+ * with no fill has one), or a view by its `target` projection key (the box the renderer draws for
358
+ * it). Both go through the viewport, so the numbers are what Playwright clicks. In reference units
359
+ * while the renderer is inert; a view has no drawn box then.
276
360
  *
277
361
  * @example
278
362
  * ```ts
279
363
  * // An e2e script clicks Home's Play plank on a 390 px wide phone.
280
- * read(app, sources.rect, { key: "play" }); // { x: 101.9, y: 469.2, w: 189.2, h: 54.6 }
281
- * read(app, sources.rect, { key: "nothing" }); // undefined: not on screen
364
+ * read(app, sources.locate, { key: "play" }); // { x: 101.9, y: 469.2, w: 189.2, h: 54.6 }
365
+ * // An agent finds the coin c7 before it drags it: 64 x 64 u, drawn at (540, 300).
366
+ * read(app, sources.locate, { target: { projection: "board.items", key: "c7" } });
367
+ * // { x: 183.4, y: 96.8, w: 23.1, h: 23.1 }; undefined while nothing draws it
282
368
  * ```
283
369
  */
284
- const rectSource = defineSource({
285
- id: "game.rect",
286
- title: "Element rect",
287
- input: { key: "string" },
370
+ const locateSource = defineSource({
371
+ id: "game.locate",
372
+ title: "Locate",
373
+ input: {
374
+ key: "string?",
375
+ target: "json?"
376
+ },
288
377
  changes: "frame",
289
- read: (app, { key }) => {
290
- const chain = app.ui.find(key) !== void 0 ? chainTo(app.ui.tree(), key) : void 0;
291
- return chain === void 0 ? void 0 : onPage(app.renderer, drawnRect(chain));
378
+ read: (app, { key, target }) => {
379
+ if (key !== void 0 && target === void 0) return locateKey(app, key);
380
+ if (target !== void 0 && key === void 0) return locateTarget(app, readTarget(target));
381
+ throw new Error("[game] game.locate takes a key or a target.\n Pass exactly one of { key } and { target }.");
292
382
  }
293
383
  });
294
384
  //#endregion
295
385
  //#region src/plugins/world/inspect.ts
296
386
  /**
297
387
  * @file world plugin — the world sources of the `/inspect` door: the entities, filtered by owner
298
- * and component, and the projection keys. Production-safe: every source only reads.
388
+ * and component, the projection keys, one entity in full, what changed between two frames and
389
+ * the component schema. Every source only reads; `game.diff` answers in a dev build only, because
390
+ * only a dev build keeps the frame history.
299
391
  */
300
392
  /**
301
393
  * Tells whether an entity carries a component, as a JSON value or as a skipped one.
@@ -356,6 +448,87 @@ const projectionsSource = defineSource({
356
448
  return keys;
357
449
  }
358
450
  });
451
+ /**
452
+ * One entity in full: its owner, its projection key, every JSON component, the names of the
453
+ * skipped ones and the components a motion still drives. `undefined` for a stale id. The source
454
+ * reads the whole snapshot once per read and picks the row.
455
+ *
456
+ * @example
457
+ * ```ts
458
+ * // The editor explains item i7 while it slides to its new cell.
459
+ * read(app, sources.explain, { entity: 1048577 });
460
+ * // { id: 1048577, owner: { kind: "projection", name: "board.items" },
461
+ * // key: { projection: "board.items", key: "i7" },
462
+ * // components: { Transform: { x: 64, y: 0, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } },
463
+ * // Layer: { name: "items" } }, skipped: [], motions: ["Transform"] }
464
+ * read(app, sources.explain, { entity: 42 }); // undefined: a stale id
465
+ * ```
466
+ */
467
+ const explainSource = defineSource({
468
+ id: "game.explain",
469
+ title: "Entity",
470
+ input: { entity: "number" },
471
+ changes: "frame",
472
+ read: (app, { entity }) => {
473
+ const row = app.world.ecs.snapshot().entities.find((candidate) => candidate.id === entity);
474
+ if (row === void 0) return void 0;
475
+ return {
476
+ id: row.id,
477
+ owner: row.owner,
478
+ key: app.world.projection.keyOf(row.id),
479
+ components: row.components,
480
+ skipped: row.skipped,
481
+ motions: app.world.projection.motionsOf(row.id)
482
+ };
483
+ }
484
+ });
485
+ /**
486
+ * What changed between the world at the end of frame `from` and at the end of frame `to`, from
487
+ * the frame history of a dev build: one entry per entity, sorted by id. Throws in a production
488
+ * build and for a frame outside the last 120.
489
+ *
490
+ * @example
491
+ * ```ts
492
+ * // The editor asks what the last second of play changed: item i7 merged up to level 3.
493
+ * read(app, sources.diff, { from: 120, to: 180 });
494
+ * // { from: 120, to: 180, entities: [{ id: 1048577,
495
+ * // owner: { kind: "projection", name: "board.items" },
496
+ * // key: { projection: "board.items", key: "i7" }, change: "changed",
497
+ * // components: { Level: { from: { level: 2 }, to: { level: 3 } } } }] }
498
+ * ```
499
+ */
500
+ const diffSource = defineSource({
501
+ id: "game.diff",
502
+ title: "Frame diff",
503
+ input: {
504
+ from: "number",
505
+ to: "number"
506
+ },
507
+ changes: "frame",
508
+ read: (app, { from, to }) => app.world.ecs.diff(from, to)
509
+ });
510
+ /**
511
+ * Every component and tag type the world met so far, sorted by name, with the JSON kind of each
512
+ * field: the editor's component palette. A system may write a new component on any frame, so it
513
+ * is read again every frame.
514
+ *
515
+ * @example
516
+ * ```ts
517
+ * // The editor's component palette, in a world that has met Transform and the tag Held.
518
+ * read(app, sources.schema);
519
+ * // [{ name: "Held", kind: "tag", json: true, fields: {}, defaults: true, owned: [] },
520
+ * // { name: "Transform", kind: "component", json: true,
521
+ * // fields: { x: "number", y: "number", rotation: "number", scale: "number", pivot: "object" },
522
+ * // defaults: { x: 0, y: 0, rotation: 0, scale: 1, pivot: { x: 0, y: 0 } }, owned: [] }]
523
+ * ```
524
+ */
525
+ const schemaSource = defineSource({
526
+ id: "game.schema",
527
+ title: "Components",
528
+ input: {},
529
+ changes: "frame",
530
+ read: (app) => app.world.ecs.schema()
531
+ });
359
532
  //#endregion
360
533
  //#region src/plugins/flow/inspect.ts
361
534
  /** The levels of `Log.LogLevel`, least severe first, as the names a caller passes. */
@@ -534,12 +707,16 @@ const sources = Object.freeze({
534
707
  entities: entitiesSource,
535
708
  projections: projectionsSource,
536
709
  ui: uiSource,
537
- rect: rectSource,
710
+ locate: locateSource,
538
711
  render: renderSource,
539
712
  sounds: soundsSource,
540
713
  effects: effectsSource,
541
714
  assets: assetsSource,
542
- log: logSource
715
+ log: logSource,
716
+ explain: explainSource,
717
+ diff: diffSource,
718
+ schema: schemaSource,
719
+ at: atSource
543
720
  });
544
721
  //#endregion
545
722
  export { read as n, watch as r, sources as t };
@@ -1,8 +1,8 @@
1
- import { At as Json, Dt as Api$2, Ft as PlayerStateProvider, It as ProviderCall, Lt as SaveDoc, Ut as Api$3, Zt as FakeClock, h as Api$1, t as Api } from "./types-BZ1OLf24.mjs";
2
- import { _ as stepFrames, a as InputOf, d as HeadlessApp, f as HeadlessGame, g as runRepro, h as createHeadless, m as ReproResult, p as Repro, r as ControlApp } from "./types-DmhTHRVW.mjs";
3
- import { H as InputApi } from "./types-X9jXjka3.mjs";
4
- import { A as UiApi, t as AnimApi } from "./types-CqkrEY06.mjs";
5
- import { t as commands } from "./commands-C0BljsM5.mjs";
1
+ import { $t as SaveDoc, Kt as Json, Qt as ProviderCall, Ut as Api$2, Zt as PlayerStateProvider, an as Api$3, k as Api$1, m as Api, pn as FakeClock } from "./types-Dy1xB68L.mjs";
2
+ import { _ as stepFrames, a as InputOf, d as HeadlessApp, f as HeadlessGame, g as runRepro, h as createHeadless, m as ReproResult, p as Repro, r as ControlApp } from "./types-DtmFVCHf.mjs";
3
+ import { H as InputApi } from "./types-VErbiEXy.mjs";
4
+ import { A as UiApi, t as AnimApi } from "./types-CVUHPvMB.mjs";
5
+ import { t as commands } from "./commands-DYO22lzi.mjs";
6
6
 
7
7
  //#region src/plugins/clock/fake.d.ts
8
8
  /**
package/dist/testing.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { n as saveOf, t as memory } from "./memory-CvgdnsQO.mjs";
2
- import { a as stepFrames, i as runRepro, n as commands, r as createHeadless, t as run } from "./run-tlcn1w7y.mjs";
3
- import { n as read, t as sources } from "./sources-BooL2MsX.mjs";
2
+ import { a as stepFrames, i as runRepro, n as commands, r as createHeadless, t as run } from "./run-BfuMLahc.mjs";
3
+ import { n as read, t as sources } from "./sources-C1uGjz4l.mjs";
4
4
  import { createBrandConsole } from "@moku-labs/common/cli";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
@@ -1242,7 +1242,7 @@ async function pageCheckpoint(argument) {
1242
1242
  await pageTick(handles);
1243
1243
  await pageTick(handles);
1244
1244
  const captured = await game.renderer.capture();
1245
- const screen = captured === void 0 ? void 0 : await pageWebp(captured);
1245
+ const screen = captured === void 0 ? void 0 : await pageWebp(captured.png);
1246
1246
  return {
1247
1247
  ok: true,
1248
1248
  value: {
@@ -1385,7 +1385,8 @@ async function pageEncode(pixels) {
1385
1385
  return `data:image/webp;base64,${btoa(binary)}`;
1386
1386
  }
1387
1387
  /**
1388
- * Turns the PNG data URL of `renderer.capture()` into the lossless WebP of the baselines.
1388
+ * Turns the PNG data URL of `renderer.capture()`, its `png`, into the lossless WebP of the
1389
+ * baselines.
1389
1390
  *
1390
1391
  * @param url - The PNG data URL.
1391
1392
  * @returns The WebP data URL of the same pixels.
@@ -1,6 +1,5 @@
1
- import { At as Json, C as LayerSpec, Dt as Api$4, F as ComponentHandle, Gt as Events$2, H as Owner, I as ComponentType, R as Entity, S as LayerSort, St as Api$3, a as PixiSprite, d as FilterSlot, h as Api$1, i as PixiModule, kt as Events$1, nn as Require, o as PixiTexture, p as Point, q as Api, r as PixiFilter, t as Api$2, vt as Descriptor } from "./types-BZ1OLf24.mjs";
2
- import { o as Events$3, t as Api$5 } from "./types-C47fKNcn.mjs";
3
- import { t as AnimApi } from "./types-CqkrEY06.mjs";
1
+ import { D as Point, E as FilterSlot, Gt as Events$2, K as Owner, Kt as Json, L as ComponentHandle, Pt as Descriptor, Rt as Api$4, Ut as Api$5, V as Entity, _ as PixiModule, et as LayerSort, g as PixiFilter, k as Api$1, m as Api$2, o as Events$1, sn as Events$3, t as Api$3, tt as LayerSpec, ut as Api, v as PixiSprite, vn as Require, y as PixiTexture, z as ComponentType } from "./types-Dy1xB68L.mjs";
2
+ import { t as AnimApi } from "./types-CVUHPvMB.mjs";
4
3
  import { Log } from "@moku-labs/common/browser";
5
4
  import { PluginCtx } from "@moku-labs/core";
6
5
 
@@ -623,7 +622,7 @@ type Deps$2 = {
623
622
  flow: Api;
624
623
  world: Api$1;
625
624
  renderer: Api$2;
626
- assets: Api$5;
625
+ assets: Api$3;
627
626
  anim: AnimApi;
628
627
  };
629
628
  /**
@@ -644,7 +643,7 @@ type EffectsCtx = KernelSlice$2 & {
644
643
  /**
645
644
  * Payload of the `assets:bundle-unloaded` hook: `keys` names every texture that is gone.
646
645
  */
647
- type BundleUnloaded$1 = Events$3["assets:bundle-unloaded"];
646
+ type BundleUnloaded$1 = Events$1["assets:bundle-unloaded"];
648
647
  declare namespace types_d_exports$1 {
649
648
  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 };
650
649
  }
@@ -871,8 +870,8 @@ type ScenesApi = {
871
870
  type Deps$1 = {
872
871
  flow: Api;
873
872
  world: Api$1;
874
- assets: Api$5;
875
- time: Api$3;
873
+ assets: Api$3;
874
+ time: Api$4;
876
875
  };
877
876
  /**
878
877
  * What the kernel context offers before the deps are attached.
@@ -905,7 +904,7 @@ type RunContext = {
905
904
  signal: AbortSignal;
906
905
  };
907
906
  declare namespace types_d_exports {
908
- export { AudioApi, AudioContextLike, AudioCtx, BundleUnloaded, Bus, BusState, Config, Deps, KernelSlice, LifecycleChanged, ModelCommitted, MusicDescriptor, MusicOptions, MusicRequest, MusicSwitch, MusicTrack, SceneChanged, SfxRequest, SoundEntry, State, Volumes };
907
+ 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 };
909
908
  }
910
909
  /**
911
910
  * The three volume buses. `master` carries the whole game, `music` the scene track, `sfx` every
@@ -929,7 +928,7 @@ type Bus = "master" | "music" | "sfx";
929
928
  * createApp({ plugins: [...screen, audioPlugin], pluginConfigs: { audio: { context: () => context } } });
930
929
  * ```
931
930
  */
932
- type AudioContextLike = Pick<AudioContext, "destination" | "currentTime" | "createGain" | "createBufferSource" | "decodeAudioData" | "resume" | "close"> & {
931
+ type AudioContextLike = Pick<AudioContext, "destination" | "currentTime" | "createGain" | "createBufferSource" | "decodeAudioData" | "createMediaElementSource" | "resume" | "close"> & {
933
932
  readonly state: AudioContext["state"] | "interrupted";
934
933
  };
935
934
  /**
@@ -963,6 +962,73 @@ type MusicDescriptor<Asset extends string> = (key: Asset | null, options?: Music
963
962
  * ```
964
963
  */
965
964
  type Volumes = (player: Json) => Partial<Record<Bus, number>>;
965
+ /**
966
+ * How a music track is played. `"decode"`: one AudioBuffer per track, gapless, about 58 MB per
967
+ * 150 s at 48 kHz. `"stream"`: an `<audio>` element on a `blob:` URL of the bytes `assets` holds,
968
+ * about 12 MB, and a gap at every loop: 5 to 49 ms in Chromium, about 0.4 s in WebKit. A tight
969
+ * loop keeps `"decode"`; a long track that tolerates a seam takes `"stream"`.
970
+ *
971
+ * @example
972
+ * ```ts
973
+ * const mode: MusicMode = "stream";
974
+ * ```
975
+ */
976
+ type MusicMode = "decode" | "stream";
977
+ /**
978
+ * What the page asks iOS for. `"ambient"`: mixes with other apps, the silent switch mutes it.
979
+ * `"playback"`: stops other apps, plays through the switch. `"auto"`: WebKit picks.
980
+ *
981
+ * @example
982
+ * ```ts
983
+ * const session: AudioSessionType = "playback";
984
+ * ```
985
+ */
986
+ type AudioSessionType = "ambient" | "playback" | "auto";
987
+ /**
988
+ * The part of `navigator.audioSession` the plugin writes. `lib.dom` has no type for it, and only
989
+ * WebKit has the object at all.
990
+ *
991
+ * @example
992
+ * ```ts
993
+ * // What Safari answers before the plugin started: no override.
994
+ * const session: AudioSessionLike = { type: "auto" };
995
+ * ```
996
+ */
997
+ type AudioSessionLike = {
998
+ type: string;
999
+ };
1000
+ /**
1001
+ * One music track played by an element at `music: "stream"`: the element, its source node in the
1002
+ * graph, the track gain every level goes through, and the `blob:` URL the element reads.
1003
+ *
1004
+ * @example
1005
+ * ```ts
1006
+ * // `state.music.stream` once "board.theme" plays at `music: "stream"`.
1007
+ * const { element, url }: StreamTrack = track;
1008
+ * element.loop; // true
1009
+ * url.startsWith("blob:"); // true
1010
+ * ```
1011
+ */
1012
+ type StreamTrack = {
1013
+ element: HTMLAudioElement;
1014
+ node: MediaElementAudioSourceNode;
1015
+ gain: GainNode;
1016
+ url: string;
1017
+ };
1018
+ /**
1019
+ * A streamed track that fades out. Its timer frees the element when the fade ends: an element has
1020
+ * no `stop(when)` on the context clock.
1021
+ *
1022
+ * @example
1023
+ * ```ts
1024
+ * // A switch at `musicFadeMs: 600` keeps the old track here for 600 ms.
1025
+ * const entry: RetiringTrack = { stream: oldTrack, timer: setTimeout(free, 600) };
1026
+ * ```
1027
+ */
1028
+ type RetiringTrack = {
1029
+ stream: StreamTrack;
1030
+ timer: ReturnType<typeof setTimeout>;
1031
+ };
966
1032
  /**
967
1033
  * audio plugin config.
968
1034
  *
@@ -970,7 +1036,9 @@ type Volumes = (player: Json) => Partial<Record<Bus, number>>;
970
1036
  * ```ts
971
1037
  * createApp({
972
1038
  * plugins: [...screen, audioPlugin],
973
- * pluginConfigs: { audio: { musicFadeMs: 400, volumes: player => player.settings.audio } }
1039
+ * pluginConfigs: {
1040
+ * audio: { musicFadeMs: 400, music: "stream", session: "playback", volumes: player => player.settings.audio }
1041
+ * }
974
1042
  * });
975
1043
  * ```
976
1044
  */
@@ -983,7 +1051,9 @@ type Config = {
983
1051
  musicFadeMs: number; /** Reads the player's choice from the committed player. `undefined`: the buses stay at `buses`. */
984
1052
  volumes: Volumes | undefined; /** Context factory, the test seam. `undefined`: `new AudioContext()` when the global exists, otherwise headless. */
985
1053
  context: (() => AudioContextLike) | undefined; /** How many started sounds `journal()` keeps, newest last. `0` turns the journal off. */
986
- journal: number;
1054
+ 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. */
1055
+ music: MusicMode; /** Set on `navigator.audioSession.type` in `onStart`, before the context exists. No-op where the API is missing (every Chromium, Firefox). */
1056
+ session: AudioSessionType;
987
1057
  };
988
1058
  /**
989
1059
  * One sound that started: a play of an `sfx` or a music track that began. `at` is the elapsed
@@ -1016,17 +1086,19 @@ type BusState = {
1016
1086
  };
1017
1087
  /**
1018
1088
  * The music that is playing, or the key remembered until the first touch unlocks the context.
1019
- * `source` and `gain` are `undefined` exactly while the track is only remembered.
1089
+ * `source` plays a decoded track, `stream` a streamed one; they are never both set. Both
1090
+ * `undefined`: the key is only remembered (a locked context, or a refused `play()`).
1020
1091
  *
1021
1092
  * @example
1022
1093
  * ```ts
1023
- * const track: MusicTrack = { key: "board.theme", source: undefined, gain: undefined };
1094
+ * const track: MusicTrack = { key: "board.theme", gain: undefined, source: undefined, stream: undefined };
1024
1095
  * ```
1025
1096
  */
1026
1097
  type MusicTrack = {
1027
1098
  key: string;
1028
- source: AudioBufferSourceNode | undefined;
1029
1099
  gain: GainNode | undefined;
1100
+ source: AudioBufferSourceNode | undefined;
1101
+ stream: StreamTrack | undefined;
1030
1102
  };
1031
1103
  /**
1032
1104
  * A music switch that waits for its buffer to decode. Its identity is the token: a later request
@@ -1056,7 +1128,9 @@ type State = {
1056
1128
  music: MusicTrack | undefined; /** The latest switch still decoding; `undefined` once it started or a later request replaced it. */
1057
1129
  musicPending: MusicSwitch | undefined; /** Removes the two window listeners of the unlock. */
1058
1130
  unlock: (() => void) | undefined; /** The removers of the two fx handlers. */
1059
- removers: Array<() => void>;
1131
+ removers: Array<() => void>; /** The `navigator.audioSession` that took `config.session`; `stop` writes `"auto"` back to it. */
1132
+ session: AudioSessionLike | undefined; /** Streamed tracks that fade out, each freed by its timer. */
1133
+ retiring: Set<RetiringTrack>;
1060
1134
  /**
1061
1135
  * The sounds that started, oldest first, at most `config.journal` of them. Frozen: a write
1062
1136
  * replaces the whole list, so `journal()` hands it out without a copy.
@@ -1156,9 +1230,9 @@ type AudioApi = {
1156
1230
  */
1157
1231
  type Deps = {
1158
1232
  flow: Api;
1159
- assets: Api$5;
1160
- model: Api$4;
1161
- time: Api$3;
1233
+ assets: Api$3;
1234
+ model: Api$5;
1235
+ time: Api$4;
1162
1236
  };
1163
1237
  /**
1164
1238
  * What the kernel context offers before the deps are attached.
@@ -1179,7 +1253,7 @@ type AudioCtx = KernelSlice & {
1179
1253
  /**
1180
1254
  * Payload of the `model:committed` hook: the source of every volume a game sets.
1181
1255
  */
1182
- type ModelCommitted = Events$1["model:committed"];
1256
+ type ModelCommitted = Events$2["model:committed"];
1183
1257
  /**
1184
1258
  * Payload of the `scenes:changed` hook: `music` is the track the next scene declared.
1185
1259
  */
@@ -1187,11 +1261,11 @@ type SceneChanged = Events["scenes:changed"];
1187
1261
  /**
1188
1262
  * Payload of the `lifecycle:changed` hook: the pause that holds every bus at zero.
1189
1263
  */
1190
- type LifecycleChanged = Events$2["lifecycle:changed"];
1264
+ type LifecycleChanged = Events$3["lifecycle:changed"];
1191
1265
  /**
1192
1266
  * Payload of the `assets:bundle-unloaded` hook: `keys` names every decoded sound that is gone.
1193
1267
  */
1194
- type BundleUnloaded = Events$3["assets:bundle-unloaded"];
1268
+ type BundleUnloaded = Events$1["assets:bundle-unloaded"];
1195
1269
  /**
1196
1270
  * What one play of a sound asks for: the key and the bus the sound goes to.
1197
1271
  */