miaoda-game-devkit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -21,7 +21,9 @@ function isEffectivelyRenderable(label) {
21
21
  }
22
22
  function requireFinite(problems, label, property, value) {
23
23
  if (!Number.isFinite(value)) {
24
- problems.push(`${describeText(label)} has non-finite ${property}: ${String(value)}`);
24
+ problems.push(
25
+ `${describeText(label)} has non-finite ${property}: ${String(value)}`
26
+ );
25
27
  }
26
28
  }
27
29
  function collectSceneTexts(scene) {
@@ -75,7 +77,9 @@ function collectMissingBitmapGlyphs(text, chars) {
75
77
  const code = text.charCodeAt(index);
76
78
  if (seen.has(code) || chars[code] !== void 0) continue;
77
79
  seen.add(code);
78
- missing.push(`${JSON.stringify(character)} (U+${code.toString(16).toUpperCase().padStart(4, "0")})`);
80
+ missing.push(
81
+ `${JSON.stringify(character)} (U+${code.toString(16).toUpperCase().padStart(4, "0")})`
82
+ );
79
83
  }
80
84
  return missing;
81
85
  }
@@ -100,19 +104,29 @@ function collectTextHealthProblems(scene) {
100
104
  requireFinite(problems, label, "resolution", label.style.resolution);
101
105
  const requiresPositiveSize = /\S/u.test(label.text) && isEffectivelyRenderable(label);
102
106
  if (requiresPositiveSize && Number.isFinite(label.width) && label.width <= 0) {
103
- problems.push(`${describeText(label)} has non-positive width: ${label.width}`);
107
+ problems.push(
108
+ `${describeText(label)} has non-positive width: ${label.width}`
109
+ );
104
110
  }
105
111
  if (requiresPositiveSize && Number.isFinite(label.height) && label.height <= 0) {
106
- problems.push(`${describeText(label)} has non-positive height: ${label.height}`);
112
+ problems.push(
113
+ `${describeText(label)} has non-positive height: ${label.height}`
114
+ );
107
115
  }
108
116
  if (Number.isFinite(label.style.fixedWidth) && label.style.fixedWidth < 0) {
109
- problems.push(`${describeText(label)} has negative fixedWidth: ${label.style.fixedWidth}`);
117
+ problems.push(
118
+ `${describeText(label)} has negative fixedWidth: ${label.style.fixedWidth}`
119
+ );
110
120
  }
111
121
  if (Number.isFinite(label.style.fixedHeight) && label.style.fixedHeight < 0) {
112
- problems.push(`${describeText(label)} has negative fixedHeight: ${label.style.fixedHeight}`);
122
+ problems.push(
123
+ `${describeText(label)} has negative fixedHeight: ${label.style.fixedHeight}`
124
+ );
113
125
  }
114
126
  if (Number.isFinite(label.style.resolution) && label.style.resolution <= 0) {
115
- problems.push(`${describeText(label)} has non-positive resolution: ${label.style.resolution}`);
127
+ problems.push(
128
+ `${describeText(label)} has non-positive resolution: ${label.style.resolution}`
129
+ );
116
130
  }
117
131
  const bounds = label.getBounds();
118
132
  for (const [property, value] of Object.entries({
@@ -146,11 +160,13 @@ function assertSceneTextHealth(scene) {
146
160
  ...collectBitmapTextHealthProblems(scene)
147
161
  ];
148
162
  if (problems.length === 0) return;
149
- throw new Error([
150
- `Phaser text health check failed in scene ${JSON.stringify(scene.scene.key)}:`,
151
- ...problems.map((problem) => `- ${problem}`),
152
- "Fix optional TextStyle values by omitting them or using Phaser defaults (for example fixedWidth: value ?? 0)."
153
- ].join("\n"));
163
+ throw new Error(
164
+ [
165
+ `Phaser text health check failed in scene ${JSON.stringify(scene.scene.key)}:`,
166
+ ...problems.map((problem) => `- ${problem}`),
167
+ "Fix optional TextStyle values by omitting them or using Phaser defaults (for example fixedWidth: value ?? 0)."
168
+ ].join("\n")
169
+ );
154
170
  }
155
171
 
156
172
  // src/phaser-runtime-assertions.ts
@@ -162,8 +178,10 @@ function collectObjects(scene) {
162
178
  if (visited.has(object)) return;
163
179
  visited.add(object);
164
180
  objects.push(object);
165
- if (object instanceof Phaser2.GameObjects.Container) object.list.forEach(visit);
166
- if (object instanceof Phaser2.GameObjects.Layer) object.getChildren().forEach(visit);
181
+ if (object instanceof Phaser2.GameObjects.Container)
182
+ object.list.forEach(visit);
183
+ if (object instanceof Phaser2.GameObjects.Layer)
184
+ object.getChildren().forEach(visit);
167
185
  };
168
186
  scene.children.getChildren().forEach(visit);
169
187
  return objects;
@@ -197,7 +215,9 @@ function requireFiniteVector(problems, subject, target, property) {
197
215
  if (!(axis in vector)) continue;
198
216
  const value = Reflect.get(vector, axis);
199
217
  if (typeof value !== "number" || !Number.isFinite(value)) {
200
- problems.push(`${subject} has non-finite ${property}.${axis}: ${String(value)}`);
218
+ problems.push(
219
+ `${subject} has non-finite ${property}.${axis}: ${String(value)}`
220
+ );
201
221
  }
202
222
  }
203
223
  }
@@ -265,7 +285,9 @@ function getArcadeBodyCollection(value) {
265
285
  function getArcadeWorldBodies(world) {
266
286
  if (!world || typeof world !== "object") return void 0;
267
287
  const bodies = getArcadeBodyCollection(Reflect.get(world, "bodies"));
268
- const staticBodies = getArcadeBodyCollection(Reflect.get(world, "staticBodies"));
288
+ const staticBodies = getArcadeBodyCollection(
289
+ Reflect.get(world, "staticBodies")
290
+ );
269
291
  return bodies && staticBodies ? [...bodies, ...staticBodies] : void 0;
270
292
  }
271
293
  function collectArcadeBodyHealthProblems(scene) {
@@ -276,22 +298,38 @@ function collectArcadeBodyHealthProblems(scene) {
276
298
  if (body.enable === false) continue;
277
299
  const gameObject = body.gameObject;
278
300
  const subject = gameObject ? `Arcade Body for ${describeObject(gameObject)}` : "Arcade Body";
279
- for (const property of ["position", "velocity", "acceleration", "gravity", "offset", "center"]) {
301
+ for (const property of [
302
+ "position",
303
+ "velocity",
304
+ "acceleration",
305
+ "gravity",
306
+ "offset",
307
+ "center"
308
+ ]) {
280
309
  requireFiniteVector(problems, subject, body, property);
281
310
  }
282
- for (const property of ["width", "height", "halfWidth", "halfHeight", "rotation"]) {
311
+ for (const property of [
312
+ "width",
313
+ "height",
314
+ "halfWidth",
315
+ "halfHeight",
316
+ "rotation"
317
+ ]) {
283
318
  requireFiniteProperty(problems, subject, body, property);
284
319
  }
285
320
  }
286
321
  return problems;
287
322
  }
288
323
  function usesCustomHitArea(object) {
289
- return Boolean(object.input?.customHitArea);
324
+ return Boolean(
325
+ object.input?.customHitArea
326
+ );
290
327
  }
291
328
  function collectInteractiveHealthProblems(scene) {
292
329
  const problems = [];
293
330
  for (const object of collectObjects(scene)) {
294
- if (object.active === false || !isEffectivelyVisible(object) || !object.input?.enabled) continue;
331
+ if (object.active === false || !isEffectivelyVisible(object) || !object.input?.enabled)
332
+ continue;
295
333
  if (usesCustomHitArea(object)) continue;
296
334
  const hitArea = object.input.hitArea;
297
335
  if (!hitArea) {
@@ -314,20 +352,24 @@ function assertSceneRuntimeHealth(scene) {
314
352
  ...collectArcadeBodyHealthProblems(scene)
315
353
  ];
316
354
  if (problems.length === 0) return;
317
- throw new Error([
318
- `Phaser runtime health check failed in scene ${JSON.stringify(scene.scene.key)}:`,
319
- ...problems.map((problem) => `- ${problem}`),
320
- "Keep values consumed by Phaser finite; zero size and zero scale remain allowed."
321
- ].join("\n"));
355
+ throw new Error(
356
+ [
357
+ `Phaser runtime health check failed in scene ${JSON.stringify(scene.scene.key)}:`,
358
+ ...problems.map((problem) => `- ${problem}`),
359
+ "Keep values consumed by Phaser finite; zero size and zero scale remain allowed."
360
+ ].join("\n")
361
+ );
322
362
  }
323
363
  function assertSceneInteractiveHealth(scene) {
324
364
  const problems = collectInteractiveHealthProblems(scene);
325
365
  if (problems.length === 0) return;
326
- throw new Error([
327
- `Phaser interactive health check failed in scene ${JSON.stringify(scene.scene.key)}:`,
328
- ...problems.map((problem) => `- ${problem}`),
329
- "Fix the default hit area or disable input before making the object interactive."
330
- ].join("\n"));
366
+ throw new Error(
367
+ [
368
+ `Phaser interactive health check failed in scene ${JSON.stringify(scene.scene.key)}:`,
369
+ ...problems.map((problem) => `- ${problem}`),
370
+ "Fix the default hit area or disable input before making the object interactive."
371
+ ].join("\n")
372
+ );
331
373
  }
332
374
 
333
375
  // src/phaser-headless-host.ts
@@ -342,11 +384,32 @@ async function createHeadlessGame(scene, options = {}) {
342
384
  let removeRuntimeListeners = () => {
343
385
  };
344
386
  const transitions = [];
387
+ const registeredScenes = [];
388
+ const visitedScenes = [];
389
+ const restartedScenes = [];
390
+ const checkpoints = [];
391
+ const appendUnique = (values, value) => {
392
+ if (value && !values.includes(value)) values.push(value);
393
+ };
394
+ const assertStepCount = (value, name) => {
395
+ if (!Number.isSafeInteger(value) || value < 0) {
396
+ throw new Error(
397
+ `${name} must be a non-negative safe integer; received ${value}`
398
+ );
399
+ }
400
+ };
345
401
  const evidence = {
346
402
  frames: 0,
347
403
  physicsSteps: 0,
348
404
  mouseEvents: 0,
405
+ keyboardEvents: 0,
406
+ touchEvents: 0,
349
407
  clicks: 0,
408
+ registeredScenes,
409
+ visitedScenes,
410
+ restartedScenes,
411
+ checkpoints,
412
+ destroyed: false,
350
413
  transitions
351
414
  };
352
415
  const created = new Promise((resolve, reject) => {
@@ -392,7 +455,9 @@ async function createHeadlessGame(scene, options = {}) {
392
455
  });
393
456
  };
394
457
  const timeout = window.setTimeout(() => {
395
- finish(new Error(`Phaser HEADLESS boot timed out after ${bootTimeoutMs}ms`));
458
+ finish(
459
+ new Error(`Phaser HEADLESS boot timed out after ${bootTimeoutMs}ms`)
460
+ );
396
461
  }, bootTimeoutMs);
397
462
  window.addEventListener("error", onWindowError, true);
398
463
  window.addEventListener("unhandledrejection", onUnhandledRejection);
@@ -404,7 +469,13 @@ async function createHeadlessGame(scene, options = {}) {
404
469
  if (guardedScenes.has(currentScene)) return;
405
470
  guardedScenes.add(currentScene);
406
471
  const scenePlugin = currentScene.scene;
407
- for (const method of ["start", "launch", "switch", "sleep", "wake"]) {
472
+ for (const method of [
473
+ "start",
474
+ "launch",
475
+ "switch",
476
+ "sleep",
477
+ "wake"
478
+ ]) {
408
479
  const original = scenePlugin[method];
409
480
  if (typeof original !== "function") continue;
410
481
  scenePlugin[method] = (key, ...args) => {
@@ -420,29 +491,49 @@ async function createHeadlessGame(scene, options = {}) {
420
491
  scenePlugin[method] = original;
421
492
  });
422
493
  }
494
+ const originalRestart = scenePlugin.restart;
495
+ if (typeof originalRestart === "function") {
496
+ scenePlugin.restart = (...args) => {
497
+ appendUnique(restartedScenes, currentScene.sys.settings.key);
498
+ return Reflect.apply(originalRestart, currentScene.scene, args);
499
+ };
500
+ restoreGuards.push(() => {
501
+ scenePlugin.restart = originalRestart;
502
+ });
503
+ }
423
504
  for (const hook of ["init", "preload", "create", "update"]) {
424
505
  const original = Reflect.get(currentScene, hook);
425
506
  if (typeof original !== "function") continue;
426
- const ownDescriptor = Object.getOwnPropertyDescriptor(currentScene, hook);
427
- Reflect.set(currentScene, hook, function guardedLifecycleHook(...args) {
428
- try {
429
- const result = Reflect.apply(original, this, args);
430
- if (result && typeof result === "object" && "then" in result) {
431
- const completion = Promise.resolve(result);
432
- if (currentScene === scene && hook === "create" && !settled) {
433
- completion.then(() => window.setTimeout(() => finish(), 0), reportError);
434
- } else {
435
- completion.catch(reportError);
507
+ const ownDescriptor = Object.getOwnPropertyDescriptor(
508
+ currentScene,
509
+ hook
510
+ );
511
+ Reflect.set(
512
+ currentScene,
513
+ hook,
514
+ function guardedLifecycleHook(...args) {
515
+ try {
516
+ const result = Reflect.apply(original, this, args);
517
+ if (result && typeof result === "object" && "then" in result) {
518
+ const completion = Promise.resolve(result);
519
+ if (currentScene === scene && hook === "create" && !settled) {
520
+ completion.then(
521
+ () => window.setTimeout(() => finish(), 0),
522
+ reportError
523
+ );
524
+ } else {
525
+ completion.catch(reportError);
526
+ }
527
+ } else if (currentScene === scene && hook === "create" && !settled) {
528
+ window.setTimeout(() => finish(), 0);
436
529
  }
437
- } else if (currentScene === scene && hook === "create" && !settled) {
438
- window.setTimeout(() => finish(), 0);
530
+ return result;
531
+ } catch (error) {
532
+ reportError(error);
533
+ return void 0;
439
534
  }
440
- return result;
441
- } catch (error) {
442
- reportError(error);
443
- return void 0;
444
535
  }
445
- });
536
+ );
446
537
  restoreGuards.push(() => {
447
538
  if (ownDescriptor) {
448
539
  Object.defineProperty(currentScene, hook, ownDescriptor);
@@ -471,13 +562,6 @@ async function createHeadlessGame(scene, options = {}) {
471
562
  banner: false,
472
563
  autoFocus: false,
473
564
  seed: ["phaser-headless-test"],
474
- physics: {
475
- default: "arcade",
476
- arcade: {
477
- gravity: { x: 0, y: 0 },
478
- fixedStep: true
479
- }
480
- },
481
565
  ...config,
482
566
  type: Phaser3.HEADLESS,
483
567
  fps,
@@ -503,7 +587,10 @@ async function createHeadlessGame(scene, options = {}) {
503
587
  };
504
588
  bootedGame.events.on(Phaser3.Core.Events.POST_STEP, checkReadiness);
505
589
  removeReadinessCheck = () => {
506
- bootedGame.events.off(Phaser3.Core.Events.POST_STEP, checkReadiness);
590
+ bootedGame.events.off(
591
+ Phaser3.Core.Events.POST_STEP,
592
+ checkReadiness
593
+ );
507
594
  };
508
595
  }
509
596
  },
@@ -543,7 +630,9 @@ async function createHeadlessGame(scene, options = {}) {
543
630
  restoreHostGuards();
544
631
  readyGame.destroy(true);
545
632
  readyGame.headlessStep(readyGame.loop.lastTime, 0);
546
- throw new Error("Phaser HEADLESS input canvas is not attached to a DOM Window");
633
+ throw new Error(
634
+ "Phaser HEADLESS input canvas is not attached to a DOM Window"
635
+ );
547
636
  }
548
637
  let canvasBounds = {
549
638
  left: 0,
@@ -579,7 +668,9 @@ async function createHeadlessGame(scene, options = {}) {
579
668
  setCanvasBounds(canvasBounds);
580
669
  const gameToClient = (x, y) => {
581
670
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
582
- throw new Error(`HEADLESS input coordinates must be finite; received (${x}, ${y})`);
671
+ throw new Error(
672
+ `HEADLESS input coordinates must be finite; received (${x}, ${y})`
673
+ );
583
674
  }
584
675
  return {
585
676
  clientX: canvasBounds.left + x / readyGame.scale.displayScale.x,
@@ -588,7 +679,10 @@ async function createHeadlessGame(scene, options = {}) {
588
679
  };
589
680
  const assertHealth = () => {
590
681
  for (const currentScene of readyGame.scene.getScenes(false)) {
591
- if (!currentScene.sys.isActive() && !currentScene.sys.isPaused()) continue;
682
+ appendUnique(registeredScenes, currentScene.sys.settings.key);
683
+ if (!currentScene.sys.isActive() && !currentScene.sys.isPaused())
684
+ continue;
685
+ appendUnique(visitedScenes, currentScene.sys.settings.key);
592
686
  assertSceneTextHealth(currentScene);
593
687
  assertSceneInteractiveHealth(currentScene);
594
688
  assertSceneRuntimeHealth(currentScene);
@@ -599,17 +693,166 @@ async function createHeadlessGame(scene, options = {}) {
599
693
  throwRuntimeError();
600
694
  assertHealth();
601
695
  };
696
+ const getEventTarget = (target, kind) => {
697
+ if (!target || typeof Reflect.get(target, "dispatchEvent") !== "function") {
698
+ throw new Error(
699
+ `Phaser HEADLESS ${kind} input target cannot dispatch DOM events`
700
+ );
701
+ }
702
+ return target;
703
+ };
602
704
  const dispatchMouse = async (type, x, y, buttons) => {
603
705
  const { clientX, clientY } = gameToClient(x, y);
604
- canvas.dispatchEvent(new inputWindow.MouseEvent(type, {
706
+ const mouseTarget = getEventTarget(readyGame.input.mouse?.target, "mouse");
707
+ mouseTarget.dispatchEvent(
708
+ new inputWindow.MouseEvent(type, {
709
+ bubbles: true,
710
+ cancelable: true,
711
+ button: 0,
712
+ buttons,
713
+ clientX,
714
+ clientY
715
+ })
716
+ );
717
+ evidence.mouseEvents += 1;
718
+ await settleRuntime();
719
+ };
720
+ const keyIdentity = (keyCode) => {
721
+ if (keyCode >= 65 && keyCode <= 90) {
722
+ const letter = String.fromCharCode(keyCode);
723
+ return { key: letter.toLowerCase(), code: `Key${letter}` };
724
+ }
725
+ if (keyCode >= 48 && keyCode <= 57) {
726
+ const digit = String.fromCharCode(keyCode);
727
+ return { key: digit, code: `Digit${digit}` };
728
+ }
729
+ const identities = /* @__PURE__ */ new Map([
730
+ [
731
+ Phaser3.Input.Keyboard.KeyCodes.LEFT,
732
+ { key: "ArrowLeft", code: "ArrowLeft" }
733
+ ],
734
+ [
735
+ Phaser3.Input.Keyboard.KeyCodes.RIGHT,
736
+ { key: "ArrowRight", code: "ArrowRight" }
737
+ ],
738
+ [Phaser3.Input.Keyboard.KeyCodes.UP, { key: "ArrowUp", code: "ArrowUp" }],
739
+ [
740
+ Phaser3.Input.Keyboard.KeyCodes.DOWN,
741
+ { key: "ArrowDown", code: "ArrowDown" }
742
+ ],
743
+ [Phaser3.Input.Keyboard.KeyCodes.SPACE, { key: " ", code: "Space" }],
744
+ [Phaser3.Input.Keyboard.KeyCodes.ENTER, { key: "Enter", code: "Enter" }],
745
+ [Phaser3.Input.Keyboard.KeyCodes.ESC, { key: "Escape", code: "Escape" }]
746
+ ]);
747
+ return identities.get(keyCode) ?? { key: "", code: "" };
748
+ };
749
+ const dispatchKeyboard = async (type, keyCode, options2 = {}) => {
750
+ if (!Number.isInteger(keyCode) || keyCode < 0) {
751
+ throw new Error(
752
+ `HEADLESS keyboard keyCode must be a non-negative integer; received ${keyCode}`
753
+ );
754
+ }
755
+ const identity = keyIdentity(keyCode);
756
+ const event = new inputWindow.KeyboardEvent(type, {
605
757
  bubbles: true,
606
758
  cancelable: true,
607
- button: 0,
608
- buttons,
759
+ key: options2.key ?? identity.key,
760
+ code: options2.code ?? identity.code,
761
+ repeat: options2.repeat ?? false,
762
+ altKey: options2.altKey ?? false,
763
+ ctrlKey: options2.ctrlKey ?? false,
764
+ metaKey: options2.metaKey ?? false,
765
+ shiftKey: options2.shiftKey ?? false
766
+ });
767
+ Object.defineProperties(event, {
768
+ // Phaser KeyboardManager 仍使用这些兼容旧浏览器的数字字段。
769
+ keyCode: { value: keyCode },
770
+ which: { value: keyCode }
771
+ });
772
+ const keyboardTarget = getEventTarget(
773
+ readyGame.input.keyboard?.target,
774
+ "keyboard"
775
+ );
776
+ keyboardTarget.dispatchEvent(event);
777
+ evidence.keyboardEvents += 1;
778
+ await settleRuntime();
779
+ };
780
+ const activeTouches = /* @__PURE__ */ new Map();
781
+ const createTouch = (x, y, identifier, target) => {
782
+ if (!Number.isInteger(identifier) || identifier < 0) {
783
+ throw new Error(
784
+ `HEADLESS touch identifier must be a non-negative integer; received ${identifier}`
785
+ );
786
+ }
787
+ const { clientX, clientY } = gameToClient(x, y);
788
+ return {
789
+ identifier,
790
+ target,
609
791
  clientX,
610
- clientY
611
- }));
612
- evidence.mouseEvents += 1;
792
+ clientY,
793
+ pageX: clientX,
794
+ pageY: clientY,
795
+ screenX: clientX,
796
+ screenY: clientY,
797
+ radiusX: 1,
798
+ radiusY: 1,
799
+ rotationAngle: 0,
800
+ force: 1
801
+ };
802
+ };
803
+ const dispatchTouch = async (type, x, y, identifier) => {
804
+ if (type === "touchstart" && activeTouches.has(identifier)) {
805
+ throw new Error(
806
+ `HEADLESS touch identifier ${identifier} is already active`
807
+ );
808
+ }
809
+ if (type !== "touchstart" && !activeTouches.has(identifier)) {
810
+ throw new Error(`HEADLESS touch identifier ${identifier} is not active`);
811
+ }
812
+ const touchTarget = getEventTarget(readyGame.input.touch?.target, "touch");
813
+ const changedTouch = createTouch(x, y, identifier, touchTarget);
814
+ if (type === "touchstart" || type === "touchmove") {
815
+ activeTouches.set(identifier, changedTouch);
816
+ } else {
817
+ activeTouches.delete(identifier);
818
+ }
819
+ const touches = [...activeTouches.values()];
820
+ const event = new inputWindow.Event(type, {
821
+ bubbles: true,
822
+ cancelable: true
823
+ });
824
+ Object.defineProperties(event, {
825
+ changedTouches: { value: [changedTouch] },
826
+ targetTouches: { value: touches },
827
+ touches: { value: touches }
828
+ });
829
+ const document = inputWindow.document;
830
+ const originalElementFromPoint = document.elementFromPoint;
831
+ const originalElementFromPointDescriptor = Object.getOwnPropertyDescriptor(
832
+ document,
833
+ "elementFromPoint"
834
+ );
835
+ Object.defineProperty(document, "elementFromPoint", {
836
+ configurable: true,
837
+ value(clientX, clientY) {
838
+ const insideCanvas = clientX >= canvasBounds.left && clientX <= canvasBounds.left + canvasBounds.width && clientY >= canvasBounds.top && clientY <= canvasBounds.top + canvasBounds.height;
839
+ return insideCanvas ? canvas : originalElementFromPoint?.call(document, clientX, clientY) ?? null;
840
+ }
841
+ });
842
+ try {
843
+ touchTarget.dispatchEvent(event);
844
+ evidence.touchEvents += 1;
845
+ } finally {
846
+ if (originalElementFromPointDescriptor) {
847
+ Object.defineProperty(
848
+ document,
849
+ "elementFromPoint",
850
+ originalElementFromPointDescriptor
851
+ );
852
+ } else {
853
+ Reflect.deleteProperty(document, "elementFromPoint");
854
+ }
855
+ }
613
856
  await settleRuntime();
614
857
  };
615
858
  const input = {
@@ -627,6 +870,24 @@ async function createHeadlessGame(scene, options = {}) {
627
870
  await dispatchMouse("mousedown", x, y, 1);
628
871
  await dispatchMouse("mouseup", x, y, 0);
629
872
  }
873
+ },
874
+ keyboard: {
875
+ down: (keyCode, options2) => dispatchKeyboard("keydown", keyCode, options2),
876
+ up: (keyCode, options2) => dispatchKeyboard("keyup", keyCode, options2),
877
+ async press(keyCode, options2) {
878
+ await dispatchKeyboard("keydown", keyCode, options2);
879
+ await dispatchKeyboard("keyup", keyCode, options2);
880
+ }
881
+ },
882
+ touch: {
883
+ start: (x, y, identifier = 1) => dispatchTouch("touchstart", x, y, identifier),
884
+ move: (x, y, identifier = 1) => dispatchTouch("touchmove", x, y, identifier),
885
+ end: (x, y, identifier = 1) => dispatchTouch("touchend", x, y, identifier),
886
+ cancel: (x, y, identifier = 1) => dispatchTouch("touchcancel", x, y, identifier),
887
+ async tap(x, y, identifier = 1) {
888
+ await dispatchTouch("touchstart", x, y, identifier);
889
+ await dispatchTouch("touchend", x, y, identifier);
890
+ }
630
891
  }
631
892
  };
632
893
  const frameDurationMs = 1e3 / (config.fps?.target ?? 60);
@@ -635,6 +896,7 @@ async function createHeadlessGame(scene, options = {}) {
635
896
  evidence.frames += 1;
636
897
  };
637
898
  const stepFrames = (count = 1) => {
899
+ assertStepCount(count, "stepFrames count");
638
900
  throwRuntimeError();
639
901
  for (let frame = 0; frame < count; frame += 1) stepFrame();
640
902
  throwRuntimeError();
@@ -658,21 +920,59 @@ async function createHeadlessGame(scene, options = {}) {
658
920
  input,
659
921
  evidence,
660
922
  assertGameplayEvidence(requirements = {}) {
661
- if (requirements.requireInput && evidence.mouseEvents === 0) {
662
- throw new Error("Gameplay evidence is missing real HEADLESS input events.");
923
+ const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
924
+ if (requirements.requireInput && inputEvents === 0) {
925
+ throw new Error(
926
+ "Gameplay evidence is missing real HEADLESS input events."
927
+ );
663
928
  }
664
929
  if (requirements.requireFrameAdvance && evidence.frames === 0) {
665
- throw new Error("Gameplay evidence is missing complete Phaser frame advancement.");
930
+ throw new Error(
931
+ "Gameplay evidence is missing complete Phaser frame advancement."
932
+ );
666
933
  }
667
934
  if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
668
- throw new Error("Gameplay evidence is missing Arcade Physics advancement.");
935
+ throw new Error(
936
+ "Gameplay evidence is missing Arcade Physics advancement."
937
+ );
669
938
  }
670
939
  const requiredTransitions = requirements.requireTransition === void 0 ? [] : typeof requirements.requireTransition === "string" ? [requirements.requireTransition] : requirements.requireTransition;
671
940
  for (const target of requiredTransitions) {
672
941
  if (!evidence.transitions.some((transition) => transition.to === target)) {
673
- throw new Error(`Gameplay evidence is missing a transition to ${target}.`);
942
+ throw new Error(
943
+ `Gameplay evidence is missing a transition to ${target}.`
944
+ );
945
+ }
946
+ }
947
+ const requiredScenes = requirements.requireScene === void 0 ? [] : typeof requirements.requireScene === "string" ? [requirements.requireScene] : requirements.requireScene;
948
+ for (const sceneKey of requiredScenes) {
949
+ if (!evidence.visitedScenes.includes(sceneKey)) {
950
+ throw new Error(
951
+ `Gameplay evidence is missing a visit to Scene ${sceneKey}.`
952
+ );
953
+ }
954
+ }
955
+ const requiredRestarts = requirements.requireRestart === void 0 ? [] : typeof requirements.requireRestart === "string" ? [requirements.requireRestart] : requirements.requireRestart;
956
+ for (const sceneKey of requiredRestarts) {
957
+ if (!evidence.restartedScenes.includes(sceneKey)) {
958
+ throw new Error(
959
+ `Gameplay evidence is missing a restart of Scene ${sceneKey}.`
960
+ );
674
961
  }
675
962
  }
963
+ const requiredCheckpoints = requirements.requireCheckpoint === void 0 ? [] : typeof requirements.requireCheckpoint === "string" ? [requirements.requireCheckpoint] : requirements.requireCheckpoint;
964
+ for (const checkpoint of requiredCheckpoints) {
965
+ if (!evidence.checkpoints.includes(checkpoint)) {
966
+ throw new Error(
967
+ `Gameplay evidence is missing checkpoint ${checkpoint}.`
968
+ );
969
+ }
970
+ }
971
+ if (requirements.requireDestroy && !evidence.destroyed) {
972
+ throw new Error(
973
+ "Gameplay evidence is missing HEADLESS host destruction."
974
+ );
975
+ }
676
976
  },
677
977
  stepFrames,
678
978
  async stepFramesAsync(count = 1) {
@@ -680,6 +980,7 @@ async function createHeadlessGame(scene, options = {}) {
680
980
  await settleRuntime();
681
981
  },
682
982
  stepUntil(condition, maxFrames = 120) {
983
+ assertStepCount(maxFrames, "stepUntil maxFrames");
683
984
  throwRuntimeError();
684
985
  for (let frame = 0; frame <= maxFrames; frame += 1) {
685
986
  if (condition()) {
@@ -692,13 +993,18 @@ async function createHeadlessGame(scene, options = {}) {
692
993
  }
693
994
  }
694
995
  assertHealth();
695
- throw new Error(`Condition was not met within ${maxFrames} complete Phaser frames`);
996
+ throw new Error(
997
+ `Condition was not met within ${maxFrames} complete Phaser frames`
998
+ );
696
999
  },
697
1000
  stepPhysics(steps = 1) {
1001
+ assertStepCount(steps, "stepPhysics steps");
698
1002
  throwRuntimeError();
699
1003
  const world = scene.physics?.world;
700
1004
  if (!world || !getArcadeWorldBodies(world) || typeof world.singleStep !== "function") {
701
- throw new Error("stepPhysics() requires an Arcade Physics world; use stepFrames() for other configurations.");
1005
+ throw new Error(
1006
+ "stepPhysics() requires an Arcade Physics world; use stepFrames() for other configurations."
1007
+ );
702
1008
  }
703
1009
  for (let step = 0; step < steps; step += 1) {
704
1010
  scene.physics.world.singleStep();
@@ -707,6 +1013,15 @@ async function createHeadlessGame(scene, options = {}) {
707
1013
  throwRuntimeError();
708
1014
  assertHealth();
709
1015
  },
1016
+ checkpoint(id) {
1017
+ const normalizedId = id.trim();
1018
+ if (!normalizedId || normalizedId.length > 120) {
1019
+ throw new Error(
1020
+ "Gameplay checkpoint id must contain 1 to 120 non-whitespace characters."
1021
+ );
1022
+ }
1023
+ appendUnique(checkpoints, normalizedId);
1024
+ },
710
1025
  assertTextHealth() {
711
1026
  assertSceneTextHealth(scene);
712
1027
  },
@@ -719,6 +1034,7 @@ async function createHeadlessGame(scene, options = {}) {
719
1034
  readyGame.destroy(true);
720
1035
  readyGame.headlessStep(readyGame.loop.lastTime, 0);
721
1036
  throwRuntimeError();
1037
+ evidence.destroyed = true;
722
1038
  } finally {
723
1039
  removeRuntimeListeners();
724
1040
  restoreHostGuards();
@@ -728,18 +1044,79 @@ async function createHeadlessGame(scene, options = {}) {
728
1044
  }
729
1045
 
730
1046
  // src/gameplay-contracts.ts
731
- function registerGameplayContract(context, options) {
732
- const task = context;
733
- const metadata = {
734
- id: options.id,
735
- requirements: {
736
- requireInput: options.requireInput,
737
- requireFrameAdvance: options.requireFrameAdvance,
738
- requirePhysicsStep: options.requirePhysicsStep,
739
- requireTransition: options.requireTransition
740
- }
1047
+ import { test } from "vitest";
1048
+
1049
+ // src/gameplay-audit.ts
1050
+ function normalizeList(value) {
1051
+ if (value === void 0) return [];
1052
+ const values = typeof value === "string" ? [value] : value;
1053
+ return [...new Set(values.map((item) => item.trim()).filter(Boolean))];
1054
+ }
1055
+ function normalizeSet(values) {
1056
+ return [
1057
+ ...new Set(values.map((value) => value.trim()).filter(Boolean))
1058
+ ].sort();
1059
+ }
1060
+ function normalizeGameplayRequirements(requirements, scenes) {
1061
+ const requiredScenes = normalizeSet([
1062
+ ...scenes,
1063
+ ...normalizeList(requirements.requireScene)
1064
+ ]);
1065
+ return {
1066
+ requireInput: requirements.requireInput || void 0,
1067
+ requireFrameAdvance: requirements.requireFrameAdvance || void 0,
1068
+ requirePhysicsStep: requirements.requirePhysicsStep || void 0,
1069
+ requireTransition: normalizeList(requirements.requireTransition),
1070
+ requireScene: requiredScenes,
1071
+ requireRestart: normalizeList(requirements.requireRestart),
1072
+ requireCheckpoint: normalizeList(requirements.requireCheckpoint),
1073
+ requireDestroy: true
1074
+ };
1075
+ }
1076
+ function createGameplayContractMetadata(contract) {
1077
+ const scenes = normalizeSet(contract.scenes);
1078
+ return {
1079
+ id: contract.id.trim(),
1080
+ scenes,
1081
+ requirements: normalizeGameplayRequirements(contract, scenes),
1082
+ verified: false
1083
+ };
1084
+ }
1085
+
1086
+ // src/gameplay-contracts.ts
1087
+ function snapshotEvidence(evidence) {
1088
+ return {
1089
+ frames: evidence.frames,
1090
+ physicsSteps: evidence.physicsSteps,
1091
+ mouseEvents: evidence.mouseEvents,
1092
+ keyboardEvents: evidence.keyboardEvents,
1093
+ touchEvents: evidence.touchEvents,
1094
+ clicks: evidence.clicks,
1095
+ registeredScenes: [...evidence.registeredScenes],
1096
+ visitedScenes: [...evidence.visitedScenes],
1097
+ restartedScenes: [...evidence.restartedScenes],
1098
+ checkpoints: [...evidence.checkpoints],
1099
+ destroyed: evidence.destroyed,
1100
+ transitions: evidence.transitions.map((transition) => ({ ...transition }))
741
1101
  };
742
- task.task.meta.gameplayContract = metadata;
1102
+ }
1103
+ function readStaticMetadata(context, options) {
1104
+ const metadata = context.task.meta.gameplayContract;
1105
+ if (!metadata || typeof metadata !== "object") return void 0;
1106
+ const candidate = metadata;
1107
+ if (candidate.id !== options.id) {
1108
+ throw new Error(
1109
+ `\u9759\u6001 gameplay contract "${candidate.id}" \u4E0E\u8FD0\u884C\u65F6 contract "${options.id}" \u4E0D\u4E00\u81F4\u3002`
1110
+ );
1111
+ }
1112
+ return candidate;
1113
+ }
1114
+ function registerGameplayContract(context, options) {
1115
+ const metadata = readStaticMetadata(context, options) ?? createGameplayContractMetadata({
1116
+ ...options,
1117
+ scenes: options.scenes ?? []
1118
+ });
1119
+ context.task.meta.gameplayContract = metadata;
743
1120
  let host;
744
1121
  let verified = false;
745
1122
  let testFailed = false;
@@ -749,16 +1126,13 @@ function registerGameplayContract(context, options) {
749
1126
  const verify = () => {
750
1127
  if (verified) return;
751
1128
  if (!host) {
752
- throw new Error(`Gameplay contract "${options.id}" did not attach a HEADLESS host.`);
1129
+ throw new Error(
1130
+ `Gameplay contract "${options.id}" \u6CA1\u6709\u7ED1\u5B9A HEADLESS host\u3002`
1131
+ );
753
1132
  }
754
1133
  host.assertGameplayEvidence(metadata.requirements);
755
- metadata.evidence = {
756
- frames: host.evidence.frames,
757
- physicsSteps: host.evidence.physicsSteps,
758
- mouseEvents: host.evidence.mouseEvents,
759
- clicks: host.evidence.clicks,
760
- transitions: host.evidence.transitions.map((transition) => ({ ...transition }))
761
- };
1134
+ metadata.evidence = snapshotEvidence(host.evidence);
1135
+ metadata.verified = true;
762
1136
  verified = true;
763
1137
  };
764
1138
  context.onTestFinished(() => {
@@ -768,18 +1142,31 @@ function registerGameplayContract(context, options) {
768
1142
  return {
769
1143
  attachHost(currentHost) {
770
1144
  if (host) {
771
- throw new Error(`Gameplay contract "${options.id}" attached more than one host.`);
1145
+ throw new Error(
1146
+ `Gameplay contract "${options.id}" \u91CD\u590D\u7ED1\u5B9A\u4E86 HEADLESS host\u3002`
1147
+ );
772
1148
  }
773
1149
  host = currentHost;
774
1150
  },
775
1151
  verify
776
1152
  };
777
1153
  }
1154
+ function gameplayTest(name, options, run) {
1155
+ const metadata = createGameplayContractMetadata(options);
1156
+ test(name, {
1157
+ ...options.testOptions,
1158
+ meta: { gameplayContract: metadata }
1159
+ }, async (context) => {
1160
+ const contract = registerGameplayContract(context, options);
1161
+ await run({ context, contract });
1162
+ });
1163
+ }
778
1164
  export {
779
1165
  assertSceneInteractiveHealth,
780
1166
  assertSceneRuntimeHealth,
781
1167
  assertSceneTextHealth,
782
1168
  collectMissingBitmapGlyphs,
783
1169
  createHeadlessGame,
1170
+ gameplayTest,
784
1171
  registerGameplayContract
785
1172
  };