miaoda-game-devkit 0.2.16 → 0.2.19

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
@@ -1,5 +1,5 @@
1
1
  // src/phaser-headless-host.ts
2
- import * as Phaser3 from "phaser";
2
+ import * as Phaser4 from "phaser";
3
3
 
4
4
  // src/phaser-engine-warnings.ts
5
5
  var originalWarn;
@@ -70,8 +70,248 @@ function matchesEngineWarning(message, matcher) {
70
70
  return matcher.test(message);
71
71
  }
72
72
 
73
- // src/phaser-text-assertions.ts
73
+ // src/headless-evidence-scene-plugin.ts
74
74
  import * as Phaser from "phaser";
75
+ function createHeadlessEvidenceTracker() {
76
+ return {
77
+ engine: {
78
+ sceneStarts: {},
79
+ sceneCreates: {},
80
+ sceneUpdates: {},
81
+ sceneShutdowns: {},
82
+ processedPointerEvents: 0,
83
+ processedGameObjectEvents: 0,
84
+ processedKeyboardEvents: 0,
85
+ arcadeWorldSteps: 0
86
+ },
87
+ registeredScenes: [],
88
+ visitedScenes: [],
89
+ restartedScenes: [],
90
+ restartRequests: [],
91
+ transitionRequests: [],
92
+ transitions: [],
93
+ pendingRestarts: [],
94
+ pendingTransitions: []
95
+ };
96
+ }
97
+ function requestHeadlessTransition(tracker, transition) {
98
+ tracker.transitionRequests.push(transition);
99
+ tracker.pendingTransitions.push(transition);
100
+ }
101
+ function requestHeadlessRestart(tracker, sceneKey) {
102
+ if (!sceneKey) return;
103
+ appendUnique(tracker.restartRequests, sceneKey);
104
+ tracker.pendingRestarts.push({
105
+ scene: sceneKey,
106
+ startCount: tracker.engine.sceneStarts[sceneKey] ?? 0,
107
+ createCount: tracker.engine.sceneCreates[sceneKey] ?? 0,
108
+ shutdownCount: tracker.engine.sceneShutdowns[sceneKey] ?? 0
109
+ });
110
+ }
111
+ var pluginSequence = 0;
112
+ function createHeadlessEvidenceScenePluginConfig(tracker) {
113
+ pluginSequence += 1;
114
+ const key = `__headlessEvidenceScenePlugin${pluginSequence}`;
115
+ const Plugin = class HeadlessEvidenceScenePlugin extends Phaser.Plugins.ScenePlugin {
116
+ currentInput;
117
+ currentKeyboard;
118
+ currentWorld;
119
+ /** 在 Scene BOOT 时注册一次生命周期监听;重启只会触发 START,不会重复注册。 */
120
+ boot() {
121
+ const scene = this.scene;
122
+ if (!scene) return;
123
+ const events = scene.sys.events;
124
+ events.once(Phaser.Scenes.Events.DESTROY, this.destroy, this);
125
+ if (scene.sys.settings.key === "__SYSTEM") return;
126
+ appendUnique(tracker.registeredScenes, scene.sys.settings.key);
127
+ events.on(Phaser.Scenes.Events.START, this.onStart, this);
128
+ events.on(Phaser.Scenes.Events.CREATE, this.onCreate, this);
129
+ events.on(Phaser.Scenes.Events.UPDATE, this.onUpdate, this);
130
+ events.on(Phaser.Scenes.Events.SLEEP, this.onSleep, this);
131
+ events.on(Phaser.Scenes.Events.WAKE, this.onWake, this);
132
+ events.on(Phaser.Scenes.Events.SHUTDOWN, this.onShutdown, this);
133
+ }
134
+ /** START 是 Scene 真正进入活动生命周期的证据,并在此时绑定本轮输入和物理实例。 */
135
+ onStart() {
136
+ const scene = this.scene;
137
+ if (!scene) return;
138
+ const sceneKey = scene.sys.settings.key;
139
+ increment(tracker.engine.sceneStarts, sceneKey);
140
+ appendUnique(tracker.visitedScenes, sceneKey);
141
+ this.bindInput(scene);
142
+ this.bindArcadeWorld(scene);
143
+ }
144
+ /** CREATE 由 SceneManager 在生产 create 调用返回后发布,比“调用过 create”更强。 */
145
+ onCreate() {
146
+ const scene = this.scene;
147
+ if (!scene) return;
148
+ const sceneKey = scene.sys.settings.key;
149
+ increment(tracker.engine.sceneCreates, sceneKey);
150
+ this.completeTransition(sceneKey, ["start", "launch", "switch"]);
151
+ const restartIndex = tracker.pendingRestarts.findIndex(
152
+ (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
153
+ );
154
+ if (restartIndex >= 0) {
155
+ tracker.pendingRestarts.splice(restartIndex, 1);
156
+ appendUnique(tracker.restartedScenes, sceneKey);
157
+ }
158
+ }
159
+ /** UPDATE 证明目标 Scene 确实参与了游戏帧,而不只是 host 调用了 loop.step。 */
160
+ onUpdate() {
161
+ const scene = this.scene;
162
+ if (scene) increment(tracker.engine.sceneUpdates, scene.sys.settings.key);
163
+ }
164
+ onSleep() {
165
+ const sceneKey = this.scene?.sys.settings.key;
166
+ if (sceneKey) this.completeTransition(sceneKey, ["sleep"]);
167
+ }
168
+ onWake() {
169
+ const sceneKey = this.scene?.sys.settings.key;
170
+ if (sceneKey) this.completeTransition(sceneKey, ["wake", "switch"]);
171
+ }
172
+ /** SHUTDOWN 表示本轮生命周期结束;解绑外部 emitter,避免重启后重复计数。 */
173
+ onShutdown() {
174
+ const scene = this.scene;
175
+ if (scene) increment(tracker.engine.sceneShutdowns, scene.sys.settings.key);
176
+ this.unbindRuntimeEvents();
177
+ }
178
+ /** 将转场请求与目标 Scene 的真实生命周期结果配对。 */
179
+ completeTransition(sceneKey, methods) {
180
+ const index = tracker.pendingTransitions.findIndex(
181
+ (transition2) => transition2.to === sceneKey && methods.includes(transition2.method)
182
+ );
183
+ if (index < 0) return;
184
+ const [transition] = tracker.pendingTransitions.splice(index, 1);
185
+ if (transition) tracker.transitions.push(transition);
186
+ }
187
+ /** 监听 Phaser 已完成的输入分发;DOM dispatch 本身不计为引擎已消费。 */
188
+ bindInput(scene) {
189
+ this.currentInput = scene.input;
190
+ for (const event of [
191
+ Phaser.Input.Events.POINTER_DOWN,
192
+ Phaser.Input.Events.POINTER_DOWN_OUTSIDE,
193
+ Phaser.Input.Events.POINTER_MOVE,
194
+ Phaser.Input.Events.POINTER_UP,
195
+ Phaser.Input.Events.POINTER_UP_OUTSIDE
196
+ ]) {
197
+ this.currentInput.on(event, this.onPointerInput, this);
198
+ }
199
+ for (const event of [
200
+ Phaser.Input.Events.GAMEOBJECT_DOWN,
201
+ Phaser.Input.Events.GAMEOBJECT_MOVE,
202
+ Phaser.Input.Events.GAMEOBJECT_UP,
203
+ Phaser.Input.Events.DRAG_START,
204
+ Phaser.Input.Events.DRAG,
205
+ Phaser.Input.Events.DRAG_END
206
+ ]) {
207
+ this.currentInput.on(event, this.onGameObjectInput, this);
208
+ }
209
+ const keyboard = scene.input.keyboard;
210
+ if (keyboard) {
211
+ this.currentKeyboard = keyboard;
212
+ keyboard.on(
213
+ Phaser.Input.Keyboard.Events.ANY_KEY_DOWN,
214
+ this.onKeyboardInput,
215
+ this
216
+ );
217
+ keyboard.on(
218
+ Phaser.Input.Keyboard.Events.ANY_KEY_UP,
219
+ this.onKeyboardInput,
220
+ this
221
+ );
222
+ }
223
+ }
224
+ /** WORLD_STEP 由 Arcade World 在实际 step 末尾发布,可验证物理推进并非只被请求。 */
225
+ bindArcadeWorld(scene) {
226
+ const world = scene.physics?.world;
227
+ if (!world) return;
228
+ this.currentWorld = world;
229
+ world.on(
230
+ Phaser.Physics.Arcade.Events.WORLD_STEP,
231
+ this.onArcadeWorldStep,
232
+ this
233
+ );
234
+ }
235
+ onPointerInput() {
236
+ tracker.engine.processedPointerEvents += 1;
237
+ }
238
+ onGameObjectInput() {
239
+ tracker.engine.processedGameObjectEvents += 1;
240
+ }
241
+ onKeyboardInput() {
242
+ tracker.engine.processedKeyboardEvents += 1;
243
+ }
244
+ onArcadeWorldStep() {
245
+ tracker.engine.arcadeWorldSteps += 1;
246
+ }
247
+ /** 仅移除本插件拥有的监听,不触碰游戏注册的输入和物理回调。 */
248
+ unbindRuntimeEvents() {
249
+ if (this.currentInput) {
250
+ for (const event of [
251
+ Phaser.Input.Events.POINTER_DOWN,
252
+ Phaser.Input.Events.POINTER_DOWN_OUTSIDE,
253
+ Phaser.Input.Events.POINTER_MOVE,
254
+ Phaser.Input.Events.POINTER_UP,
255
+ Phaser.Input.Events.POINTER_UP_OUTSIDE
256
+ ]) {
257
+ this.currentInput.off(event, this.onPointerInput, this);
258
+ }
259
+ for (const event of [
260
+ Phaser.Input.Events.GAMEOBJECT_DOWN,
261
+ Phaser.Input.Events.GAMEOBJECT_MOVE,
262
+ Phaser.Input.Events.GAMEOBJECT_UP,
263
+ Phaser.Input.Events.DRAG_START,
264
+ Phaser.Input.Events.DRAG,
265
+ Phaser.Input.Events.DRAG_END
266
+ ]) {
267
+ this.currentInput.off(event, this.onGameObjectInput, this);
268
+ }
269
+ }
270
+ this.currentKeyboard?.off(
271
+ Phaser.Input.Keyboard.Events.ANY_KEY_DOWN,
272
+ this.onKeyboardInput,
273
+ this
274
+ );
275
+ this.currentKeyboard?.off(
276
+ Phaser.Input.Keyboard.Events.ANY_KEY_UP,
277
+ this.onKeyboardInput,
278
+ this
279
+ );
280
+ this.currentWorld?.off(
281
+ Phaser.Physics.Arcade.Events.WORLD_STEP,
282
+ this.onArcadeWorldStep,
283
+ this
284
+ );
285
+ this.currentInput = void 0;
286
+ this.currentKeyboard = void 0;
287
+ this.currentWorld = void 0;
288
+ }
289
+ destroy() {
290
+ this.unbindRuntimeEvents();
291
+ const scene = this.scene;
292
+ if (scene) {
293
+ const events = scene.sys.events;
294
+ events.off(Phaser.Scenes.Events.START, this.onStart, this);
295
+ events.off(Phaser.Scenes.Events.CREATE, this.onCreate, this);
296
+ events.off(Phaser.Scenes.Events.UPDATE, this.onUpdate, this);
297
+ events.off(Phaser.Scenes.Events.SLEEP, this.onSleep, this);
298
+ events.off(Phaser.Scenes.Events.WAKE, this.onWake, this);
299
+ events.off(Phaser.Scenes.Events.SHUTDOWN, this.onShutdown, this);
300
+ }
301
+ super.destroy();
302
+ }
303
+ };
304
+ return { key, plugin: Plugin, mapping: key };
305
+ }
306
+ function appendUnique(values, value) {
307
+ if (value && !values.includes(value)) values.push(value);
308
+ }
309
+ function increment(values, key) {
310
+ if (key) values[key] = (values[key] ?? 0) + 1;
311
+ }
312
+
313
+ // src/phaser-text-assertions.ts
314
+ import * as Phaser2 from "phaser";
75
315
  function describeText(label) {
76
316
  const name = label.name ? ` name=${JSON.stringify(label.name)}` : "";
77
317
  const value = label.text.length > 80 ? `${label.text.slice(0, 77)}...` : label.text;
@@ -87,7 +327,7 @@ function isEffectivelyRenderable(label) {
87
327
  if (state.visible === false || state.alpha === 0 || state.scaleX === 0 || state.scaleY === 0) {
88
328
  return false;
89
329
  }
90
- current = state.parentContainer ?? (state.displayList instanceof Phaser.GameObjects.Layer ? state.displayList : null);
330
+ current = state.parentContainer ?? (state.displayList instanceof Phaser2.GameObjects.Layer ? state.displayList : null);
91
331
  }
92
332
  return true;
93
333
  }
@@ -104,15 +344,15 @@ function collectSceneTexts(scene) {
104
344
  const visit = (child) => {
105
345
  if (visited.has(child)) return;
106
346
  visited.add(child);
107
- if (child instanceof Phaser.GameObjects.Text) {
347
+ if (child instanceof Phaser2.GameObjects.Text) {
108
348
  labels.push(child);
109
349
  return;
110
350
  }
111
- if (child instanceof Phaser.GameObjects.Container) {
351
+ if (child instanceof Phaser2.GameObjects.Container) {
112
352
  child.list.forEach(visit);
113
353
  return;
114
354
  }
115
- if (child instanceof Phaser.GameObjects.Layer) {
355
+ if (child instanceof Phaser2.GameObjects.Layer) {
116
356
  child.getChildren().forEach(visit);
117
357
  }
118
358
  };
@@ -125,15 +365,15 @@ function collectSceneBitmapTexts(scene) {
125
365
  const visit = (child) => {
126
366
  if (visited.has(child)) return;
127
367
  visited.add(child);
128
- if (child instanceof Phaser.GameObjects.BitmapText) {
368
+ if (child instanceof Phaser2.GameObjects.BitmapText) {
129
369
  labels.push(child);
130
370
  return;
131
371
  }
132
- if (child instanceof Phaser.GameObjects.Container) {
372
+ if (child instanceof Phaser2.GameObjects.Container) {
133
373
  child.list.forEach(visit);
134
374
  return;
135
375
  }
136
- if (child instanceof Phaser.GameObjects.Layer) {
376
+ if (child instanceof Phaser2.GameObjects.Layer) {
137
377
  child.getChildren().forEach(visit);
138
378
  }
139
379
  };
@@ -242,7 +482,7 @@ function assertSceneTextHealth(scene) {
242
482
  }
243
483
 
244
484
  // src/phaser-runtime-assertions.ts
245
- import * as Phaser2 from "phaser";
485
+ import * as Phaser3 from "phaser";
246
486
  function collectObjects(scene) {
247
487
  const objects = [];
248
488
  const visited = /* @__PURE__ */ new Set();
@@ -250,9 +490,9 @@ function collectObjects(scene) {
250
490
  if (visited.has(object)) return;
251
491
  visited.add(object);
252
492
  objects.push(object);
253
- if (object instanceof Phaser2.GameObjects.Container)
493
+ if (object instanceof Phaser3.GameObjects.Container)
254
494
  object.list.forEach(visit);
255
- if (object instanceof Phaser2.GameObjects.Layer)
495
+ if (object instanceof Phaser3.GameObjects.Layer)
256
496
  object.getChildren().forEach(visit);
257
497
  };
258
498
  scene.children.getChildren().forEach(visit);
@@ -271,7 +511,7 @@ function isEffectivelyVisible(object) {
271
511
  if (state.visible === false || state.alpha === 0 || state.scaleX === 0 || state.scaleY === 0) {
272
512
  return false;
273
513
  }
274
- current = state.parentContainer ?? (state.displayList instanceof Phaser2.GameObjects.Layer ? state.displayList : null);
514
+ current = state.parentContainer ?? (state.displayList instanceof Phaser3.GameObjects.Layer ? state.displayList : null);
275
515
  }
276
516
  return true;
277
517
  }
@@ -480,12 +720,9 @@ async function createHeadlessGame(scene, options = {}) {
480
720
  }
481
721
  engineDiagnostics.push(diagnostic);
482
722
  });
483
- const transitions = [];
484
- const registeredScenes = [];
485
- const visitedScenes = [];
486
- const restartedScenes = [];
723
+ const tracker = createHeadlessEvidenceTracker();
487
724
  const checkpoints = [];
488
- const appendUnique = (values, value) => {
725
+ const appendUnique2 = (values, value) => {
489
726
  if (value && !values.includes(value)) values.push(value);
490
727
  };
491
728
  const assertStepCount = (value, name) => {
@@ -497,17 +734,23 @@ async function createHeadlessGame(scene, options = {}) {
497
734
  };
498
735
  const evidence = {
499
736
  frames: 0,
737
+ observedFrames: 0,
500
738
  physicsSteps: 0,
739
+ observedPhysicsSteps: 0,
501
740
  mouseEvents: 0,
502
741
  keyboardEvents: 0,
503
742
  touchEvents: 0,
504
743
  clicks: 0,
505
- registeredScenes,
506
- visitedScenes,
507
- restartedScenes,
744
+ consumedInputEvents: 0,
745
+ engine: tracker.engine,
746
+ registeredScenes: tracker.registeredScenes,
747
+ visitedScenes: tracker.visitedScenes,
748
+ restartRequests: tracker.restartRequests,
749
+ restartedScenes: tracker.restartedScenes,
508
750
  checkpoints,
509
751
  destroyed: false,
510
- transitions
752
+ transitionRequests: tracker.transitionRequests,
753
+ transitions: tracker.transitions
511
754
  };
512
755
  const created = new Promise((resolve, reject) => {
513
756
  const guardedScenes = /* @__PURE__ */ new WeakSet();
@@ -575,12 +818,12 @@ async function createHeadlessGame(scene, options = {}) {
575
818
  });
576
819
  };
577
820
  currentScene.load.on(
578
- Phaser3.Loader.Events.FILE_LOAD_ERROR,
821
+ Phaser4.Loader.Events.FILE_LOAD_ERROR,
579
822
  onFileLoadError
580
823
  );
581
824
  restoreGuards.push(() => {
582
825
  currentScene.load.off(
583
- Phaser3.Loader.Events.FILE_LOAD_ERROR,
826
+ Phaser4.Loader.Events.FILE_LOAD_ERROR,
584
827
  onFileLoadError
585
828
  );
586
829
  });
@@ -604,7 +847,9 @@ async function createHeadlessGame(scene, options = {}) {
604
847
  guardScene(target);
605
848
  const from = currentScene.sys.settings.key;
606
849
  const to = key === void 0 ? from : typeof key === "string" ? key : key.sys.settings.key;
607
- if (from && to) transitions.push({ from, to, method });
850
+ if (from && to) {
851
+ requestHeadlessTransition(tracker, { from, to, method });
852
+ }
608
853
  const callArgs = key === void 0 && args.length === 0 ? [] : [key, ...args];
609
854
  return Reflect.apply(original, currentScene.scene, callArgs);
610
855
  };
@@ -615,7 +860,7 @@ async function createHeadlessGame(scene, options = {}) {
615
860
  const originalRestart = scenePlugin.restart;
616
861
  if (typeof originalRestart === "function") {
617
862
  scenePlugin.restart = (...args) => {
618
- appendUnique(restartedScenes, currentScene.sys.settings.key);
863
+ requestHeadlessRestart(tracker, currentScene.sys.settings.key);
619
864
  return Reflect.apply(originalRestart, currentScene.scene, args);
620
865
  };
621
866
  restoreGuards.push(() => {
@@ -676,17 +921,26 @@ async function createHeadlessGame(scene, options = {}) {
676
921
  ...config.audio,
677
922
  noAudio: true
678
923
  };
924
+ const evidencePlugin = createHeadlessEvidenceScenePluginConfig(tracker);
925
+ const configuredPlugins = config.plugins;
926
+ const plugins = Array.isArray(
927
+ configuredPlugins
928
+ ) ? { global: configuredPlugins, scene: [evidencePlugin] } : {
929
+ ...configuredPlugins,
930
+ scene: [...configuredPlugins?.scene ?? [], evidencePlugin]
931
+ };
679
932
  try {
680
- game = new Phaser3.Game({
933
+ game = new Phaser4.Game({
681
934
  width: 320,
682
935
  height: 180,
683
936
  banner: false,
684
937
  autoFocus: false,
685
938
  seed: ["phaser-headless-test"],
686
939
  ...config,
687
- type: Phaser3.HEADLESS,
940
+ type: Phaser4.HEADLESS,
688
941
  fps,
689
942
  audio,
943
+ plugins,
690
944
  callbacks: {
691
945
  preBoot(bootedGame) {
692
946
  const manager = bootedGame.scene;
@@ -706,10 +960,10 @@ async function createHeadlessGame(scene, options = {}) {
706
960
  finish();
707
961
  }
708
962
  };
709
- bootedGame.events.on(Phaser3.Core.Events.POST_STEP, checkReadiness);
963
+ bootedGame.events.on(Phaser4.Core.Events.POST_STEP, checkReadiness);
710
964
  removeReadinessCheck = () => {
711
965
  bootedGame.events.off(
712
- Phaser3.Core.Events.POST_STEP,
966
+ Phaser4.Core.Events.POST_STEP,
713
967
  checkReadiness
714
968
  );
715
969
  };
@@ -830,10 +1084,8 @@ async function createHeadlessGame(scene, options = {}) {
830
1084
  };
831
1085
  const assertHealth = () => {
832
1086
  for (const currentScene of readyGame.scene.getScenes(false)) {
833
- appendUnique(registeredScenes, currentScene.sys.settings.key);
834
1087
  if (!currentScene.sys.isActive() && !currentScene.sys.isPaused())
835
1088
  continue;
836
- appendUnique(visitedScenes, currentScene.sys.settings.key);
837
1089
  if (!allowIdleLoaderQueues.includes(currentScene.sys.settings.key) && !currentScene.load.isLoading() && currentScene.load.list.size > 0) {
838
1090
  const queuedFiles = [...currentScene.load.list].map(
839
1091
  (file) => `${file.type}:${String(file.key)}`
@@ -860,7 +1112,9 @@ async function createHeadlessGame(scene, options = {}) {
860
1112
  }
861
1113
  return target;
862
1114
  };
1115
+ const getProcessedInputEvents = () => evidence.engine.processedPointerEvents + evidence.engine.processedKeyboardEvents;
863
1116
  const dispatchMouse = async (type, x, y, buttons) => {
1117
+ const processedBefore = getProcessedInputEvents();
864
1118
  const { clientX, clientY } = gameToClient(x, y);
865
1119
  const mouseTarget = getEventTarget(readyGame.input.mouse?.target, "mouse");
866
1120
  mouseTarget.dispatchEvent(
@@ -874,6 +1128,10 @@ async function createHeadlessGame(scene, options = {}) {
874
1128
  })
875
1129
  );
876
1130
  evidence.mouseEvents += 1;
1131
+ evidence.consumedInputEvents += Math.max(
1132
+ 0,
1133
+ getProcessedInputEvents() - processedBefore
1134
+ );
877
1135
  await settleRuntime();
878
1136
  };
879
1137
  const keyIdentity = (keyCode) => {
@@ -887,25 +1145,26 @@ async function createHeadlessGame(scene, options = {}) {
887
1145
  }
888
1146
  const identities = /* @__PURE__ */ new Map([
889
1147
  [
890
- Phaser3.Input.Keyboard.KeyCodes.LEFT,
1148
+ Phaser4.Input.Keyboard.KeyCodes.LEFT,
891
1149
  { key: "ArrowLeft", code: "ArrowLeft" }
892
1150
  ],
893
1151
  [
894
- Phaser3.Input.Keyboard.KeyCodes.RIGHT,
1152
+ Phaser4.Input.Keyboard.KeyCodes.RIGHT,
895
1153
  { key: "ArrowRight", code: "ArrowRight" }
896
1154
  ],
897
- [Phaser3.Input.Keyboard.KeyCodes.UP, { key: "ArrowUp", code: "ArrowUp" }],
1155
+ [Phaser4.Input.Keyboard.KeyCodes.UP, { key: "ArrowUp", code: "ArrowUp" }],
898
1156
  [
899
- Phaser3.Input.Keyboard.KeyCodes.DOWN,
1157
+ Phaser4.Input.Keyboard.KeyCodes.DOWN,
900
1158
  { key: "ArrowDown", code: "ArrowDown" }
901
1159
  ],
902
- [Phaser3.Input.Keyboard.KeyCodes.SPACE, { key: " ", code: "Space" }],
903
- [Phaser3.Input.Keyboard.KeyCodes.ENTER, { key: "Enter", code: "Enter" }],
904
- [Phaser3.Input.Keyboard.KeyCodes.ESC, { key: "Escape", code: "Escape" }]
1160
+ [Phaser4.Input.Keyboard.KeyCodes.SPACE, { key: " ", code: "Space" }],
1161
+ [Phaser4.Input.Keyboard.KeyCodes.ENTER, { key: "Enter", code: "Enter" }],
1162
+ [Phaser4.Input.Keyboard.KeyCodes.ESC, { key: "Escape", code: "Escape" }]
905
1163
  ]);
906
1164
  return identities.get(keyCode) ?? { key: "", code: "" };
907
1165
  };
908
1166
  const dispatchKeyboard = async (type, keyCode, options2 = {}) => {
1167
+ const processedBefore = getProcessedInputEvents();
909
1168
  if (!Number.isInteger(keyCode) || keyCode < 0) {
910
1169
  throw new Error(
911
1170
  `HEADLESS keyboard keyCode must be a non-negative integer; received ${keyCode}`
@@ -934,6 +1193,10 @@ async function createHeadlessGame(scene, options = {}) {
934
1193
  );
935
1194
  keyboardTarget.dispatchEvent(event);
936
1195
  evidence.keyboardEvents += 1;
1196
+ evidence.consumedInputEvents += Math.max(
1197
+ 0,
1198
+ getProcessedInputEvents() - processedBefore
1199
+ );
937
1200
  await settleRuntime();
938
1201
  };
939
1202
  const activeTouches = /* @__PURE__ */ new Map();
@@ -960,6 +1223,7 @@ async function createHeadlessGame(scene, options = {}) {
960
1223
  };
961
1224
  };
962
1225
  const dispatchTouch = async (type, x, y, identifier) => {
1226
+ const processedBefore = getProcessedInputEvents();
963
1227
  if (type === "touchstart" && activeTouches.has(identifier)) {
964
1228
  throw new Error(
965
1229
  `HEADLESS touch identifier ${identifier} is already active`
@@ -1001,6 +1265,10 @@ async function createHeadlessGame(scene, options = {}) {
1001
1265
  try {
1002
1266
  touchTarget.dispatchEvent(event);
1003
1267
  evidence.touchEvents += 1;
1268
+ evidence.consumedInputEvents += Math.max(
1269
+ 0,
1270
+ getProcessedInputEvents() - processedBefore
1271
+ );
1004
1272
  } finally {
1005
1273
  if (originalElementFromPointDescriptor) {
1006
1274
  Object.defineProperty(
@@ -1051,8 +1319,17 @@ async function createHeadlessGame(scene, options = {}) {
1051
1319
  };
1052
1320
  const frameDurationMs = 1e3 / (config.fps?.target ?? 60);
1053
1321
  const stepFrame = () => {
1322
+ const updatesBefore = Object.values(evidence.engine.sceneUpdates).reduce(
1323
+ (total, count) => total + count,
1324
+ 0
1325
+ );
1054
1326
  readyGame.loop.step(readyGame.loop.lastTime + frameDurationMs);
1055
1327
  evidence.frames += 1;
1328
+ const updatesAfter = Object.values(evidence.engine.sceneUpdates).reduce(
1329
+ (total, count) => total + count,
1330
+ 0
1331
+ );
1332
+ if (updatesAfter > updatesBefore) evidence.observedFrames += 1;
1056
1333
  };
1057
1334
  const stepFrames = (count = 1) => {
1058
1335
  assertStepCount(count, "stepFrames count");
@@ -1081,19 +1358,19 @@ async function createHeadlessGame(scene, options = {}) {
1081
1358
  evidence,
1082
1359
  assertGameplayEvidence(requirements = {}) {
1083
1360
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
1084
- if (requirements.requireInput && inputEvents === 0) {
1361
+ if (requirements.requireInput && (inputEvents === 0 || evidence.consumedInputEvents === 0)) {
1085
1362
  throw new Error(
1086
- "Gameplay evidence is missing real HEADLESS input events."
1363
+ "Gameplay evidence is missing DOM input consumed by Phaser InputPlugin."
1087
1364
  );
1088
1365
  }
1089
- if (requirements.requireFrameAdvance && evidence.frames === 0) {
1366
+ if (requirements.requireFrameAdvance && (evidence.frames === 0 || evidence.observedFrames === 0)) {
1090
1367
  throw new Error(
1091
- "Gameplay evidence is missing complete Phaser frame advancement."
1368
+ "Gameplay evidence is missing a complete frame observed by an active Phaser Scene."
1092
1369
  );
1093
1370
  }
1094
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
1371
+ if (requirements.requirePhysicsStep && (evidence.physicsSteps === 0 || evidence.observedPhysicsSteps === 0)) {
1095
1372
  throw new Error(
1096
- "Gameplay evidence is missing Arcade Physics advancement."
1373
+ "Gameplay evidence is missing an Arcade Physics step observed through WORLD_STEP."
1097
1374
  );
1098
1375
  }
1099
1376
  const requiredTransitions = requirements.requireTransition === void 0 ? [] : typeof requirements.requireTransition === "string" ? [requirements.requireTransition] : requirements.requireTransition;
@@ -1168,8 +1445,12 @@ async function createHeadlessGame(scene, options = {}) {
1168
1445
  );
1169
1446
  }
1170
1447
  for (let step = 0; step < steps; step += 1) {
1448
+ const worldStepsBefore = evidence.engine.arcadeWorldSteps;
1171
1449
  scene.physics.world.singleStep();
1172
1450
  evidence.physicsSteps += 1;
1451
+ if (evidence.engine.arcadeWorldSteps > worldStepsBefore) {
1452
+ evidence.observedPhysicsSteps += 1;
1453
+ }
1173
1454
  }
1174
1455
  throwRuntimeError();
1175
1456
  assertHealth();
@@ -1181,7 +1462,7 @@ async function createHeadlessGame(scene, options = {}) {
1181
1462
  "Gameplay checkpoint id must contain 1 to 120 non-whitespace characters."
1182
1463
  );
1183
1464
  }
1184
- appendUnique(checkpoints, normalizedId);
1465
+ appendUnique2(checkpoints, normalizedId);
1185
1466
  },
1186
1467
  assertTextHealth() {
1187
1468
  assertSceneTextHealth(scene);
@@ -1252,16 +1533,33 @@ function createGameplayContractMetadata(contract) {
1252
1533
  function snapshotEvidence(evidence) {
1253
1534
  return {
1254
1535
  frames: evidence.frames,
1536
+ observedFrames: evidence.observedFrames,
1255
1537
  physicsSteps: evidence.physicsSteps,
1538
+ observedPhysicsSteps: evidence.observedPhysicsSteps,
1256
1539
  mouseEvents: evidence.mouseEvents,
1257
1540
  keyboardEvents: evidence.keyboardEvents,
1258
1541
  touchEvents: evidence.touchEvents,
1259
1542
  clicks: evidence.clicks,
1543
+ consumedInputEvents: evidence.consumedInputEvents,
1544
+ engine: {
1545
+ sceneStarts: { ...evidence.engine.sceneStarts },
1546
+ sceneCreates: { ...evidence.engine.sceneCreates },
1547
+ sceneUpdates: { ...evidence.engine.sceneUpdates },
1548
+ sceneShutdowns: { ...evidence.engine.sceneShutdowns },
1549
+ processedPointerEvents: evidence.engine.processedPointerEvents,
1550
+ processedGameObjectEvents: evidence.engine.processedGameObjectEvents,
1551
+ processedKeyboardEvents: evidence.engine.processedKeyboardEvents,
1552
+ arcadeWorldSteps: evidence.engine.arcadeWorldSteps
1553
+ },
1260
1554
  registeredScenes: [...evidence.registeredScenes],
1261
1555
  visitedScenes: [...evidence.visitedScenes],
1556
+ restartRequests: [...evidence.restartRequests],
1262
1557
  restartedScenes: [...evidence.restartedScenes],
1263
1558
  checkpoints: [...evidence.checkpoints],
1264
1559
  destroyed: evidence.destroyed,
1560
+ transitionRequests: evidence.transitionRequests.map((transition) => ({
1561
+ ...transition
1562
+ })),
1265
1563
  transitions: evidence.transitions.map((transition) => ({ ...transition }))
1266
1564
  };
1267
1565
  }
@@ -1336,7 +1634,7 @@ function gameplayTest(name, options, run) {
1336
1634
  }
1337
1635
 
1338
1636
  // src/game-telemetry.ts
1339
- import * as Phaser4 from "phaser";
1637
+ import * as Phaser5 from "phaser";
1340
1638
  var GAME_TELEMETRY_GLOBAL = "gameTelemetry";
1341
1639
  function telemetryGlobal() {
1342
1640
  return globalThis;
@@ -1357,7 +1655,7 @@ function installGameTelemetry(owner, telemetry) {
1357
1655
  );
1358
1656
  }
1359
1657
  const events = owner.events;
1360
- const lifecycleEvents = owner instanceof Phaser4.Game ? [Phaser4.Core.Events.DESTROY] : [Phaser4.Scenes.Events.SHUTDOWN, Phaser4.Scenes.Events.DESTROY];
1658
+ const lifecycleEvents = owner instanceof Phaser5.Game ? [Phaser5.Core.Events.DESTROY] : [Phaser5.Scenes.Events.SHUTDOWN, Phaser5.Scenes.Events.DESTROY];
1361
1659
  let installed = true;
1362
1660
  const cleanup = () => {
1363
1661
  if (!installed) return;