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.
- package/README.md +27 -1
- package/dist/cli/lint.js +32 -1
- package/dist/game-clock-suUidZdT.d.mts +11 -0
- package/dist/game-clock-suUidZdT.d.ts +11 -0
- package/dist/{gameplay-audit-CCJo4Qhk.d.mts → gameplay-audit-DmbBA0M_.d.mts} +7 -1
- package/dist/{gameplay-audit-CCJo4Qhk.d.ts → gameplay-audit-DmbBA0M_.d.ts} +7 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +165 -4
- package/dist/index.mjs +165 -4
- package/dist/lint/game-telemetry.contract.mjs +165 -4
- package/dist/lint/gameplay-contract.contract.mjs +165 -4
- package/dist/lint/phaser-headless.contract.mjs +281 -4
- package/dist/lint/vitest-config.contract.mjs +234 -11
- package/dist/phaser-facade.mjs +156 -0
- package/dist/react/index.d.mts +13 -0
- package/dist/react/index.d.ts +13 -0
- package/dist/react/index.js +57 -0
- package/dist/react/index.mjs +29 -0
- package/dist/react/testing.d.mts +21 -0
- package/dist/react/testing.d.ts +21 -0
- package/dist/react/testing.js +82 -0
- package/dist/react/testing.mjs +57 -0
- package/dist/react/vitest-config.d.mts +14 -0
- package/dist/react/vitest-config.d.ts +14 -0
- package/dist/react/vitest-config.js +57 -0
- package/dist/react/vitest-config.mjs +32 -0
- package/dist/react/vitest-setup.d.mts +2 -0
- package/dist/react/vitest-setup.d.ts +2 -0
- package/dist/react/vitest-setup.js +35 -0
- package/dist/react/vitest-setup.mjs +33 -0
- package/dist/vite.d.mts +22 -0
- package/dist/vite.d.ts +22 -0
- package/dist/vite.js +93 -0
- package/dist/vite.mjs +67 -0
- package/dist/vitest-config.d.mts +2 -2
- package/dist/vitest-config.d.ts +2 -2
- package/dist/vitest-config.js +53 -3
- package/dist/vitest-config.mjs +53 -3
- 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 {
|
|
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)
|
|
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 {
|
|
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)
|
|
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
|
};
|