miaoda-game-devkit 0.2.17 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ __export(src_exports, {
44
44
  module.exports = __toCommonJS(src_exports);
45
45
 
46
46
  // src/phaser-headless-host.ts
47
- var Phaser3 = __toESM(require("phaser"));
47
+ var Phaser4 = __toESM(require("phaser"));
48
48
 
49
49
  // src/phaser-engine-warnings.ts
50
50
  var originalWarn;
@@ -115,8 +115,248 @@ function matchesEngineWarning(message, matcher) {
115
115
  return matcher.test(message);
116
116
  }
117
117
 
118
- // src/phaser-text-assertions.ts
118
+ // src/headless-evidence-scene-plugin.ts
119
119
  var Phaser = __toESM(require("phaser"));
120
+ function createHeadlessEvidenceTracker() {
121
+ return {
122
+ engine: {
123
+ sceneStarts: {},
124
+ sceneCreates: {},
125
+ sceneUpdates: {},
126
+ sceneShutdowns: {},
127
+ processedPointerEvents: 0,
128
+ processedGameObjectEvents: 0,
129
+ processedKeyboardEvents: 0,
130
+ arcadeWorldSteps: 0
131
+ },
132
+ registeredScenes: [],
133
+ visitedScenes: [],
134
+ restartedScenes: [],
135
+ restartRequests: [],
136
+ transitionRequests: [],
137
+ transitions: [],
138
+ pendingRestarts: [],
139
+ pendingTransitions: []
140
+ };
141
+ }
142
+ function requestHeadlessTransition(tracker, transition) {
143
+ tracker.transitionRequests.push(transition);
144
+ tracker.pendingTransitions.push(transition);
145
+ }
146
+ function requestHeadlessRestart(tracker, sceneKey) {
147
+ if (!sceneKey) return;
148
+ appendUnique(tracker.restartRequests, sceneKey);
149
+ tracker.pendingRestarts.push({
150
+ scene: sceneKey,
151
+ startCount: tracker.engine.sceneStarts[sceneKey] ?? 0,
152
+ createCount: tracker.engine.sceneCreates[sceneKey] ?? 0,
153
+ shutdownCount: tracker.engine.sceneShutdowns[sceneKey] ?? 0
154
+ });
155
+ }
156
+ var pluginSequence = 0;
157
+ function createHeadlessEvidenceScenePluginConfig(tracker) {
158
+ pluginSequence += 1;
159
+ const key = `__headlessEvidenceScenePlugin${pluginSequence}`;
160
+ const Plugin = class HeadlessEvidenceScenePlugin extends Phaser.Plugins.ScenePlugin {
161
+ currentInput;
162
+ currentKeyboard;
163
+ currentWorld;
164
+ /** 在 Scene BOOT 时注册一次生命周期监听;重启只会触发 START,不会重复注册。 */
165
+ boot() {
166
+ const scene = this.scene;
167
+ if (!scene) return;
168
+ const events = scene.sys.events;
169
+ events.once(Phaser.Scenes.Events.DESTROY, this.destroy, this);
170
+ if (scene.sys.settings.key === "__SYSTEM") return;
171
+ appendUnique(tracker.registeredScenes, scene.sys.settings.key);
172
+ events.on(Phaser.Scenes.Events.START, this.onStart, this);
173
+ events.on(Phaser.Scenes.Events.CREATE, this.onCreate, this);
174
+ events.on(Phaser.Scenes.Events.UPDATE, this.onUpdate, this);
175
+ events.on(Phaser.Scenes.Events.SLEEP, this.onSleep, this);
176
+ events.on(Phaser.Scenes.Events.WAKE, this.onWake, this);
177
+ events.on(Phaser.Scenes.Events.SHUTDOWN, this.onShutdown, this);
178
+ }
179
+ /** START 是 Scene 真正进入活动生命周期的证据,并在此时绑定本轮输入和物理实例。 */
180
+ onStart() {
181
+ const scene = this.scene;
182
+ if (!scene) return;
183
+ const sceneKey = scene.sys.settings.key;
184
+ increment(tracker.engine.sceneStarts, sceneKey);
185
+ appendUnique(tracker.visitedScenes, sceneKey);
186
+ this.bindInput(scene);
187
+ this.bindArcadeWorld(scene);
188
+ }
189
+ /** CREATE 由 SceneManager 在生产 create 调用返回后发布,比“调用过 create”更强。 */
190
+ onCreate() {
191
+ const scene = this.scene;
192
+ if (!scene) return;
193
+ const sceneKey = scene.sys.settings.key;
194
+ increment(tracker.engine.sceneCreates, sceneKey);
195
+ this.completeTransition(sceneKey, ["start", "launch", "switch"]);
196
+ const restartIndex = tracker.pendingRestarts.findIndex(
197
+ (request) => request.scene === sceneKey && (tracker.engine.sceneStarts[sceneKey] ?? 0) > request.startCount && (tracker.engine.sceneCreates[sceneKey] ?? 0) > request.createCount && (tracker.engine.sceneShutdowns[sceneKey] ?? 0) > request.shutdownCount
198
+ );
199
+ if (restartIndex >= 0) {
200
+ tracker.pendingRestarts.splice(restartIndex, 1);
201
+ appendUnique(tracker.restartedScenes, sceneKey);
202
+ }
203
+ }
204
+ /** UPDATE 证明目标 Scene 确实参与了游戏帧,而不只是 host 调用了 loop.step。 */
205
+ onUpdate() {
206
+ const scene = this.scene;
207
+ if (scene) increment(tracker.engine.sceneUpdates, scene.sys.settings.key);
208
+ }
209
+ onSleep() {
210
+ const sceneKey = this.scene?.sys.settings.key;
211
+ if (sceneKey) this.completeTransition(sceneKey, ["sleep"]);
212
+ }
213
+ onWake() {
214
+ const sceneKey = this.scene?.sys.settings.key;
215
+ if (sceneKey) this.completeTransition(sceneKey, ["wake", "switch"]);
216
+ }
217
+ /** SHUTDOWN 表示本轮生命周期结束;解绑外部 emitter,避免重启后重复计数。 */
218
+ onShutdown() {
219
+ const scene = this.scene;
220
+ if (scene) increment(tracker.engine.sceneShutdowns, scene.sys.settings.key);
221
+ this.unbindRuntimeEvents();
222
+ }
223
+ /** 将转场请求与目标 Scene 的真实生命周期结果配对。 */
224
+ completeTransition(sceneKey, methods) {
225
+ const index = tracker.pendingTransitions.findIndex(
226
+ (transition2) => transition2.to === sceneKey && methods.includes(transition2.method)
227
+ );
228
+ if (index < 0) return;
229
+ const [transition] = tracker.pendingTransitions.splice(index, 1);
230
+ if (transition) tracker.transitions.push(transition);
231
+ }
232
+ /** 监听 Phaser 已完成的输入分发;DOM dispatch 本身不计为引擎已消费。 */
233
+ bindInput(scene) {
234
+ this.currentInput = scene.input;
235
+ for (const event of [
236
+ Phaser.Input.Events.POINTER_DOWN,
237
+ Phaser.Input.Events.POINTER_DOWN_OUTSIDE,
238
+ Phaser.Input.Events.POINTER_MOVE,
239
+ Phaser.Input.Events.POINTER_UP,
240
+ Phaser.Input.Events.POINTER_UP_OUTSIDE
241
+ ]) {
242
+ this.currentInput.on(event, this.onPointerInput, this);
243
+ }
244
+ for (const event of [
245
+ Phaser.Input.Events.GAMEOBJECT_DOWN,
246
+ Phaser.Input.Events.GAMEOBJECT_MOVE,
247
+ Phaser.Input.Events.GAMEOBJECT_UP,
248
+ Phaser.Input.Events.DRAG_START,
249
+ Phaser.Input.Events.DRAG,
250
+ Phaser.Input.Events.DRAG_END
251
+ ]) {
252
+ this.currentInput.on(event, this.onGameObjectInput, this);
253
+ }
254
+ const keyboard = scene.input.keyboard;
255
+ if (keyboard) {
256
+ this.currentKeyboard = keyboard;
257
+ keyboard.on(
258
+ Phaser.Input.Keyboard.Events.ANY_KEY_DOWN,
259
+ this.onKeyboardInput,
260
+ this
261
+ );
262
+ keyboard.on(
263
+ Phaser.Input.Keyboard.Events.ANY_KEY_UP,
264
+ this.onKeyboardInput,
265
+ this
266
+ );
267
+ }
268
+ }
269
+ /** WORLD_STEP 由 Arcade World 在实际 step 末尾发布,可验证物理推进并非只被请求。 */
270
+ bindArcadeWorld(scene) {
271
+ const world = scene.physics?.world;
272
+ if (!world) return;
273
+ this.currentWorld = world;
274
+ world.on(
275
+ Phaser.Physics.Arcade.Events.WORLD_STEP,
276
+ this.onArcadeWorldStep,
277
+ this
278
+ );
279
+ }
280
+ onPointerInput() {
281
+ tracker.engine.processedPointerEvents += 1;
282
+ }
283
+ onGameObjectInput() {
284
+ tracker.engine.processedGameObjectEvents += 1;
285
+ }
286
+ onKeyboardInput() {
287
+ tracker.engine.processedKeyboardEvents += 1;
288
+ }
289
+ onArcadeWorldStep() {
290
+ tracker.engine.arcadeWorldSteps += 1;
291
+ }
292
+ /** 仅移除本插件拥有的监听,不触碰游戏注册的输入和物理回调。 */
293
+ unbindRuntimeEvents() {
294
+ if (this.currentInput) {
295
+ for (const event of [
296
+ Phaser.Input.Events.POINTER_DOWN,
297
+ Phaser.Input.Events.POINTER_DOWN_OUTSIDE,
298
+ Phaser.Input.Events.POINTER_MOVE,
299
+ Phaser.Input.Events.POINTER_UP,
300
+ Phaser.Input.Events.POINTER_UP_OUTSIDE
301
+ ]) {
302
+ this.currentInput.off(event, this.onPointerInput, this);
303
+ }
304
+ for (const event of [
305
+ Phaser.Input.Events.GAMEOBJECT_DOWN,
306
+ Phaser.Input.Events.GAMEOBJECT_MOVE,
307
+ Phaser.Input.Events.GAMEOBJECT_UP,
308
+ Phaser.Input.Events.DRAG_START,
309
+ Phaser.Input.Events.DRAG,
310
+ Phaser.Input.Events.DRAG_END
311
+ ]) {
312
+ this.currentInput.off(event, this.onGameObjectInput, this);
313
+ }
314
+ }
315
+ this.currentKeyboard?.off(
316
+ Phaser.Input.Keyboard.Events.ANY_KEY_DOWN,
317
+ this.onKeyboardInput,
318
+ this
319
+ );
320
+ this.currentKeyboard?.off(
321
+ Phaser.Input.Keyboard.Events.ANY_KEY_UP,
322
+ this.onKeyboardInput,
323
+ this
324
+ );
325
+ this.currentWorld?.off(
326
+ Phaser.Physics.Arcade.Events.WORLD_STEP,
327
+ this.onArcadeWorldStep,
328
+ this
329
+ );
330
+ this.currentInput = void 0;
331
+ this.currentKeyboard = void 0;
332
+ this.currentWorld = void 0;
333
+ }
334
+ destroy() {
335
+ this.unbindRuntimeEvents();
336
+ const scene = this.scene;
337
+ if (scene) {
338
+ const events = scene.sys.events;
339
+ events.off(Phaser.Scenes.Events.START, this.onStart, this);
340
+ events.off(Phaser.Scenes.Events.CREATE, this.onCreate, this);
341
+ events.off(Phaser.Scenes.Events.UPDATE, this.onUpdate, this);
342
+ events.off(Phaser.Scenes.Events.SLEEP, this.onSleep, this);
343
+ events.off(Phaser.Scenes.Events.WAKE, this.onWake, this);
344
+ events.off(Phaser.Scenes.Events.SHUTDOWN, this.onShutdown, this);
345
+ }
346
+ super.destroy();
347
+ }
348
+ };
349
+ return { key, plugin: Plugin, mapping: key };
350
+ }
351
+ function appendUnique(values, value) {
352
+ if (value && !values.includes(value)) values.push(value);
353
+ }
354
+ function increment(values, key) {
355
+ if (key) values[key] = (values[key] ?? 0) + 1;
356
+ }
357
+
358
+ // src/phaser-text-assertions.ts
359
+ var Phaser2 = __toESM(require("phaser"));
120
360
  function describeText(label) {
121
361
  const name = label.name ? ` name=${JSON.stringify(label.name)}` : "";
122
362
  const value = label.text.length > 80 ? `${label.text.slice(0, 77)}...` : label.text;
@@ -132,7 +372,7 @@ function isEffectivelyRenderable(label) {
132
372
  if (state.visible === false || state.alpha === 0 || state.scaleX === 0 || state.scaleY === 0) {
133
373
  return false;
134
374
  }
135
- current = state.parentContainer ?? (state.displayList instanceof Phaser.GameObjects.Layer ? state.displayList : null);
375
+ current = state.parentContainer ?? (state.displayList instanceof Phaser2.GameObjects.Layer ? state.displayList : null);
136
376
  }
137
377
  return true;
138
378
  }
@@ -149,15 +389,15 @@ function collectSceneTexts(scene) {
149
389
  const visit = (child) => {
150
390
  if (visited.has(child)) return;
151
391
  visited.add(child);
152
- if (child instanceof Phaser.GameObjects.Text) {
392
+ if (child instanceof Phaser2.GameObjects.Text) {
153
393
  labels.push(child);
154
394
  return;
155
395
  }
156
- if (child instanceof Phaser.GameObjects.Container) {
396
+ if (child instanceof Phaser2.GameObjects.Container) {
157
397
  child.list.forEach(visit);
158
398
  return;
159
399
  }
160
- if (child instanceof Phaser.GameObjects.Layer) {
400
+ if (child instanceof Phaser2.GameObjects.Layer) {
161
401
  child.getChildren().forEach(visit);
162
402
  }
163
403
  };
@@ -170,15 +410,15 @@ function collectSceneBitmapTexts(scene) {
170
410
  const visit = (child) => {
171
411
  if (visited.has(child)) return;
172
412
  visited.add(child);
173
- if (child instanceof Phaser.GameObjects.BitmapText) {
413
+ if (child instanceof Phaser2.GameObjects.BitmapText) {
174
414
  labels.push(child);
175
415
  return;
176
416
  }
177
- if (child instanceof Phaser.GameObjects.Container) {
417
+ if (child instanceof Phaser2.GameObjects.Container) {
178
418
  child.list.forEach(visit);
179
419
  return;
180
420
  }
181
- if (child instanceof Phaser.GameObjects.Layer) {
421
+ if (child instanceof Phaser2.GameObjects.Layer) {
182
422
  child.getChildren().forEach(visit);
183
423
  }
184
424
  };
@@ -287,7 +527,7 @@ function assertSceneTextHealth(scene) {
287
527
  }
288
528
 
289
529
  // src/phaser-runtime-assertions.ts
290
- var Phaser2 = __toESM(require("phaser"));
530
+ var Phaser3 = __toESM(require("phaser"));
291
531
  function collectObjects(scene) {
292
532
  const objects = [];
293
533
  const visited = /* @__PURE__ */ new Set();
@@ -295,9 +535,9 @@ function collectObjects(scene) {
295
535
  if (visited.has(object)) return;
296
536
  visited.add(object);
297
537
  objects.push(object);
298
- if (object instanceof Phaser2.GameObjects.Container)
538
+ if (object instanceof Phaser3.GameObjects.Container)
299
539
  object.list.forEach(visit);
300
- if (object instanceof Phaser2.GameObjects.Layer)
540
+ if (object instanceof Phaser3.GameObjects.Layer)
301
541
  object.getChildren().forEach(visit);
302
542
  };
303
543
  scene.children.getChildren().forEach(visit);
@@ -316,7 +556,7 @@ function isEffectivelyVisible(object) {
316
556
  if (state.visible === false || state.alpha === 0 || state.scaleX === 0 || state.scaleY === 0) {
317
557
  return false;
318
558
  }
319
- current = state.parentContainer ?? (state.displayList instanceof Phaser2.GameObjects.Layer ? state.displayList : null);
559
+ current = state.parentContainer ?? (state.displayList instanceof Phaser3.GameObjects.Layer ? state.displayList : null);
320
560
  }
321
561
  return true;
322
562
  }
@@ -525,12 +765,9 @@ async function createHeadlessGame(scene, options = {}) {
525
765
  }
526
766
  engineDiagnostics.push(diagnostic);
527
767
  });
528
- const transitions = [];
529
- const registeredScenes = [];
530
- const visitedScenes = [];
531
- const restartedScenes = [];
768
+ const tracker = createHeadlessEvidenceTracker();
532
769
  const checkpoints = [];
533
- const appendUnique = (values, value) => {
770
+ const appendUnique2 = (values, value) => {
534
771
  if (value && !values.includes(value)) values.push(value);
535
772
  };
536
773
  const assertStepCount = (value, name) => {
@@ -542,17 +779,23 @@ async function createHeadlessGame(scene, options = {}) {
542
779
  };
543
780
  const evidence = {
544
781
  frames: 0,
782
+ observedFrames: 0,
545
783
  physicsSteps: 0,
784
+ observedPhysicsSteps: 0,
546
785
  mouseEvents: 0,
547
786
  keyboardEvents: 0,
548
787
  touchEvents: 0,
549
788
  clicks: 0,
550
- registeredScenes,
551
- visitedScenes,
552
- restartedScenes,
789
+ consumedInputEvents: 0,
790
+ engine: tracker.engine,
791
+ registeredScenes: tracker.registeredScenes,
792
+ visitedScenes: tracker.visitedScenes,
793
+ restartRequests: tracker.restartRequests,
794
+ restartedScenes: tracker.restartedScenes,
553
795
  checkpoints,
554
796
  destroyed: false,
555
- transitions
797
+ transitionRequests: tracker.transitionRequests,
798
+ transitions: tracker.transitions
556
799
  };
557
800
  const created = new Promise((resolve, reject) => {
558
801
  const guardedScenes = /* @__PURE__ */ new WeakSet();
@@ -620,12 +863,12 @@ async function createHeadlessGame(scene, options = {}) {
620
863
  });
621
864
  };
622
865
  currentScene.load.on(
623
- Phaser3.Loader.Events.FILE_LOAD_ERROR,
866
+ Phaser4.Loader.Events.FILE_LOAD_ERROR,
624
867
  onFileLoadError
625
868
  );
626
869
  restoreGuards.push(() => {
627
870
  currentScene.load.off(
628
- Phaser3.Loader.Events.FILE_LOAD_ERROR,
871
+ Phaser4.Loader.Events.FILE_LOAD_ERROR,
629
872
  onFileLoadError
630
873
  );
631
874
  });
@@ -649,7 +892,9 @@ async function createHeadlessGame(scene, options = {}) {
649
892
  guardScene(target);
650
893
  const from = currentScene.sys.settings.key;
651
894
  const to = key === void 0 ? from : typeof key === "string" ? key : key.sys.settings.key;
652
- if (from && to) transitions.push({ from, to, method });
895
+ if (from && to) {
896
+ requestHeadlessTransition(tracker, { from, to, method });
897
+ }
653
898
  const callArgs = key === void 0 && args.length === 0 ? [] : [key, ...args];
654
899
  return Reflect.apply(original, currentScene.scene, callArgs);
655
900
  };
@@ -660,7 +905,7 @@ async function createHeadlessGame(scene, options = {}) {
660
905
  const originalRestart = scenePlugin.restart;
661
906
  if (typeof originalRestart === "function") {
662
907
  scenePlugin.restart = (...args) => {
663
- appendUnique(restartedScenes, currentScene.sys.settings.key);
908
+ requestHeadlessRestart(tracker, currentScene.sys.settings.key);
664
909
  return Reflect.apply(originalRestart, currentScene.scene, args);
665
910
  };
666
911
  restoreGuards.push(() => {
@@ -721,17 +966,26 @@ async function createHeadlessGame(scene, options = {}) {
721
966
  ...config.audio,
722
967
  noAudio: true
723
968
  };
969
+ const evidencePlugin = createHeadlessEvidenceScenePluginConfig(tracker);
970
+ const configuredPlugins = config.plugins;
971
+ const plugins = Array.isArray(
972
+ configuredPlugins
973
+ ) ? { global: configuredPlugins, scene: [evidencePlugin] } : {
974
+ ...configuredPlugins,
975
+ scene: [...configuredPlugins?.scene ?? [], evidencePlugin]
976
+ };
724
977
  try {
725
- game = new Phaser3.Game({
978
+ game = new Phaser4.Game({
726
979
  width: 320,
727
980
  height: 180,
728
981
  banner: false,
729
982
  autoFocus: false,
730
983
  seed: ["phaser-headless-test"],
731
984
  ...config,
732
- type: Phaser3.HEADLESS,
985
+ type: Phaser4.HEADLESS,
733
986
  fps,
734
987
  audio,
988
+ plugins,
735
989
  callbacks: {
736
990
  preBoot(bootedGame) {
737
991
  const manager = bootedGame.scene;
@@ -751,10 +1005,10 @@ async function createHeadlessGame(scene, options = {}) {
751
1005
  finish();
752
1006
  }
753
1007
  };
754
- bootedGame.events.on(Phaser3.Core.Events.POST_STEP, checkReadiness);
1008
+ bootedGame.events.on(Phaser4.Core.Events.POST_STEP, checkReadiness);
755
1009
  removeReadinessCheck = () => {
756
1010
  bootedGame.events.off(
757
- Phaser3.Core.Events.POST_STEP,
1011
+ Phaser4.Core.Events.POST_STEP,
758
1012
  checkReadiness
759
1013
  );
760
1014
  };
@@ -875,10 +1129,8 @@ async function createHeadlessGame(scene, options = {}) {
875
1129
  };
876
1130
  const assertHealth = () => {
877
1131
  for (const currentScene of readyGame.scene.getScenes(false)) {
878
- appendUnique(registeredScenes, currentScene.sys.settings.key);
879
1132
  if (!currentScene.sys.isActive() && !currentScene.sys.isPaused())
880
1133
  continue;
881
- appendUnique(visitedScenes, currentScene.sys.settings.key);
882
1134
  if (!allowIdleLoaderQueues.includes(currentScene.sys.settings.key) && !currentScene.load.isLoading() && currentScene.load.list.size > 0) {
883
1135
  const queuedFiles = [...currentScene.load.list].map(
884
1136
  (file) => `${file.type}:${String(file.key)}`
@@ -905,7 +1157,9 @@ async function createHeadlessGame(scene, options = {}) {
905
1157
  }
906
1158
  return target;
907
1159
  };
1160
+ const getProcessedInputEvents = () => evidence.engine.processedPointerEvents + evidence.engine.processedKeyboardEvents;
908
1161
  const dispatchMouse = async (type, x, y, buttons) => {
1162
+ const processedBefore = getProcessedInputEvents();
909
1163
  const { clientX, clientY } = gameToClient(x, y);
910
1164
  const mouseTarget = getEventTarget(readyGame.input.mouse?.target, "mouse");
911
1165
  mouseTarget.dispatchEvent(
@@ -919,6 +1173,10 @@ async function createHeadlessGame(scene, options = {}) {
919
1173
  })
920
1174
  );
921
1175
  evidence.mouseEvents += 1;
1176
+ evidence.consumedInputEvents += Math.max(
1177
+ 0,
1178
+ getProcessedInputEvents() - processedBefore
1179
+ );
922
1180
  await settleRuntime();
923
1181
  };
924
1182
  const keyIdentity = (keyCode) => {
@@ -932,25 +1190,26 @@ async function createHeadlessGame(scene, options = {}) {
932
1190
  }
933
1191
  const identities = /* @__PURE__ */ new Map([
934
1192
  [
935
- Phaser3.Input.Keyboard.KeyCodes.LEFT,
1193
+ Phaser4.Input.Keyboard.KeyCodes.LEFT,
936
1194
  { key: "ArrowLeft", code: "ArrowLeft" }
937
1195
  ],
938
1196
  [
939
- Phaser3.Input.Keyboard.KeyCodes.RIGHT,
1197
+ Phaser4.Input.Keyboard.KeyCodes.RIGHT,
940
1198
  { key: "ArrowRight", code: "ArrowRight" }
941
1199
  ],
942
- [Phaser3.Input.Keyboard.KeyCodes.UP, { key: "ArrowUp", code: "ArrowUp" }],
1200
+ [Phaser4.Input.Keyboard.KeyCodes.UP, { key: "ArrowUp", code: "ArrowUp" }],
943
1201
  [
944
- Phaser3.Input.Keyboard.KeyCodes.DOWN,
1202
+ Phaser4.Input.Keyboard.KeyCodes.DOWN,
945
1203
  { key: "ArrowDown", code: "ArrowDown" }
946
1204
  ],
947
- [Phaser3.Input.Keyboard.KeyCodes.SPACE, { key: " ", code: "Space" }],
948
- [Phaser3.Input.Keyboard.KeyCodes.ENTER, { key: "Enter", code: "Enter" }],
949
- [Phaser3.Input.Keyboard.KeyCodes.ESC, { key: "Escape", code: "Escape" }]
1205
+ [Phaser4.Input.Keyboard.KeyCodes.SPACE, { key: " ", code: "Space" }],
1206
+ [Phaser4.Input.Keyboard.KeyCodes.ENTER, { key: "Enter", code: "Enter" }],
1207
+ [Phaser4.Input.Keyboard.KeyCodes.ESC, { key: "Escape", code: "Escape" }]
950
1208
  ]);
951
1209
  return identities.get(keyCode) ?? { key: "", code: "" };
952
1210
  };
953
1211
  const dispatchKeyboard = async (type, keyCode, options2 = {}) => {
1212
+ const processedBefore = getProcessedInputEvents();
954
1213
  if (!Number.isInteger(keyCode) || keyCode < 0) {
955
1214
  throw new Error(
956
1215
  `HEADLESS keyboard keyCode must be a non-negative integer; received ${keyCode}`
@@ -979,6 +1238,10 @@ async function createHeadlessGame(scene, options = {}) {
979
1238
  );
980
1239
  keyboardTarget.dispatchEvent(event);
981
1240
  evidence.keyboardEvents += 1;
1241
+ evidence.consumedInputEvents += Math.max(
1242
+ 0,
1243
+ getProcessedInputEvents() - processedBefore
1244
+ );
982
1245
  await settleRuntime();
983
1246
  };
984
1247
  const activeTouches = /* @__PURE__ */ new Map();
@@ -1005,6 +1268,7 @@ async function createHeadlessGame(scene, options = {}) {
1005
1268
  };
1006
1269
  };
1007
1270
  const dispatchTouch = async (type, x, y, identifier) => {
1271
+ const processedBefore = getProcessedInputEvents();
1008
1272
  if (type === "touchstart" && activeTouches.has(identifier)) {
1009
1273
  throw new Error(
1010
1274
  `HEADLESS touch identifier ${identifier} is already active`
@@ -1046,6 +1310,10 @@ async function createHeadlessGame(scene, options = {}) {
1046
1310
  try {
1047
1311
  touchTarget.dispatchEvent(event);
1048
1312
  evidence.touchEvents += 1;
1313
+ evidence.consumedInputEvents += Math.max(
1314
+ 0,
1315
+ getProcessedInputEvents() - processedBefore
1316
+ );
1049
1317
  } finally {
1050
1318
  if (originalElementFromPointDescriptor) {
1051
1319
  Object.defineProperty(
@@ -1096,8 +1364,17 @@ async function createHeadlessGame(scene, options = {}) {
1096
1364
  };
1097
1365
  const frameDurationMs = 1e3 / (config.fps?.target ?? 60);
1098
1366
  const stepFrame = () => {
1367
+ const updatesBefore = Object.values(evidence.engine.sceneUpdates).reduce(
1368
+ (total, count) => total + count,
1369
+ 0
1370
+ );
1099
1371
  readyGame.loop.step(readyGame.loop.lastTime + frameDurationMs);
1100
1372
  evidence.frames += 1;
1373
+ const updatesAfter = Object.values(evidence.engine.sceneUpdates).reduce(
1374
+ (total, count) => total + count,
1375
+ 0
1376
+ );
1377
+ if (updatesAfter > updatesBefore) evidence.observedFrames += 1;
1101
1378
  };
1102
1379
  const stepFrames = (count = 1) => {
1103
1380
  assertStepCount(count, "stepFrames count");
@@ -1126,19 +1403,19 @@ async function createHeadlessGame(scene, options = {}) {
1126
1403
  evidence,
1127
1404
  assertGameplayEvidence(requirements = {}) {
1128
1405
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
1129
- if (requirements.requireInput && inputEvents === 0) {
1406
+ if (requirements.requireInput && (inputEvents === 0 || evidence.consumedInputEvents === 0)) {
1130
1407
  throw new Error(
1131
- "Gameplay evidence is missing real HEADLESS input events."
1408
+ "Gameplay evidence is missing DOM input consumed by Phaser InputPlugin."
1132
1409
  );
1133
1410
  }
1134
- if (requirements.requireFrameAdvance && evidence.frames === 0) {
1411
+ if (requirements.requireFrameAdvance && (evidence.frames === 0 || evidence.observedFrames === 0)) {
1135
1412
  throw new Error(
1136
- "Gameplay evidence is missing complete Phaser frame advancement."
1413
+ "Gameplay evidence is missing a complete frame observed by an active Phaser Scene."
1137
1414
  );
1138
1415
  }
1139
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
1416
+ if (requirements.requirePhysicsStep && (evidence.physicsSteps === 0 || evidence.observedPhysicsSteps === 0)) {
1140
1417
  throw new Error(
1141
- "Gameplay evidence is missing Arcade Physics advancement."
1418
+ "Gameplay evidence is missing an Arcade Physics step observed through WORLD_STEP."
1142
1419
  );
1143
1420
  }
1144
1421
  const requiredTransitions = requirements.requireTransition === void 0 ? [] : typeof requirements.requireTransition === "string" ? [requirements.requireTransition] : requirements.requireTransition;
@@ -1213,8 +1490,12 @@ async function createHeadlessGame(scene, options = {}) {
1213
1490
  );
1214
1491
  }
1215
1492
  for (let step = 0; step < steps; step += 1) {
1493
+ const worldStepsBefore = evidence.engine.arcadeWorldSteps;
1216
1494
  scene.physics.world.singleStep();
1217
1495
  evidence.physicsSteps += 1;
1496
+ if (evidence.engine.arcadeWorldSteps > worldStepsBefore) {
1497
+ evidence.observedPhysicsSteps += 1;
1498
+ }
1218
1499
  }
1219
1500
  throwRuntimeError();
1220
1501
  assertHealth();
@@ -1226,7 +1507,7 @@ async function createHeadlessGame(scene, options = {}) {
1226
1507
  "Gameplay checkpoint id must contain 1 to 120 non-whitespace characters."
1227
1508
  );
1228
1509
  }
1229
- appendUnique(checkpoints, normalizedId);
1510
+ appendUnique2(checkpoints, normalizedId);
1230
1511
  },
1231
1512
  assertTextHealth() {
1232
1513
  assertSceneTextHealth(scene);
@@ -1297,16 +1578,33 @@ function createGameplayContractMetadata(contract) {
1297
1578
  function snapshotEvidence(evidence) {
1298
1579
  return {
1299
1580
  frames: evidence.frames,
1581
+ observedFrames: evidence.observedFrames,
1300
1582
  physicsSteps: evidence.physicsSteps,
1583
+ observedPhysicsSteps: evidence.observedPhysicsSteps,
1301
1584
  mouseEvents: evidence.mouseEvents,
1302
1585
  keyboardEvents: evidence.keyboardEvents,
1303
1586
  touchEvents: evidence.touchEvents,
1304
1587
  clicks: evidence.clicks,
1588
+ consumedInputEvents: evidence.consumedInputEvents,
1589
+ engine: {
1590
+ sceneStarts: { ...evidence.engine.sceneStarts },
1591
+ sceneCreates: { ...evidence.engine.sceneCreates },
1592
+ sceneUpdates: { ...evidence.engine.sceneUpdates },
1593
+ sceneShutdowns: { ...evidence.engine.sceneShutdowns },
1594
+ processedPointerEvents: evidence.engine.processedPointerEvents,
1595
+ processedGameObjectEvents: evidence.engine.processedGameObjectEvents,
1596
+ processedKeyboardEvents: evidence.engine.processedKeyboardEvents,
1597
+ arcadeWorldSteps: evidence.engine.arcadeWorldSteps
1598
+ },
1305
1599
  registeredScenes: [...evidence.registeredScenes],
1306
1600
  visitedScenes: [...evidence.visitedScenes],
1601
+ restartRequests: [...evidence.restartRequests],
1307
1602
  restartedScenes: [...evidence.restartedScenes],
1308
1603
  checkpoints: [...evidence.checkpoints],
1309
1604
  destroyed: evidence.destroyed,
1605
+ transitionRequests: evidence.transitionRequests.map((transition) => ({
1606
+ ...transition
1607
+ })),
1310
1608
  transitions: evidence.transitions.map((transition) => ({ ...transition }))
1311
1609
  };
1312
1610
  }
@@ -1381,7 +1679,7 @@ function gameplayTest(name, options, run) {
1381
1679
  }
1382
1680
 
1383
1681
  // src/game-telemetry.ts
1384
- var Phaser4 = __toESM(require("phaser"));
1682
+ var Phaser5 = __toESM(require("phaser"));
1385
1683
  var GAME_TELEMETRY_GLOBAL = "gameTelemetry";
1386
1684
  function telemetryGlobal() {
1387
1685
  return globalThis;
@@ -1402,7 +1700,7 @@ function installGameTelemetry(owner, telemetry) {
1402
1700
  );
1403
1701
  }
1404
1702
  const events = owner.events;
1405
- const lifecycleEvents = owner instanceof Phaser4.Game ? [Phaser4.Core.Events.DESTROY] : [Phaser4.Scenes.Events.SHUTDOWN, Phaser4.Scenes.Events.DESTROY];
1703
+ const lifecycleEvents = owner instanceof Phaser5.Game ? [Phaser5.Core.Events.DESTROY] : [Phaser5.Scenes.Events.SHUTDOWN, Phaser5.Scenes.Events.DESTROY];
1406
1704
  let installed = true;
1407
1705
  const cleanup = () => {
1408
1706
  if (!installed) return;