@simpludify/web 0.1.5 → 0.1.7

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./style.css";
2
- import { ActionController, Aspect, Camera, CanvasButton as CanvasButton$1, CanvasUI, Engine, EngineError, EventBus, FixedStepRunner, GeometryTilemapLayer, Lifetime, PointerRouting, Renderable, ReplayPlayer, Scene, ScenePointer, SceneState, Shape, TilemapLayer, Transform, VirtualInput, canvasControlText, canvasHoverHighlight, canvasOpacity, canvasScrollbars, canvasSliderGeometry, cleanupAll, containsScreenPoint, createViewTransform, defineEvent, imageSize, intersectUI, isKeyCode, positive, screenPoint, screenToWorld, spriteFrameBounds, worldToScreen, wrapCanvasText } from "@simpludify/core";
2
+ import { ActionController, Aspect, Camera, CanvasButton as CanvasButton$1, CanvasUI, Engine, EngineError, EventBus, FixedStepRunner, GamepadAxis, GamepadBindings, GamepadButton, GeometryTilemapLayer, InputCapture, InputCaptureState, InputDevice, Lifetime, MouseButton, PointerRouting, Renderable, ReplayPlayer, Scene, ScenePointer, SceneState, Shape, TilemapLayer, Transform, VirtualInput, canvasControlText, canvasHoverHighlight, canvasOpacity, canvasScrollbars, canvasSliderGeometry, checkedGamepadBindings, cleanupAll, containsScreenPoint, createViewTransform, defineEvent, gamepadDeadZone, gamepadIndex, imageSize, intersectUI, isKeyCode, isMouseButton, positive, screenPoint, screenToWorld, spriteFrameBounds, worldToScreen, wrapCanvasText } from "@simpludify/core";
3
3
  import { Application, CanvasTextMetrics, Container, Graphics, Rectangle, RendererType, Sprite, Text, TextStyle, Texture } from "pixi.js";
4
4
  export * from "@simpludify/core";
5
5
  // packages/web/src/canvas-node.ts
@@ -17,135 +17,6 @@ function CanvasButton(text, click, style = {}) {
17
17
  return click(signal);
18
18
  }, style);
19
19
  }
20
- // packages/web/src/gamepad-bindings.ts
21
- /** Standard mapping physical positions, not product-specific printed labels. */
22
- var GamepadButton = Object.freeze({
23
- South: 0,
24
- East: 1,
25
- West: 2,
26
- North: 3,
27
- LeftShoulder: 4,
28
- RightShoulder: 5,
29
- LeftTrigger: 6,
30
- RightTrigger: 7,
31
- Select: 8,
32
- Start: 9,
33
- LeftStick: 10,
34
- RightStick: 11,
35
- DPadUp: 12,
36
- DPadDown: 13,
37
- DPadLeft: 14,
38
- DPadRight: 15,
39
- Home: 16
40
- });
41
- var GamepadAxis = Object.freeze({
42
- LeftX: 0,
43
- LeftY: 1,
44
- RightX: 2,
45
- RightY: 3
46
- });
47
- var axis = (index) => Object.freeze({ axis: index });
48
- /** Ready-to-use data; game Actions retain application-defined names. */
49
- var GamepadBindings = Object.freeze({
50
- LeftStick: Object.freeze({
51
- x: axis(0),
52
- y: axis(1)
53
- }),
54
- RightStick: Object.freeze({
55
- x: axis(2),
56
- y: axis(3)
57
- }),
58
- DPad: Object.freeze({
59
- x: Object.freeze({
60
- negative: Object.freeze([14]),
61
- positive: Object.freeze([15])
62
- }),
63
- y: Object.freeze({
64
- negative: Object.freeze([12]),
65
- positive: Object.freeze([13])
66
- })
67
- }),
68
- Move: Object.freeze({
69
- x: Object.freeze({
70
- axis: 0,
71
- negative: Object.freeze([14]),
72
- positive: Object.freeze([15])
73
- }),
74
- y: Object.freeze({
75
- axis: 1,
76
- negative: Object.freeze([12]),
77
- positive: Object.freeze([13])
78
- })
79
- })
80
- });
81
- var fail = () => {
82
- throw new EngineError("INVALID_GAMEPAD_BINDING", "Invalid gamepad binding");
83
- };
84
- function gamepadIndex(value) {
85
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return fail();
86
- return value;
87
- }
88
- function gamepadDeadZone(value) {
89
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value >= 1) return fail();
90
- return value;
91
- }
92
- function record(value, keys) {
93
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((k) => !keys.includes(k))) return fail();
94
- return Object.fromEntries(Object.entries(value));
95
- }
96
- function axisBinding(value) {
97
- const v = record(value, [
98
- "axis",
99
- "invert",
100
- "negative",
101
- "positive",
102
- "deadZone"
103
- ]);
104
- const indices = (value) => {
105
- if (value === void 0) return Object.freeze([]);
106
- if (!Array.isArray(value)) return fail();
107
- return Object.freeze([...new Set(value.map(gamepadIndex))]);
108
- };
109
- if (v.invert !== void 0 && typeof v.invert !== "boolean") return fail();
110
- return Object.freeze({
111
- ...v.axis === void 0 ? {} : { axis: gamepadIndex(v.axis) },
112
- invert: v.invert ?? false,
113
- negative: indices(v.negative),
114
- positive: indices(v.positive),
115
- ...v.deadZone === void 0 ? {} : { deadZone: gamepadDeadZone(v.deadZone) }
116
- });
117
- }
118
- function checkedGamepadBindings(actions, value) {
119
- const v = record(value, Object.keys(actions));
120
- return Object.freeze(Object.entries(v).map(([name, binding]) => {
121
- if (!Object.hasOwn(actions, name)) return fail();
122
- const action = actions[name];
123
- if (!action) return fail();
124
- let result;
125
- if (action.kind === "button") {
126
- const b = record(binding, ["buttons"]);
127
- if (b.buttons !== void 0 && !Array.isArray(b.buttons)) return fail();
128
- result = Object.freeze({ buttons: Object.freeze((b.buttons ?? []).map((item) => {
129
- if (typeof item === "number") return gamepadIndex(item);
130
- const b = record(item, ["button", "threshold"]), threshold = b.threshold ?? .5;
131
- if (typeof threshold !== "number" || !Number.isFinite(threshold) || threshold <= 0 || threshold > 1) return fail();
132
- return Object.freeze({
133
- button: gamepadIndex(b.button),
134
- threshold
135
- });
136
- })) });
137
- } else if (action.kind === "axis1d") result = axisBinding(binding);
138
- else {
139
- const keys = action.kind === "axis2d" ? ["x", "y"] : [
140
- "x",
141
- "y",
142
- "z"
143
- ], b = record(binding, keys);
144
- result = Object.freeze(Object.fromEntries(keys.map((k) => [k, axisBinding(b[k] ?? {})])));
145
- }
146
- return Object.freeze([action, result]);
147
- }));
148
- }
149
20
  // packages/web/src/gamepad-layout.ts
150
21
  var positions = Object.fromEntries(Object.entries(GamepadButton).map(([name, index]) => [index, name]));
151
22
  var faces = Object.freeze({
@@ -230,6 +101,9 @@ var GamepadSource = class {
230
101
  signature;
231
102
  zone;
232
103
  mapping;
104
+ following;
105
+ bindingRevision;
106
+ waiting = /* @__PURE__ */ new Set();
233
107
  constructor(input, controller, options) {
234
108
  this.input = input;
235
109
  this.controller = controller;
@@ -238,7 +112,9 @@ var GamepadSource = class {
238
112
  this.zone = gamepadDeadZone(options.deadZone ?? .15);
239
113
  this.mapping = options.mapping ?? "standard";
240
114
  if (this.mapping !== "standard" && this.mapping !== "raw") throw new EngineError("INVALID_GAMEPAD_BINDING", "Unknown gamepad mapping policy");
241
- this.bindings = checkedGamepadBindings(controller.actions, options.bindings);
115
+ this.following = options.bindings === void 0;
116
+ this.bindingRevision = controller.bindingRevision;
117
+ this.bindings = this.following ? this.commonBindings() : checkedGamepadBindings(controller.actions, options.bindings);
242
118
  this.source = controller.createSource();
243
119
  this.life.onDispose(() => this.source.dispose());
244
120
  this.life.onDispose(controller.onDispose(() => this.dispose()));
@@ -277,19 +153,42 @@ var GamepadSource = class {
277
153
  }
278
154
  rebind(bindings) {
279
155
  this.alive();
280
- const checked = checkedGamepadBindings(this.controller.actions, bindings);
156
+ const checked = bindings === void 0 ? this.commonBindings() : checkedGamepadBindings(this.controller.actions, bindings);
281
157
  this.clear();
282
158
  this.bindings = checked;
159
+ this.following = bindings === void 0;
160
+ this.bindingRevision = this.controller.bindingRevision;
161
+ }
162
+ commonBindings() {
163
+ return Object.freeze(Object.values(this.controller.actions).flatMap((action) => {
164
+ const binding = this.controller.getBinding(action).gamepad;
165
+ return binding ? [Object.freeze([action, binding])] : [];
166
+ }));
167
+ }
168
+ syncBindings() {
169
+ if (!this.following || this.bindingRevision === this.controller.bindingRevision) return;
170
+ const next = this.commonBindings();
171
+ const before = new Map(this.bindings), after = new Map(next);
172
+ for (const action of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) {
173
+ if (JSON.stringify(before.get(action)) === JSON.stringify(after.get(action))) continue;
174
+ this.source.resetBinding(action);
175
+ if (after.has(action)) this.waiting.add(action);
176
+ else this.waiting.delete(action);
177
+ }
178
+ this.bindings = next;
179
+ this.bindingRevision = this.controller.bindingRevision;
283
180
  }
284
181
  clear() {
285
182
  this.source.clear();
286
183
  this.armed = false;
184
+ this.waiting.clear();
287
185
  }
288
186
  alive() {
289
187
  if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Gamepad source has been disposed");
290
188
  }
291
189
  /** @internal */ sample(pad, allowed) {
292
190
  if (this.disposed) return;
191
+ this.syncBindings();
293
192
  if (!allowed || !this.enabled || !this.controller.active || !pad || this.mapping === "standard" && pad.mapping !== "standard") {
294
193
  this.clear();
295
194
  this.signature = void 0;
@@ -310,7 +209,13 @@ var GamepadSource = class {
310
209
  if (next.every(([, value]) => neutral(value))) this.armed = true;
311
210
  return;
312
211
  }
313
- for (const [action, value] of next) this.source.set(action, value);
212
+ for (const [action, value] of next) {
213
+ if (this.waiting.has(action)) {
214
+ if (!neutral(value)) continue;
215
+ this.waiting.delete(action);
216
+ }
217
+ this.source.set(action, value);
218
+ }
314
219
  }
315
220
  /** @internal */ onDispose(callback) {
316
221
  return this.life.onDispose(callback);
@@ -331,6 +236,7 @@ var GamepadInput = class {
331
236
  readable = false;
332
237
  poll;
333
238
  active;
239
+ observe;
334
240
  constructor(input, options = {}) {
335
241
  this.input = input;
336
242
  this.life.onDispose(() => this.changes.dispose());
@@ -338,6 +244,7 @@ var GamepadInput = class {
338
244
  throw new Error("Gamepad API unavailable");
339
245
  })());
340
246
  this.active = options.active ?? (() => true);
247
+ this.observe = options.observe;
341
248
  this.life.onDispose(input.addAdapter(this));
342
249
  const events = options.events ?? (typeof window === "undefined" ? void 0 : window), abort = new AbortController();
343
250
  this.life.onDispose(() => abort.abort());
@@ -412,7 +319,8 @@ var GamepadInput = class {
412
319
  axisCount: p.axes.length,
413
320
  inferredLayout: inferredGamepadLayout(p.id)
414
321
  })));
415
- const allowed = this.active();
322
+ this.observe?.(pads);
323
+ const allowed = this.active() && this.input.liveInput;
416
324
  for (const source of this.sources) if (source.disposed) this.sources.delete(source);
417
325
  else source.sample(pads.get(source.index), allowed);
418
326
  this.publish(listed, wasReadable !== this.readable);
@@ -520,35 +428,287 @@ var BrowserInput = class {
520
428
  this.clear();
521
429
  }
522
430
  };
431
+ // packages/web/src/input-capture.ts
432
+ var mouseMasks$1 = [
433
+ [0, 1],
434
+ [1, 4],
435
+ [2, 2],
436
+ [3, 8],
437
+ [4, 16]
438
+ ];
439
+ /** @internal One Game-owned observer; existing adapters retain gameplay ownership. */
440
+ var InputCaptureManager = class {
441
+ host;
442
+ input;
443
+ scene;
444
+ releasePointers;
445
+ report;
446
+ abort = new AbortController();
447
+ current;
448
+ keys = /* @__PURE__ */ new Set();
449
+ blockedKeys = /* @__PURE__ */ new Set();
450
+ blockedPointers = /* @__PURE__ */ new Set();
451
+ blockedAuxiliary = /* @__PURE__ */ new Set();
452
+ mouseButtons = 0;
453
+ needsPadSample = false;
454
+ pads = /* @__PURE__ */ new Map();
455
+ readyPads = /* @__PURE__ */ new Set();
456
+ dead = false;
457
+ constructor(host, input, scene, releasePointers, report) {
458
+ this.host = host;
459
+ this.input = input;
460
+ this.scene = scene;
461
+ this.releasePointers = releasePointers;
462
+ this.report = report;
463
+ const options = {
464
+ capture: true,
465
+ signal: this.abort.signal
466
+ };
467
+ window.addEventListener("keydown", (event) => this.guard(() => this.keyDown(event)), options);
468
+ window.addEventListener("keyup", (event) => this.guard(() => {
469
+ if (isKeyCode(event.code)) {
470
+ this.keys.delete(event.code);
471
+ this.blockedKeys.delete(event.code);
472
+ }
473
+ this.arm();
474
+ }), options);
475
+ for (const kind of [
476
+ "pointerdown",
477
+ "pointermove",
478
+ "pointerup",
479
+ "pointercancel"
480
+ ]) window.addEventListener(kind, (event) => this.guard(() => this.pointer(event)), options);
481
+ for (const kind of ["auxclick", "contextmenu"]) window.addEventListener(kind, (event) => {
482
+ if (this.surface(event.target) && isMouseButton(event.button) && this.blockedAuxiliary.has(event.button)) {
483
+ this.consume(event);
484
+ if (kind === "auxclick") this.blockedAuxiliary.delete(event.button);
485
+ }
486
+ }, options);
487
+ window.addEventListener("blur", (event) => {
488
+ if (event.target === window) this.interrupt();
489
+ }, options);
490
+ document.addEventListener("visibilitychange", () => {
491
+ if (document.hidden) this.interrupt();
492
+ }, options);
493
+ host.addEventListener("focusout", (event) => {
494
+ if (!(event.relatedTarget instanceof Node) || !host.contains(event.relatedTarget) || blocksGameKeyboard(event.relatedTarget)) this.interrupt();
495
+ }, options);
496
+ host.addEventListener("focusin", (event) => {
497
+ if (blocksGameKeyboard(event.target)) this.interrupt();
498
+ }, options);
499
+ }
500
+ start(options = {}) {
501
+ if (this.dead) throw new EngineError("GAME_DISPOSED", "Capture host has been disposed");
502
+ if (this.current) throw new EngineError("INPUT_CAPTURE_ACTIVE", "Cancel the active capture before starting another");
503
+ const scene = this.scene();
504
+ if (scene.state !== SceneState.Active || document.hidden || !document.hasFocus()) throw new EngineError("INPUT_CAPTURE_UNAVAILABLE", "Capture requires an active Scene and focused visible document");
505
+ const capture = new InputCapture(options);
506
+ let release;
507
+ try {
508
+ capture.onDispose(scene.onDispose(() => capture.cancel()));
509
+ capture.onDispose(() => {
510
+ if (this.current === capture) this.current = void 0;
511
+ this.pads.clear();
512
+ this.readyPads.clear();
513
+ this.needsPadSample = false;
514
+ release?.();
515
+ });
516
+ this.current = capture;
517
+ this.needsPadSample = capture.devices.includes(InputDevice.Gamepad);
518
+ for (const key of this.keys) this.blockedKeys.add(key);
519
+ for (const [button, mask] of mouseMasks$1) if (this.mouseButtons & mask) this.blockedAuxiliary.add(button);
520
+ this.releasePointers();
521
+ if (capture.disposed) return capture;
522
+ release = this.input.suspendLiveInput(() => capture.cancel());
523
+ if (capture.disposed) {
524
+ release();
525
+ return capture;
526
+ }
527
+ this.host.focus({ preventScroll: true });
528
+ this.arm();
529
+ return capture;
530
+ } catch (error) {
531
+ capture.cancel();
532
+ throw error;
533
+ }
534
+ }
535
+ arm() {
536
+ const capture = this.current;
537
+ if (!capture || capture.state !== InputCaptureState.Arming || this.needsPadSample) return;
538
+ if (capture.devices.includes(InputDevice.Keyboard) && this.keys.size) return;
539
+ if (capture.devices.includes(InputDevice.Mouse) && this.mouseButtons) return;
540
+ if (capture.devices.includes(InputDevice.Gamepad) && [...this.pads.values()].some((pad) => (capture.gamepadIndex === void 0 || capture.gamepadIndex === pad.index) && pad.buttons.size)) return;
541
+ capture.arm();
542
+ }
543
+ keyDown(event) {
544
+ if (event.isComposing || event.keyCode === 229 || !isKeyCode(event.code)) return;
545
+ const previous = this.keys.has(event.code);
546
+ this.keys.add(event.code);
547
+ if (!(event.target instanceof Node) || !this.host.contains(event.target)) return;
548
+ if (blocksGameKeyboard(event.target)) return;
549
+ if (event.target instanceof HTMLElement && event.target.closest("button")) return;
550
+ const capture = this.current;
551
+ if (!capture && !this.blockedKeys.has(event.code)) return;
552
+ this.blockedKeys.add(event.code);
553
+ this.consume(event);
554
+ if (capture && !previous && !event.repeat) capture.accept({
555
+ device: InputDevice.Keyboard,
556
+ code: event.code
557
+ });
558
+ }
559
+ surface(target) {
560
+ return target === this.host || target instanceof HTMLCanvasElement && target.parentElement === this.host;
561
+ }
562
+ pointer(event) {
563
+ const capture = this.current, mouse = event.pointerType === "mouse", old = this.mouseButtons;
564
+ if (mouse) this.mouseButtons = event.type === "pointercancel" ? 0 : event.buttons;
565
+ if (event.type === "pointerdown" && !capture) {
566
+ this.blockedPointers.delete(event.pointerId);
567
+ if (isMouseButton(event.button)) this.blockedAuxiliary.delete(event.button);
568
+ }
569
+ const blocked = this.blockedPointers.has(event.pointerId);
570
+ if (capture && this.surface(event.target)) {
571
+ if (event.type === "pointerdown") this.host.focus({ preventScroll: true });
572
+ this.blockedPointers.add(event.pointerId);
573
+ this.consume(event);
574
+ if (mouse && isMouseButton(event.button)) {
575
+ const mask = mouseMasks$1.find(([button]) => button === event.button)?.[1] ?? 0;
576
+ if (event.buttons & mask && !(old & mask)) {
577
+ this.blockedAuxiliary.add(event.button);
578
+ capture.accept({
579
+ device: InputDevice.Mouse,
580
+ button: event.button
581
+ });
582
+ }
583
+ }
584
+ } else if (blocked) this.consume(event);
585
+ if (event.type === "pointerup" || event.type === "pointercancel" || mouse && event.buttons === 0) this.blockedPointers.delete(event.pointerId);
586
+ this.arm();
587
+ }
588
+ /** Same validated gamepad poll as gameplay; no navigator polling in this manager. */
589
+ sample(pads) {
590
+ const capture = this.current;
591
+ if (!capture?.devices.includes(InputDevice.Gamepad)) return;
592
+ const before = this.pads, first = this.needsPadSample, next = /* @__PURE__ */ new Map();
593
+ for (const pad of pads.values()) {
594
+ const buttons = /* @__PURE__ */ new Set();
595
+ pad.buttons.forEach((button, index) => {
596
+ if (Number.isFinite(button?.value) && button.value >= .5) buttons.add(index);
597
+ });
598
+ const signature = JSON.stringify([
599
+ pad.id,
600
+ pad.mapping,
601
+ pad.buttons.length
602
+ ]);
603
+ const entry = {
604
+ index: pad.index,
605
+ id: pad.id,
606
+ mapping: pad.mapping,
607
+ signature,
608
+ buttons
609
+ };
610
+ next.set(pad.index, entry);
611
+ if (first || before.get(pad.index)?.signature !== signature) this.readyPads.delete(pad.index);
612
+ if (!buttons.size) this.readyPads.add(pad.index);
613
+ }
614
+ for (const index of this.readyPads) if (!next.has(index)) this.readyPads.delete(index);
615
+ this.pads = next;
616
+ this.needsPadSample = false;
617
+ this.arm();
618
+ if (first || capture.state !== InputCaptureState.Listening) return;
619
+ for (const pad of [...next.values()].sort((a, b) => a.index - b.index)) {
620
+ if (!this.readyPads.has(pad.index) || before.get(pad.index)?.signature !== pad.signature) continue;
621
+ for (const button of pad.buttons) if (!before.get(pad.index)?.buttons.has(button) && capture.accept({
622
+ device: InputDevice.Gamepad,
623
+ index: pad.index,
624
+ id: pad.id,
625
+ mapping: pad.mapping,
626
+ button
627
+ })) return;
628
+ }
629
+ }
630
+ consume(event) {
631
+ event.preventDefault();
632
+ event.stopImmediatePropagation();
633
+ }
634
+ guard(action) {
635
+ try {
636
+ action();
637
+ } catch (error) {
638
+ this.report(error);
639
+ }
640
+ }
641
+ interrupt() {
642
+ this.guard(() => this.current?.cancel());
643
+ this.keys.clear();
644
+ this.blockedKeys.clear();
645
+ this.blockedPointers.clear();
646
+ this.blockedAuxiliary.clear();
647
+ this.mouseButtons = 0;
648
+ }
649
+ dispose() {
650
+ if (this.dead) return;
651
+ this.dead = true;
652
+ this.abort.abort();
653
+ this.interrupt();
654
+ }
655
+ };
523
656
  // packages/web/src/pointer.ts
657
+ var mouseMasks = [
658
+ [MouseButton.Left, 1],
659
+ [MouseButton.Middle, 4],
660
+ [MouseButton.Right, 2],
661
+ [MouseButton.Back, 8],
662
+ [MouseButton.Forward, 16]
663
+ ];
524
664
  /** One DOM capture-phase adapter per Game; Pixi federated events never route these widgets. */
525
665
  var BrowserPointer = class {
526
666
  host;
527
667
  project;
528
668
  report;
529
669
  flush;
670
+ input;
530
671
  routing;
531
672
  abort = new AbortController();
532
- constructor(host, project, report, flush) {
673
+ mouse;
674
+ mouseStates = /* @__PURE__ */ new Map();
675
+ blockedMouse = /* @__PURE__ */ new Set();
676
+ auxiliary = /* @__PURE__ */ new Set();
677
+ offInput;
678
+ constructor(host, project, report, flush, input) {
533
679
  this.host = host;
534
680
  this.project = project;
535
681
  this.report = report;
536
682
  this.flush = flush;
683
+ this.input = input;
537
684
  this.routing = new PointerRouting((id) => {
538
685
  if (host.hasPointerCapture(id)) host.releasePointerCapture(id);
539
686
  });
687
+ this.mouse = input?.createMouseSource();
688
+ this.offInput = input?.addAdapter({
689
+ sample: () => {},
690
+ reset: () => this.resetMouse(),
691
+ dispose: () => this.dispose()
692
+ });
540
693
  const options = {
541
694
  signal: this.abort.signal,
542
695
  capture: true
543
696
  };
544
697
  host.addEventListener("pointerdown", (e) => this.guard(() => {
545
698
  if (!this.surface(e.target)) return;
699
+ if (e.pointerType === "mouse") {
700
+ this.releaseMouse(e.pointerId);
701
+ this.blockedMouse.delete(e.pointerId);
702
+ if (isMouseButton(e.button)) this.auxiliary.delete(e.button);
703
+ }
546
704
  this.flush();
547
705
  const data = this.data(e);
706
+ if (!data) return this.release();
548
707
  try {
549
708
  host.setPointerCapture(e.pointerId);
550
709
  } catch {}
551
710
  const target = this.routing.down(data);
711
+ this.updateMouse(e);
552
712
  if (this.routing.has(e.pointerId)) {
553
713
  e.preventDefault();
554
714
  if (target) focusGameSurface(host);
@@ -561,7 +721,9 @@ var BrowserPointer = class {
561
721
  host.addEventListener("wheel", (e) => this.guard(() => {
562
722
  if (!this.surface(e.target)) return;
563
723
  this.flush();
564
- const { screen } = this.project(e);
724
+ const projected = this.project(e);
725
+ if (!projected) return;
726
+ const { screen } = projected;
565
727
  const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? host.clientHeight : 1;
566
728
  if (this.routing.scroll(screen.x, screen.y, e.deltaX * unit, e.deltaY * unit)) e.preventDefault();
567
729
  }), {
@@ -570,37 +732,98 @@ var BrowserPointer = class {
570
732
  });
571
733
  window.addEventListener("pointermove", (e) => this.guard(() => {
572
734
  if (!this.routing.owns(e.pointerId) && !this.surface(e.target)) {
573
- if (this.routing.hasHover(e.pointerId)) this.routing.leave(this.data(e));
735
+ if (e.pointerType === "mouse") this.releaseMouse(e.pointerId);
736
+ if (this.routing.hasHover(e.pointerId)) {
737
+ const data = this.data(e);
738
+ if (data) this.routing.leave(data);
739
+ else this.release();
740
+ }
574
741
  return;
575
742
  }
576
743
  this.flush();
577
- this.routing.move(this.data(e));
744
+ const data = this.data(e);
745
+ if (!data) return this.release();
746
+ this.updateMouse(e);
747
+ this.routing.move(data);
578
748
  }), options);
579
749
  window.addEventListener("pointerup", (e) => this.guard(() => {
750
+ if (e.pointerType === "mouse") this.releaseMouse(e.pointerId);
580
751
  if (this.routing.owns(e.pointerId)) {
581
752
  e.preventDefault();
582
753
  this.flush();
583
- this.routing.up(this.data(e));
754
+ const data = this.data(e);
755
+ if (data) this.routing.up(data);
756
+ else this.release();
584
757
  }
585
758
  }), options);
586
- window.addEventListener("pointercancel", (e) => this.guard(() => this.routing.cancel(e.pointerId, false)), options);
587
- host.addEventListener("lostpointercapture", (e) => this.guard(() => this.routing.cancel(e.pointerId)), options);
588
- host.addEventListener("pointerleave", (e) => this.guard(() => this.routing.leave(this.data(e))), options);
759
+ window.addEventListener("pointercancel", (e) => this.guard(() => {
760
+ this.releaseMouse(e.pointerId);
761
+ this.blockedMouse.delete(e.pointerId);
762
+ this.routing.cancel(e.pointerId, false);
763
+ }), options);
764
+ host.addEventListener("lostpointercapture", (e) => this.guard(() => {
765
+ this.releaseMouse(e.pointerId);
766
+ if (e.pointerType === "mouse" && e.buttons !== 0) this.blockedMouse.add(e.pointerId);
767
+ else this.blockedMouse.delete(e.pointerId);
768
+ this.routing.cancel(e.pointerId);
769
+ }), options);
770
+ host.addEventListener("pointerleave", (e) => this.guard(() => {
771
+ const data = this.data(e);
772
+ if (data) this.routing.leave(data);
773
+ else this.release();
774
+ }), options);
589
775
  window.addEventListener("blur", (event) => {
590
776
  if (event.target === window) this.release();
591
777
  }, options);
592
778
  host.addEventListener("focusout", (event) => this.guard(() => {
593
- if (!(event.relatedTarget instanceof Node) || !host.contains(event.relatedTarget) || blocksGameKeyboard(event.relatedTarget)) this.routing.releaseVirtual();
779
+ if (!(event.relatedTarget instanceof Node) || !host.contains(event.relatedTarget) || blocksGameKeyboard(event.relatedTarget)) {
780
+ this.resetMouse();
781
+ this.routing.releaseVirtual();
782
+ }
594
783
  }), options);
595
784
  document.addEventListener("visibilitychange", () => {
596
785
  if (document.hidden) this.release();
597
786
  }, options);
787
+ for (const kind of ["auxclick", "contextmenu"]) host.addEventListener(kind, (event) => {
788
+ if (this.surface(event.target) && isMouseButton(event.button) && this.auxiliary.has(event.button) && input?.acceptsMouse(event.button)) {
789
+ event.preventDefault();
790
+ if (kind === "auxclick") this.auxiliary.delete(event.button);
791
+ }
792
+ }, options);
793
+ }
794
+ applyMouse() {
795
+ let buttons = 0;
796
+ for (const state of this.mouseStates.values()) buttons |= state;
797
+ for (const [button, mask] of mouseMasks) this.mouse?.set(button, (buttons & mask) !== 0);
798
+ }
799
+ releaseMouse(id) {
800
+ if (this.mouseStates.delete(id)) this.applyMouse();
801
+ }
802
+ updateMouse(event) {
803
+ if (event.pointerType !== "mouse" || !this.mouse) return;
804
+ if (event.buttons === 0) this.blockedMouse.delete(event.pointerId);
805
+ const world = this.routing.worldOwns(event.pointerId) && !this.blockedMouse.has(event.pointerId) && this.input?.liveInput;
806
+ if (world) this.mouseStates.set(event.pointerId, event.buttons);
807
+ else this.mouseStates.delete(event.pointerId);
808
+ if (isMouseButton(event.button)) {
809
+ if (world && this.input?.acceptsMouse(event.button)) this.auxiliary.add(event.button);
810
+ else this.auxiliary.delete(event.button);
811
+ }
812
+ this.applyMouse();
813
+ }
814
+ resetMouse() {
815
+ for (const id of this.mouseStates.keys()) this.blockedMouse.add(id);
816
+ this.mouseStates.clear();
817
+ this.auxiliary.clear();
818
+ this.mouse?.clear();
598
819
  }
599
820
  surface(target) {
600
821
  return target === this.host || target instanceof HTMLCanvasElement && target.parentElement === this.host;
601
822
  }
602
823
  data(e) {
603
- const { screen, world, view } = this.project(e);
824
+ const projected = this.project(e);
825
+ if (!projected) return void 0;
826
+ const { screen, world, view } = projected;
604
827
  return Object.freeze({
605
828
  pointerId: e.pointerId,
606
829
  pointerType: e.pointerType,
@@ -620,10 +843,15 @@ var BrowserPointer = class {
620
843
  }
621
844
  }
622
845
  release() {
846
+ this.resetMouse();
623
847
  this.guard(() => this.routing.release());
624
848
  }
625
849
  dispose() {
626
850
  this.abort.abort();
851
+ this.offInput?.();
852
+ this.resetMouse();
853
+ this.mouse?.dispose();
854
+ this.blockedMouse.clear();
627
855
  this.guard(() => this.routing.dispose());
628
856
  }
629
857
  };
@@ -1063,6 +1291,7 @@ var CanvasVisual = class {
1063
1291
  },
1064
1292
  wheel: (event) => {
1065
1293
  const rect = this.host.getBoundingClientRect();
1294
+ if (rect.width <= 0 || rect.height <= 0) return;
1066
1295
  const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? this.host.clientHeight : 1;
1067
1296
  if (this.routing.scroll((event.clientX - rect.x) * this.host.clientWidth / rect.width, (event.clientY - rect.y) * this.host.clientHeight / rect.height, event.deltaX * unit, event.deltaY * unit)) event.preventDefault();
1068
1297
  }
@@ -1536,11 +1765,11 @@ var GeometryTilemapVisual = class {
1536
1765
  g.clear().poly(this.map.geometry.vertices(x, y).flatMap((p) => [p.x, p.y]), true).fill(color);
1537
1766
  this.stats.updatedTiles++;
1538
1767
  }
1539
- render(view) {
1768
+ render(view, enabled = true) {
1540
1769
  if (this.disposed) return;
1541
1770
  const m = this.map;
1542
1771
  this.display.position.set(m.x, m.y);
1543
- this.display.visible = m.visible && m.enabled;
1772
+ this.display.visible = enabled && m.visible && m.enabled;
1544
1773
  if (!this.display.visible) {
1545
1774
  this.invalid = true;
1546
1775
  this.bulk = true;
@@ -1775,7 +2004,7 @@ var TilemapVisual = class {
1775
2004
  invalid = true;
1776
2005
  off;
1777
2006
  offDispose;
1778
- constructor(map, scene, acquire, changed, onError, released) {
2007
+ constructor(map, isCurrent, acquire, changed, onError, released) {
1779
2008
  this.map = map;
1780
2009
  this.changed = changed;
1781
2010
  this.onError = onError;
@@ -1793,7 +2022,7 @@ var TilemapVisual = class {
1793
2022
  const controller = this.controller;
1794
2023
  if (!controller) throw Error("Missing tilemap request controller");
1795
2024
  acquire(map.sheet.url, controller.signal).then((lease) => {
1796
- if (this.disposed || map.disposed || map.scene !== scene) {
2025
+ if (this.disposed || map.disposed || !isCurrent()) {
1797
2026
  lease.release();
1798
2027
  return;
1799
2028
  }
@@ -1855,12 +2084,12 @@ var TilemapVisual = class {
1855
2084
  sprite.height = this.map.tileHeight;
1856
2085
  this.stats.updatedTiles++;
1857
2086
  }
1858
- render(view) {
2087
+ render(view, enabled = true) {
1859
2088
  if (this.disposed) return;
1860
2089
  this.sync();
1861
2090
  const m = this.map;
1862
2091
  this.display.position.set(m.x, m.y);
1863
- this.display.visible = m.visible && m.enabled;
2092
+ this.display.visible = enabled && m.visible && m.enabled;
1864
2093
  if (!this.display.visible) {
1865
2094
  this.invalid = true;
1866
2095
  return;
@@ -2260,6 +2489,7 @@ var PixiRenderer = class {
2260
2489
  root.mask = mask;
2261
2490
  const visuals = /* @__PURE__ */ new Map();
2262
2491
  const tilemaps = /* @__PURE__ */ new Map();
2492
+ const geometryTilemaps = /* @__PURE__ */ new Map();
2263
2493
  const drawOrder = [];
2264
2494
  let depthDirty = true;
2265
2495
  let revision = -1, disposed = false, clipX = NaN, clipY = NaN, clipWidth = NaN, clipHeight = NaN, removeSceneCleanup;
@@ -2459,20 +2689,30 @@ var PixiRenderer = class {
2459
2689
  entry.dispose();
2460
2690
  visuals.delete(actor);
2461
2691
  }
2462
- for (const [actor, entry] of tilemaps) if (actor.disposed || actor.scene !== scene || !(entry instanceof TilemapVisual ? actor.get(TilemapLayer.type) : actor.get(GeometryTilemapLayer.type)) || entry.disposed) {
2463
- entry.dispose();
2464
- if (entry instanceof TilemapVisual) this.tilemaps.delete(entry);
2465
- else this.geometryTilemaps.delete(entry);
2466
- tilemaps.delete(actor);
2692
+ for (const [actor, entry] of tilemaps) {
2693
+ const layer = actor.get(TilemapLayer.type);
2694
+ if (actor.disposed || actor.scene !== scene || !layer || layer.disposed || layer.map !== entry.map || entry.disposed) {
2695
+ entry.dispose();
2696
+ this.tilemaps.delete(entry);
2697
+ tilemaps.delete(actor);
2698
+ }
2699
+ }
2700
+ for (const [actor, entry] of geometryTilemaps) {
2701
+ const layer = actor.get(GeometryTilemapLayer.type);
2702
+ if (actor.disposed || actor.scene !== scene || !layer || layer.disposed || layer.map !== entry.map || entry.disposed) {
2703
+ entry.dispose();
2704
+ this.geometryTilemaps.delete(entry);
2705
+ geometryTilemaps.delete(actor);
2706
+ }
2467
2707
  }
2468
2708
  let order = 0;
2469
2709
  for (const actor of scene.actors) {
2470
2710
  const layer = actor.get(TilemapLayer.type);
2471
- if (layer) {
2711
+ if (layer && !layer.disposed && !layer.map.disposed) {
2472
2712
  let tile = tilemaps.get(actor);
2473
2713
  if (!tile) {
2474
- tile = new TilemapVisual(layer.map, scene, (url, signal) => this.acquireImage(url, signal), changed, this.onError, () => {
2475
- if (tile instanceof TilemapVisual) this.tilemaps.delete(tile);
2714
+ tile = new TilemapVisual(layer.map, () => actor.scene === scene && actor.get(TilemapLayer.type)?.map === layer.map, (url, signal) => this.acquireImage(url, signal), changed, this.onError, () => {
2715
+ if (tile) this.tilemaps.delete(tile);
2476
2716
  depthDirty = true;
2477
2717
  });
2478
2718
  world.addChild(tile.display);
@@ -2482,8 +2722,8 @@ var PixiRenderer = class {
2482
2722
  tile.order = order;
2483
2723
  }
2484
2724
  const geometryLayer = actor.get(GeometryTilemapLayer.type);
2485
- if (geometryLayer) {
2486
- let tile = tilemaps.get(actor);
2725
+ if (geometryLayer && !geometryLayer.disposed && !geometryLayer.map.disposed) {
2726
+ let tile = geometryTilemaps.get(actor);
2487
2727
  if (!tile) {
2488
2728
  const created = new GeometryTilemapVisual(geometryLayer.map, () => {
2489
2729
  this.geometryTilemaps.delete(created);
@@ -2491,7 +2731,7 @@ var PixiRenderer = class {
2491
2731
  });
2492
2732
  tile = created;
2493
2733
  world.addChild(tile.display);
2494
- tilemaps.set(actor, tile);
2734
+ geometryTilemaps.set(actor, tile);
2495
2735
  this.geometryTilemaps.add(created);
2496
2736
  }
2497
2737
  tile.order = order;
@@ -2511,7 +2751,7 @@ var PixiRenderer = class {
2511
2751
  const sync = () => {
2512
2752
  reconcile();
2513
2753
  for (const entry of visuals.values()) update(entry);
2514
- for (const entry of tilemaps.values()) if (entry instanceof TilemapVisual) entry.sync();
2754
+ for (const entry of tilemaps.values()) entry.sync();
2515
2755
  };
2516
2756
  const prepared = {
2517
2757
  ready: async () => {
@@ -2524,7 +2764,6 @@ var PixiRenderer = class {
2524
2764
  if (entry.controller) pending = true;
2525
2765
  }
2526
2766
  for (const entry of tilemaps.values()) {
2527
- if (!(entry instanceof TilemapVisual)) continue;
2528
2767
  if (entry.error) throw entry.error;
2529
2768
  if (entry.controller) pending = true;
2530
2769
  }
@@ -2566,8 +2805,15 @@ var PixiRenderer = class {
2566
2805
  entry.display.scale.set(scaleX, scaleY);
2567
2806
  }
2568
2807
  }
2569
- for (const entry of tilemaps.values()) {
2570
- entry.render(view);
2808
+ for (const [actor, entry] of tilemaps) {
2809
+ entry.render(view, actor.enabled && actor.get(TilemapLayer.type)?.enabled === true);
2810
+ if (entry.depth !== entry.map.zIndex) {
2811
+ entry.depth = entry.map.zIndex;
2812
+ depthDirty = true;
2813
+ }
2814
+ }
2815
+ for (const [actor, entry] of geometryTilemaps) {
2816
+ entry.render(view, actor.enabled && actor.get(GeometryTilemapLayer.type)?.enabled === true);
2571
2817
  if (entry.depth !== entry.map.zIndex) {
2572
2818
  entry.depth = entry.map.zIndex;
2573
2819
  depthDirty = true;
@@ -2585,6 +2831,11 @@ var PixiRenderer = class {
2585
2831
  order: entry.order,
2586
2832
  zIndex: entry.map.zIndex
2587
2833
  });
2834
+ for (const entry of geometryTilemaps.values()) drawOrder.push({
2835
+ display: entry.display,
2836
+ order: entry.order,
2837
+ zIndex: entry.map.zIndex
2838
+ });
2588
2839
  drawOrder.sort((a, b) => a.zIndex - b.zIndex || a.order - b.order);
2589
2840
  for (let i = 0; i < drawOrder.length; i++) {
2590
2841
  const entry = drawOrder[i];
@@ -2609,10 +2860,14 @@ var PixiRenderer = class {
2609
2860
  () => {
2610
2861
  for (const entry of tilemaps.values()) {
2611
2862
  entry.dispose();
2612
- if (entry instanceof TilemapVisual) this.tilemaps.delete(entry);
2613
- else this.geometryTilemaps.delete(entry);
2863
+ this.tilemaps.delete(entry);
2614
2864
  }
2615
2865
  tilemaps.clear();
2866
+ for (const entry of geometryTilemaps.values()) {
2867
+ entry.dispose();
2868
+ this.geometryTilemaps.delete(entry);
2869
+ }
2870
+ geometryTilemaps.clear();
2616
2871
  },
2617
2872
  () => root.destroy({
2618
2873
  children: true,
@@ -2912,6 +3167,11 @@ var Game = class {
2912
3167
  gamepads;
2913
3168
  renderer;
2914
3169
  keyboard;
3170
+ captures;
3171
+ captureInput(options = {}) {
3172
+ this.assertAlive();
3173
+ return this.captures.start(options);
3174
+ }
2915
3175
  pointers;
2916
3176
  pointerChannels = /* @__PURE__ */ new WeakMap();
2917
3177
  virtualInputs = /* @__PURE__ */ new WeakMap();
@@ -2985,7 +3245,7 @@ var Game = class {
2985
3245
  height = 450;
2986
3246
  ratio = 1;
2987
3247
  scenes = /* @__PURE__ */ new Map();
2988
- pendingScenes = /* @__PURE__ */ new Map();
3248
+ pendingScene;
2989
3249
  defaultUI = [];
2990
3250
  ui;
2991
3251
  prepared;
@@ -3054,18 +3314,22 @@ var Game = class {
3054
3314
  request: (callback) => requestAnimationFrame(callback),
3055
3315
  cancel: (handle) => cancelAnimationFrame(handle)
3056
3316
  };
3317
+ this.captures = new InputCaptureManager(this.element, this.engine.input, () => this.engine.scene, () => this.pointers.release(), this.report);
3057
3318
  this.keyboard = new BrowserInput(this.element, this.engine.input);
3058
- this.gamepads = new GamepadInput(this.engine.input, { active: () => !document.hidden && document.hasFocus() && this.element.contains(document.activeElement) && !blocksGameKeyboard(document.activeElement) });
3319
+ this.gamepads = new GamepadInput(this.engine.input, {
3320
+ observe: (pads) => this.captures.sample(pads),
3321
+ active: () => !document.hidden && document.hasFocus() && this.element.contains(document.activeElement) && !blocksGameKeyboard(document.activeElement)
3322
+ });
3059
3323
  this.pointers = new BrowserPointer(this.element, (event) => {
3060
3324
  const rect = this.element.getBoundingClientRect();
3061
- if (rect.width <= 0 || rect.height <= 0) throw new EngineError("VIEWPORT_UNAVAILABLE", "Game has no visible bounds");
3325
+ if (rect.width <= 0 || rect.height <= 0) return;
3062
3326
  const screen = screenPoint((event.clientX - rect.left) * this.width / rect.width, (event.clientY - rect.top) * this.height / rect.height);
3063
3327
  return {
3064
3328
  screen,
3065
3329
  world: this.screenToWorld(screen),
3066
3330
  view: this.view
3067
3331
  };
3068
- }, this.report, () => this.updateProjection());
3332
+ }, this.report, () => this.updateProjection(), this.engine.input);
3069
3333
  document.addEventListener("visibilitychange", () => {
3070
3334
  this.previousTime = void 0;
3071
3335
  this.fixed?.reset();
@@ -3164,12 +3428,17 @@ var Game = class {
3164
3428
  }
3165
3429
  scene(id, setup) {
3166
3430
  this.assertAlive();
3431
+ if (typeof id !== "string" || !id.length) throw new EngineError("INVALID_VALUE", "Scene id must not be empty");
3432
+ if (typeof setup !== "function") throw new EngineError("INVALID_VALUE", "Scene setup must be a function");
3167
3433
  if (this.scenes.has(id)) throw new EngineError("DUPLICATE_SCENE", "Scene id is already registered", { sceneId: id });
3168
3434
  this.scenes.set(id, setup);
3169
3435
  return this;
3170
3436
  }
3171
3437
  wait(promise) {
3172
- if (this.dead) return Promise.reject(new EngineError("GAME_DISPOSED", "Game has been disposed"));
3438
+ if (this.dead) {
3439
+ promise.catch(() => {});
3440
+ return Promise.reject(new EngineError("GAME_DISPOSED", "Game has been disposed"));
3441
+ }
3173
3442
  const signal = this.shutdown.signal;
3174
3443
  return new Promise((resolve, reject) => {
3175
3444
  const abort = () => {
@@ -3205,7 +3474,7 @@ var Game = class {
3205
3474
  })();
3206
3475
  return this.startPromise;
3207
3476
  }
3208
- goTo(id) {
3477
+ changeScene(id) {
3209
3478
  if (this.dead) return Promise.reject(new EngineError("GAME_DISPOSED", "Game has been disposed"));
3210
3479
  if (!this.ready) return Promise.reject(new EngineError("GAME_NOT_READY", "Await run before navigating"));
3211
3480
  return this.navigate(id);
@@ -3213,8 +3482,7 @@ var Game = class {
3213
3482
  navigate(id) {
3214
3483
  const setup = this.scenes.get(id);
3215
3484
  if (!setup) return Promise.reject(new EngineError("SCENE_NOT_FOUND", "Unknown scene", { sceneId: id }));
3216
- const existing = this.pendingScenes.get(id);
3217
- if (existing) return existing;
3485
+ if (this.pendingScene?.id === id) return this.pendingScene.task;
3218
3486
  const task = this.queue.then(async () => {
3219
3487
  this.assertAlive();
3220
3488
  const scene = new Scene(id), nodes = [], controller = new AbortController();
@@ -3256,7 +3524,9 @@ var Game = class {
3256
3524
  onDispose: (callback) => scene.onDispose(callback)
3257
3525
  })));
3258
3526
  this.assertAlive();
3259
- await this.present(scene, nodes, canvas);
3527
+ await this.present(scene, nodes, canvas, () => {
3528
+ if (this.pendingScene?.task === task) this.pendingScene = void 0;
3529
+ });
3260
3530
  } catch (error) {
3261
3531
  try {
3262
3532
  scene.dispose();
@@ -3268,13 +3538,16 @@ var Game = class {
3268
3538
  if (this.staging === scene) this.staging = void 0;
3269
3539
  }
3270
3540
  }).finally(() => {
3271
- if (this.pendingScenes.get(id) === task) this.pendingScenes.delete(id);
3541
+ if (this.pendingScene?.task === task) this.pendingScene = void 0;
3272
3542
  });
3273
- this.pendingScenes.set(id, task);
3543
+ this.pendingScene = {
3544
+ id,
3545
+ task
3546
+ };
3274
3547
  this.queue = task.catch(() => {});
3275
3548
  return task;
3276
3549
  }
3277
- async present(scene, nodes, canvas) {
3550
+ async present(scene, nodes, canvas, beforeActivate) {
3278
3551
  let prepared, ui, committed = false;
3279
3552
  let canvasPresentation;
3280
3553
  let virtualPresentation;
@@ -3300,6 +3573,7 @@ var Game = class {
3300
3573
  }
3301
3574
  virtualPresentation = this.prepareVirtual(scene);
3302
3575
  scene.markReady();
3576
+ beforeActivate?.();
3303
3577
  this.errorBox.hidden = true;
3304
3578
  try {
3305
3579
  if (this.playback && this.playbackScene !== scene) this.stopReplay();
@@ -3315,18 +3589,21 @@ var Game = class {
3315
3589
  this.canvas = canvas;
3316
3590
  this.canvasPresentation = canvasPresentation;
3317
3591
  this.virtualPresentation = virtualPresentation;
3318
- this.pointers.routing.setScene(this.channelFor(scene), canvas);
3319
3592
  committed = true;
3320
- this.element.append(ui.element);
3321
- for (const cleanup of [
3322
- () => oldPrepared?.dispose(),
3323
- () => oldUI?.dispose(),
3324
- () => oldCanvas?.dispose(),
3325
- () => oldVirtual?.dispose()
3326
- ]) try {
3327
- cleanup();
3328
- } catch (error) {
3329
- this.report(error);
3593
+ try {
3594
+ this.pointers.routing.setScene(this.channelFor(scene), canvas);
3595
+ this.element.append(ui.element);
3596
+ } finally {
3597
+ for (const cleanup of [
3598
+ () => oldPrepared?.dispose(),
3599
+ () => oldUI?.dispose(),
3600
+ () => oldCanvas?.dispose(),
3601
+ () => oldVirtual?.dispose()
3602
+ ]) try {
3603
+ cleanup();
3604
+ } catch (error) {
3605
+ this.report(error);
3606
+ }
3330
3607
  }
3331
3608
  this.assertAlive();
3332
3609
  this.previousTime = void 0;
@@ -3544,6 +3821,7 @@ var Game = class {
3544
3821
  for (const cleanup of [
3545
3822
  () => this.stopReplay(),
3546
3823
  () => this.staging?.dispose(),
3824
+ () => this.captures.dispose(),
3547
3825
  () => this.pointers.dispose(),
3548
3826
  () => this.gamepads.dispose(),
3549
3827
  () => this.keyboard.dispose(),
@@ -3570,7 +3848,7 @@ var Game = class {
3570
3848
  this.defaultCanvas = void 0;
3571
3849
  this.defaultUI.length = 0;
3572
3850
  this.scenes.clear();
3573
- this.pendingScenes.clear();
3851
+ this.pendingScene = void 0;
3574
3852
  }
3575
3853
  };
3576
3854
  function validFontFamily(value) {