miaoda-game-devkit 0.2.9 → 0.2.11

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 (40) hide show
  1. package/README.md +27 -1
  2. package/dist/cli/lint.js +32 -1
  3. package/dist/game-clock-suUidZdT.d.mts +11 -0
  4. package/dist/game-clock-suUidZdT.d.ts +11 -0
  5. package/dist/{gameplay-audit-CCJo4Qhk.d.mts → gameplay-audit-DmbBA0M_.d.mts} +7 -1
  6. package/dist/{gameplay-audit-CCJo4Qhk.d.ts → gameplay-audit-DmbBA0M_.d.ts} +7 -1
  7. package/dist/index.d.mts +2 -2
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +165 -4
  10. package/dist/index.mjs +165 -4
  11. package/dist/lint/game-telemetry.contract.mjs +165 -4
  12. package/dist/lint/gameplay-contract.contract.mjs +165 -4
  13. package/dist/lint/phaser-headless.contract.mjs +281 -4
  14. package/dist/lint/vitest-config.contract.mjs +234 -11
  15. package/dist/phaser-facade.mjs +156 -0
  16. package/dist/react/index.d.mts +13 -0
  17. package/dist/react/index.d.ts +13 -0
  18. package/dist/react/index.js +57 -0
  19. package/dist/react/index.mjs +29 -0
  20. package/dist/react/testing.d.mts +21 -0
  21. package/dist/react/testing.d.ts +21 -0
  22. package/dist/react/testing.js +82 -0
  23. package/dist/react/testing.mjs +57 -0
  24. package/dist/react/vitest-config.d.mts +14 -0
  25. package/dist/react/vitest-config.d.ts +14 -0
  26. package/dist/react/vitest-config.js +57 -0
  27. package/dist/react/vitest-config.mjs +32 -0
  28. package/dist/react/vitest-setup.d.mts +2 -0
  29. package/dist/react/vitest-setup.d.ts +2 -0
  30. package/dist/react/vitest-setup.js +35 -0
  31. package/dist/react/vitest-setup.mjs +33 -0
  32. package/dist/vite.d.mts +22 -0
  33. package/dist/vite.d.ts +22 -0
  34. package/dist/vite.js +93 -0
  35. package/dist/vite.mjs +67 -0
  36. package/dist/vitest-config.d.mts +2 -2
  37. package/dist/vitest-config.d.ts +2 -2
  38. package/dist/vitest-config.js +53 -3
  39. package/dist/vitest-config.mjs +53 -3
  40. package/package.json +72 -2
package/dist/index.mjs CHANGED
@@ -1,6 +1,75 @@
1
1
  // src/phaser-headless-host.ts
2
2
  import * as Phaser3 from "phaser";
3
3
 
4
+ // src/phaser-engine-warnings.ts
5
+ var originalWarn;
6
+ var originalError;
7
+ var listener;
8
+ function formatWarningArgument(value) {
9
+ if (typeof value === "string") return value;
10
+ if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
11
+ return String(value);
12
+ }
13
+ if (value instanceof Error) return value.message;
14
+ const constructorName = typeof value === "object" ? value.constructor?.name : void 0;
15
+ return constructorName ? `[${constructorName}]` : String(value);
16
+ }
17
+ function comesFromPhaser(stack) {
18
+ return /(?:^|[/\\])phaser(?:[/\\]|\.(?:js|mjs|cjs)(?::\d+)?)/m.test(stack);
19
+ }
20
+ function captureEngineDiagnostic(level, original, args) {
21
+ const stack = new Error(`Phaser engine ${level}`).stack ?? "";
22
+ if (!comesFromPhaser(stack)) {
23
+ original(...args);
24
+ return;
25
+ }
26
+ listener?.({
27
+ level,
28
+ message: args.map(formatWarningArgument).join(" "),
29
+ stack
30
+ });
31
+ }
32
+ function installWarningMultiplexer(currentListener) {
33
+ if (listener) {
34
+ throw new Error(
35
+ "Only one Phaser HEADLESS host may be active in the same Vitest worker. Destroy the current host before creating another one."
36
+ );
37
+ }
38
+ listener = currentListener;
39
+ const passthroughWarn = console.warn;
40
+ const passthroughError = console.error;
41
+ originalWarn = passthroughWarn;
42
+ originalError = passthroughError;
43
+ console.warn = (...args) => {
44
+ captureEngineDiagnostic("warn", passthroughWarn, args);
45
+ };
46
+ console.error = (...args) => {
47
+ captureEngineDiagnostic("error", passthroughError, args);
48
+ };
49
+ }
50
+ function uninstallWarningMultiplexer() {
51
+ if (!listener) return;
52
+ if (originalWarn) console.warn = originalWarn;
53
+ if (originalError) console.error = originalError;
54
+ listener = void 0;
55
+ originalWarn = void 0;
56
+ originalError = void 0;
57
+ }
58
+ function observeEngineDiagnostics(currentListener) {
59
+ installWarningMultiplexer(currentListener);
60
+ let observing = true;
61
+ return () => {
62
+ if (!observing) return;
63
+ observing = false;
64
+ uninstallWarningMultiplexer();
65
+ };
66
+ }
67
+ function matchesEngineWarning(message, matcher) {
68
+ if (typeof matcher === "string") return message.includes(matcher);
69
+ matcher.lastIndex = 0;
70
+ return matcher.test(message);
71
+ }
72
+
4
73
  // src/phaser-text-assertions.ts
5
74
  import * as Phaser from "phaser";
6
75
  function describeText(label) {
@@ -10,12 +79,15 @@ function describeText(label) {
10
79
  }
11
80
  function isEffectivelyRenderable(label) {
12
81
  let current = label;
82
+ const visited = /* @__PURE__ */ new Set();
13
83
  while (current) {
84
+ if (visited.has(current)) return false;
85
+ visited.add(current);
14
86
  const state = current;
15
87
  if (state.visible === false || state.alpha === 0 || state.scaleX === 0 || state.scaleY === 0) {
16
88
  return false;
17
89
  }
18
- current = state.parentContainer ?? null;
90
+ current = state.parentContainer ?? (state.displayList instanceof Phaser.GameObjects.Layer ? state.displayList : null);
19
91
  }
20
92
  return true;
21
93
  }
@@ -191,12 +263,15 @@ function describeObject(object) {
191
263
  }
192
264
  function isEffectivelyVisible(object) {
193
265
  let current = object;
266
+ const visited = /* @__PURE__ */ new Set();
194
267
  while (current) {
268
+ if (visited.has(current)) return false;
269
+ visited.add(current);
195
270
  const state = current;
196
271
  if (state.visible === false || state.alpha === 0 || state.scaleX === 0 || state.scaleY === 0) {
197
272
  return false;
198
273
  }
199
- current = state.parentContainer ?? null;
274
+ current = state.parentContainer ?? (state.displayList instanceof Phaser2.GameObjects.Layer ? state.displayList : null);
200
275
  }
201
276
  return true;
202
277
  }
@@ -237,6 +312,12 @@ function collectGameObjectHealthProblems(scene) {
237
312
  if (!checksObjectState(object)) continue;
238
313
  const subject = describeObject(object);
239
314
  const target = object;
315
+ const texture = target.texture;
316
+ if (isEffectivelyVisible(object) && texture && typeof texture === "object" && Reflect.get(texture, "key") === "__MISSING") {
317
+ problems.push(
318
+ `${subject} uses Phaser's __MISSING texture; verify the loaded texture key and asset path`
319
+ );
320
+ }
240
321
  if (typeof target.setPosition === "function") {
241
322
  for (const property of transformProperties) {
242
323
  requireFiniteProperty(problems, subject, object, property);
@@ -374,7 +455,13 @@ function assertSceneInteractiveHealth(scene) {
374
455
 
375
456
  // src/phaser-headless-host.ts
376
457
  async function createHeadlessGame(scene, options = {}) {
377
- const { bootTimeoutMs = 2e3, additionalScenes = [], ...config } = options;
458
+ const {
459
+ bootTimeoutMs = 2e3,
460
+ additionalScenes = [],
461
+ ignoreEngineWarnings = [],
462
+ allowIdleLoaderQueues = [],
463
+ ...config
464
+ } = options;
378
465
  let game;
379
466
  let settled = false;
380
467
  let runtimeError;
@@ -383,6 +470,16 @@ async function createHeadlessGame(scene, options = {}) {
383
470
  };
384
471
  let removeRuntimeListeners = () => {
385
472
  };
473
+ const engineDiagnostics = [];
474
+ const loaderFailures = [];
475
+ const stopObservingEngineDiagnostics = observeEngineDiagnostics((diagnostic) => {
476
+ if (diagnostic.level === "warn" && ignoreEngineWarnings.some(
477
+ (matcher) => matchesEngineWarning(diagnostic.message, matcher)
478
+ )) {
479
+ return;
480
+ }
481
+ engineDiagnostics.push(diagnostic);
482
+ });
386
483
  const transitions = [];
387
484
  const registeredScenes = [];
388
485
  const visitedScenes = [];
@@ -468,6 +565,25 @@ async function createHeadlessGame(scene, options = {}) {
468
565
  const guardScene = (currentScene) => {
469
566
  if (guardedScenes.has(currentScene)) return;
470
567
  guardedScenes.add(currentScene);
568
+ const onFileLoadError = (file) => {
569
+ const source = file.src || file.url || "unknown source";
570
+ loaderFailures.push({
571
+ scene: currentScene.sys.settings.key,
572
+ type: String(file.type),
573
+ key: String(file.key),
574
+ source: String(source)
575
+ });
576
+ };
577
+ currentScene.load.on(
578
+ Phaser3.Loader.Events.FILE_LOAD_ERROR,
579
+ onFileLoadError
580
+ );
581
+ restoreGuards.push(() => {
582
+ currentScene.load.off(
583
+ Phaser3.Loader.Events.FILE_LOAD_ERROR,
584
+ onFileLoadError
585
+ );
586
+ });
471
587
  const scenePlugin = currentScene.scene;
472
588
  for (const method of [
473
589
  "start",
@@ -480,7 +596,12 @@ async function createHeadlessGame(scene, options = {}) {
480
596
  if (typeof original !== "function") continue;
481
597
  scenePlugin[method] = (key, ...args) => {
482
598
  const target = key === void 0 ? currentScene : typeof key === "string" ? currentScene.scene.get(key) : key;
483
- if (target) guardScene(target);
599
+ if (!target) {
600
+ throw new Error(
601
+ `[SCENE_NOT_REGISTERED] Scene ${JSON.stringify(currentScene.sys.settings.key)} called ${method}(${JSON.stringify(key)}), but the target Scene is not registered. Add it to the production Scene registry and pass it through additionalScenes in focused HEADLESS tests.`
602
+ );
603
+ }
604
+ guardScene(target);
484
605
  const from = currentScene.sys.settings.key;
485
606
  const to = key === void 0 ? from : typeof key === "string" ? key : key.sys.settings.key;
486
607
  if (from && to) transitions.push({ from, to, method });
@@ -608,6 +729,7 @@ async function createHeadlessGame(scene, options = {}) {
608
729
  game.headlessStep(game.loop.lastTime, 0);
609
730
  }
610
731
  restoreHostGuards();
732
+ stopObservingEngineDiagnostics();
611
733
  throw error;
612
734
  }
613
735
  const readyGame = game;
@@ -615,11 +737,39 @@ async function createHeadlessGame(scene, options = {}) {
615
737
  if (hasRuntimeError) {
616
738
  throw runtimeError instanceof Error ? runtimeError : new Error(`Unhandled Phaser runtime error: ${String(runtimeError)}`);
617
739
  }
740
+ if (loaderFailures.length > 0) {
741
+ const imageTypes = /* @__PURE__ */ new Set(["image", "normalMap", "spritesheet"]);
742
+ const hasHeadlessImageFailure = loaderFailures.some(
743
+ (failure) => imageTypes.has(failure.type)
744
+ );
745
+ throw new Error(
746
+ [
747
+ "Phaser loader failed to load one or more files:",
748
+ ...loaderFailures.map(
749
+ (failure) => `- ${failure.scene}: failed to load ${failure.type} ${JSON.stringify(failure.key)} from ${JSON.stringify(failure.source)}`
750
+ ),
751
+ hasHeadlessImageFailure ? "HEADLESS image loading supports data:image/ URLs only; use a data URL or a deliberate loader stub in tests, and verify production asset paths in a browser." : "Verify the resource URL and data, or install a deliberate loader stub for network resources in HEADLESS tests."
752
+ ].join("\n")
753
+ );
754
+ }
755
+ if (engineDiagnostics.length > 0) {
756
+ throw new Error(
757
+ [
758
+ "Phaser emitted an engine diagnostic:",
759
+ ...engineDiagnostics.flatMap((diagnostic) => [
760
+ `- ${diagnostic.level}: ${diagnostic.message}`,
761
+ ...diagnostic.stack.split("\n").slice(2, 8).map((frame) => ` ${frame.trim()}`)
762
+ ]),
763
+ "Fix the invalid Phaser operation. Only known warnings (not errors) may be allowed with ignoreEngineWarnings."
764
+ ].join("\n")
765
+ );
766
+ }
618
767
  };
619
768
  const canvas = readyGame.canvas;
620
769
  if (!canvas) {
621
770
  removeRuntimeListeners();
622
771
  restoreHostGuards();
772
+ stopObservingEngineDiagnostics();
623
773
  readyGame.destroy(true);
624
774
  readyGame.headlessStep(readyGame.loop.lastTime, 0);
625
775
  throw new Error("Phaser HEADLESS did not create an input canvas");
@@ -628,6 +778,7 @@ async function createHeadlessGame(scene, options = {}) {
628
778
  if (!inputWindow) {
629
779
  removeRuntimeListeners();
630
780
  restoreHostGuards();
781
+ stopObservingEngineDiagnostics();
631
782
  readyGame.destroy(true);
632
783
  readyGame.headlessStep(readyGame.loop.lastTime, 0);
633
784
  throw new Error(
@@ -683,6 +834,14 @@ async function createHeadlessGame(scene, options = {}) {
683
834
  if (!currentScene.sys.isActive() && !currentScene.sys.isPaused())
684
835
  continue;
685
836
  appendUnique(visitedScenes, currentScene.sys.settings.key);
837
+ if (!allowIdleLoaderQueues.includes(currentScene.sys.settings.key) && !currentScene.load.isLoading() && currentScene.load.list.size > 0) {
838
+ const queuedFiles = [...currentScene.load.list].map(
839
+ (file) => `${file.type}:${String(file.key)}`
840
+ );
841
+ throw new Error(
842
+ `Phaser loader queue is not running in scene ${JSON.stringify(currentScene.sys.settings.key)}: ${queuedFiles.length} file(s) are queued (${queuedFiles.join(", ")}). Files added outside preload() require this.load.start().`
843
+ );
844
+ }
686
845
  assertSceneTextHealth(currentScene);
687
846
  assertSceneInteractiveHealth(currentScene);
688
847
  assertSceneRuntimeHealth(currentScene);
@@ -908,6 +1067,7 @@ async function createHeadlessGame(scene, options = {}) {
908
1067
  } catch (error) {
909
1068
  removeRuntimeListeners();
910
1069
  restoreHostGuards();
1070
+ stopObservingEngineDiagnostics();
911
1071
  readyGame.destroy(true);
912
1072
  readyGame.headlessStep(readyGame.loop.lastTime, 0);
913
1073
  throw error;
@@ -1039,6 +1199,7 @@ async function createHeadlessGame(scene, options = {}) {
1039
1199
  } finally {
1040
1200
  removeRuntimeListeners();
1041
1201
  restoreHostGuards();
1202
+ stopObservingEngineDiagnostics();
1042
1203
  }
1043
1204
  }
1044
1205
  };
@@ -5,6 +5,75 @@ import { afterEach, describe, expect, it } from "vitest";
5
5
  // src/phaser-headless-host.ts
6
6
  import * as Phaser3 from "phaser";
7
7
 
8
+ // src/phaser-engine-warnings.ts
9
+ var originalWarn;
10
+ var originalError;
11
+ var listener;
12
+ function formatWarningArgument(value) {
13
+ if (typeof value === "string") return value;
14
+ if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
15
+ return String(value);
16
+ }
17
+ if (value instanceof Error) return value.message;
18
+ const constructorName = typeof value === "object" ? value.constructor?.name : void 0;
19
+ return constructorName ? `[${constructorName}]` : String(value);
20
+ }
21
+ function comesFromPhaser(stack) {
22
+ return /(?:^|[/\\])phaser(?:[/\\]|\.(?:js|mjs|cjs)(?::\d+)?)/m.test(stack);
23
+ }
24
+ function captureEngineDiagnostic(level, original, args) {
25
+ const stack = new Error(`Phaser engine ${level}`).stack ?? "";
26
+ if (!comesFromPhaser(stack)) {
27
+ original(...args);
28
+ return;
29
+ }
30
+ listener?.({
31
+ level,
32
+ message: args.map(formatWarningArgument).join(" "),
33
+ stack
34
+ });
35
+ }
36
+ function installWarningMultiplexer(currentListener) {
37
+ if (listener) {
38
+ throw new Error(
39
+ "Only one Phaser HEADLESS host may be active in the same Vitest worker. Destroy the current host before creating another one."
40
+ );
41
+ }
42
+ listener = currentListener;
43
+ const passthroughWarn = console.warn;
44
+ const passthroughError = console.error;
45
+ originalWarn = passthroughWarn;
46
+ originalError = passthroughError;
47
+ console.warn = (...args) => {
48
+ captureEngineDiagnostic("warn", passthroughWarn, args);
49
+ };
50
+ console.error = (...args) => {
51
+ captureEngineDiagnostic("error", passthroughError, args);
52
+ };
53
+ }
54
+ function uninstallWarningMultiplexer() {
55
+ if (!listener) return;
56
+ if (originalWarn) console.warn = originalWarn;
57
+ if (originalError) console.error = originalError;
58
+ listener = void 0;
59
+ originalWarn = void 0;
60
+ originalError = void 0;
61
+ }
62
+ function observeEngineDiagnostics(currentListener) {
63
+ installWarningMultiplexer(currentListener);
64
+ let observing = true;
65
+ return () => {
66
+ if (!observing) return;
67
+ observing = false;
68
+ uninstallWarningMultiplexer();
69
+ };
70
+ }
71
+ function matchesEngineWarning(message, matcher) {
72
+ if (typeof matcher === "string") return message.includes(matcher);
73
+ matcher.lastIndex = 0;
74
+ return matcher.test(message);
75
+ }
76
+
8
77
  // src/phaser-text-assertions.ts
9
78
  import * as Phaser from "phaser";
10
79
  function describeText(label) {
@@ -14,12 +83,15 @@ function describeText(label) {
14
83
  }
15
84
  function isEffectivelyRenderable(label) {
16
85
  let current = label;
86
+ const visited = /* @__PURE__ */ new Set();
17
87
  while (current) {
88
+ if (visited.has(current)) return false;
89
+ visited.add(current);
18
90
  const state = current;
19
91
  if (state.visible === false || state.alpha === 0 || state.scaleX === 0 || state.scaleY === 0) {
20
92
  return false;
21
93
  }
22
- current = state.parentContainer ?? null;
94
+ current = state.parentContainer ?? (state.displayList instanceof Phaser.GameObjects.Layer ? state.displayList : null);
23
95
  }
24
96
  return true;
25
97
  }
@@ -195,12 +267,15 @@ function describeObject(object) {
195
267
  }
196
268
  function isEffectivelyVisible(object) {
197
269
  let current = object;
270
+ const visited = /* @__PURE__ */ new Set();
198
271
  while (current) {
272
+ if (visited.has(current)) return false;
273
+ visited.add(current);
199
274
  const state = current;
200
275
  if (state.visible === false || state.alpha === 0 || state.scaleX === 0 || state.scaleY === 0) {
201
276
  return false;
202
277
  }
203
- current = state.parentContainer ?? null;
278
+ current = state.parentContainer ?? (state.displayList instanceof Phaser2.GameObjects.Layer ? state.displayList : null);
204
279
  }
205
280
  return true;
206
281
  }
@@ -241,6 +316,12 @@ function collectGameObjectHealthProblems(scene) {
241
316
  if (!checksObjectState(object)) continue;
242
317
  const subject = describeObject(object);
243
318
  const target = object;
319
+ const texture = target.texture;
320
+ if (isEffectivelyVisible(object) && texture && typeof texture === "object" && Reflect.get(texture, "key") === "__MISSING") {
321
+ problems.push(
322
+ `${subject} uses Phaser's __MISSING texture; verify the loaded texture key and asset path`
323
+ );
324
+ }
244
325
  if (typeof target.setPosition === "function") {
245
326
  for (const property of transformProperties) {
246
327
  requireFiniteProperty(problems, subject, object, property);
@@ -378,7 +459,13 @@ function assertSceneInteractiveHealth(scene) {
378
459
 
379
460
  // src/phaser-headless-host.ts
380
461
  async function createHeadlessGame(scene, options = {}) {
381
- const { bootTimeoutMs = 2e3, additionalScenes = [], ...config } = options;
462
+ const {
463
+ bootTimeoutMs = 2e3,
464
+ additionalScenes = [],
465
+ ignoreEngineWarnings = [],
466
+ allowIdleLoaderQueues = [],
467
+ ...config
468
+ } = options;
382
469
  let game;
383
470
  let settled = false;
384
471
  let runtimeError;
@@ -387,6 +474,16 @@ async function createHeadlessGame(scene, options = {}) {
387
474
  };
388
475
  let removeRuntimeListeners = () => {
389
476
  };
477
+ const engineDiagnostics = [];
478
+ const loaderFailures = [];
479
+ const stopObservingEngineDiagnostics = observeEngineDiagnostics((diagnostic) => {
480
+ if (diagnostic.level === "warn" && ignoreEngineWarnings.some(
481
+ (matcher) => matchesEngineWarning(diagnostic.message, matcher)
482
+ )) {
483
+ return;
484
+ }
485
+ engineDiagnostics.push(diagnostic);
486
+ });
390
487
  const transitions = [];
391
488
  const registeredScenes = [];
392
489
  const visitedScenes = [];
@@ -472,6 +569,25 @@ async function createHeadlessGame(scene, options = {}) {
472
569
  const guardScene = (currentScene) => {
473
570
  if (guardedScenes.has(currentScene)) return;
474
571
  guardedScenes.add(currentScene);
572
+ const onFileLoadError = (file) => {
573
+ const source = file.src || file.url || "unknown source";
574
+ loaderFailures.push({
575
+ scene: currentScene.sys.settings.key,
576
+ type: String(file.type),
577
+ key: String(file.key),
578
+ source: String(source)
579
+ });
580
+ };
581
+ currentScene.load.on(
582
+ Phaser3.Loader.Events.FILE_LOAD_ERROR,
583
+ onFileLoadError
584
+ );
585
+ restoreGuards.push(() => {
586
+ currentScene.load.off(
587
+ Phaser3.Loader.Events.FILE_LOAD_ERROR,
588
+ onFileLoadError
589
+ );
590
+ });
475
591
  const scenePlugin = currentScene.scene;
476
592
  for (const method of [
477
593
  "start",
@@ -484,7 +600,12 @@ async function createHeadlessGame(scene, options = {}) {
484
600
  if (typeof original !== "function") continue;
485
601
  scenePlugin[method] = (key, ...args) => {
486
602
  const target = key === void 0 ? currentScene : typeof key === "string" ? currentScene.scene.get(key) : key;
487
- if (target) guardScene(target);
603
+ if (!target) {
604
+ throw new Error(
605
+ `[SCENE_NOT_REGISTERED] Scene ${JSON.stringify(currentScene.sys.settings.key)} called ${method}(${JSON.stringify(key)}), but the target Scene is not registered. Add it to the production Scene registry and pass it through additionalScenes in focused HEADLESS tests.`
606
+ );
607
+ }
608
+ guardScene(target);
488
609
  const from = currentScene.sys.settings.key;
489
610
  const to = key === void 0 ? from : typeof key === "string" ? key : key.sys.settings.key;
490
611
  if (from && to) transitions.push({ from, to, method });
@@ -612,6 +733,7 @@ async function createHeadlessGame(scene, options = {}) {
612
733
  game.headlessStep(game.loop.lastTime, 0);
613
734
  }
614
735
  restoreHostGuards();
736
+ stopObservingEngineDiagnostics();
615
737
  throw error;
616
738
  }
617
739
  const readyGame = game;
@@ -619,11 +741,39 @@ async function createHeadlessGame(scene, options = {}) {
619
741
  if (hasRuntimeError) {
620
742
  throw runtimeError instanceof Error ? runtimeError : new Error(`Unhandled Phaser runtime error: ${String(runtimeError)}`);
621
743
  }
744
+ if (loaderFailures.length > 0) {
745
+ const imageTypes = /* @__PURE__ */ new Set(["image", "normalMap", "spritesheet"]);
746
+ const hasHeadlessImageFailure = loaderFailures.some(
747
+ (failure) => imageTypes.has(failure.type)
748
+ );
749
+ throw new Error(
750
+ [
751
+ "Phaser loader failed to load one or more files:",
752
+ ...loaderFailures.map(
753
+ (failure) => `- ${failure.scene}: failed to load ${failure.type} ${JSON.stringify(failure.key)} from ${JSON.stringify(failure.source)}`
754
+ ),
755
+ hasHeadlessImageFailure ? "HEADLESS image loading supports data:image/ URLs only; use a data URL or a deliberate loader stub in tests, and verify production asset paths in a browser." : "Verify the resource URL and data, or install a deliberate loader stub for network resources in HEADLESS tests."
756
+ ].join("\n")
757
+ );
758
+ }
759
+ if (engineDiagnostics.length > 0) {
760
+ throw new Error(
761
+ [
762
+ "Phaser emitted an engine diagnostic:",
763
+ ...engineDiagnostics.flatMap((diagnostic) => [
764
+ `- ${diagnostic.level}: ${diagnostic.message}`,
765
+ ...diagnostic.stack.split("\n").slice(2, 8).map((frame) => ` ${frame.trim()}`)
766
+ ]),
767
+ "Fix the invalid Phaser operation. Only known warnings (not errors) may be allowed with ignoreEngineWarnings."
768
+ ].join("\n")
769
+ );
770
+ }
622
771
  };
623
772
  const canvas = readyGame.canvas;
624
773
  if (!canvas) {
625
774
  removeRuntimeListeners();
626
775
  restoreHostGuards();
776
+ stopObservingEngineDiagnostics();
627
777
  readyGame.destroy(true);
628
778
  readyGame.headlessStep(readyGame.loop.lastTime, 0);
629
779
  throw new Error("Phaser HEADLESS did not create an input canvas");
@@ -632,6 +782,7 @@ async function createHeadlessGame(scene, options = {}) {
632
782
  if (!inputWindow) {
633
783
  removeRuntimeListeners();
634
784
  restoreHostGuards();
785
+ stopObservingEngineDiagnostics();
635
786
  readyGame.destroy(true);
636
787
  readyGame.headlessStep(readyGame.loop.lastTime, 0);
637
788
  throw new Error(
@@ -687,6 +838,14 @@ async function createHeadlessGame(scene, options = {}) {
687
838
  if (!currentScene.sys.isActive() && !currentScene.sys.isPaused())
688
839
  continue;
689
840
  appendUnique(visitedScenes, currentScene.sys.settings.key);
841
+ if (!allowIdleLoaderQueues.includes(currentScene.sys.settings.key) && !currentScene.load.isLoading() && currentScene.load.list.size > 0) {
842
+ const queuedFiles = [...currentScene.load.list].map(
843
+ (file) => `${file.type}:${String(file.key)}`
844
+ );
845
+ throw new Error(
846
+ `Phaser loader queue is not running in scene ${JSON.stringify(currentScene.sys.settings.key)}: ${queuedFiles.length} file(s) are queued (${queuedFiles.join(", ")}). Files added outside preload() require this.load.start().`
847
+ );
848
+ }
690
849
  assertSceneTextHealth(currentScene);
691
850
  assertSceneInteractiveHealth(currentScene);
692
851
  assertSceneRuntimeHealth(currentScene);
@@ -912,6 +1071,7 @@ async function createHeadlessGame(scene, options = {}) {
912
1071
  } catch (error) {
913
1072
  removeRuntimeListeners();
914
1073
  restoreHostGuards();
1074
+ stopObservingEngineDiagnostics();
915
1075
  readyGame.destroy(true);
916
1076
  readyGame.headlessStep(readyGame.loop.lastTime, 0);
917
1077
  throw error;
@@ -1043,6 +1203,7 @@ async function createHeadlessGame(scene, options = {}) {
1043
1203
  } finally {
1044
1204
  removeRuntimeListeners();
1045
1205
  restoreHostGuards();
1206
+ stopObservingEngineDiagnostics();
1046
1207
  }
1047
1208
  }
1048
1209
  };