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