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/README.md +22 -6
- package/dist/{gameplay-audit-DmbBA0M_.d.mts → gameplay-audit-CKgNYw2r.d.mts} +46 -13
- package/dist/{gameplay-audit-DmbBA0M_.d.ts → gameplay-audit-CKgNYw2r.d.ts} +46 -13
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +346 -48
- package/dist/index.mjs +346 -48
- package/dist/react/testing.d.mts +36 -2
- package/dist/react/testing.d.ts +36 -2
- package/dist/react/testing.js +141 -15
- package/dist/react/testing.mjs +130 -14
- package/dist/react/vitest-config.js +146 -18
- package/dist/react/vitest-config.mjs +135 -17
- package/dist/vitest-config.d.mts +1 -1
- package/dist/vitest-config.d.ts +1 -1
- package/dist/vitest-config.js +9 -9
- package/dist/vitest-config.mjs +9 -9
- package/package.json +5 -1
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
|
|
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/
|
|
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
|
|
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
|
|
392
|
+
if (child instanceof Phaser2.GameObjects.Text) {
|
|
153
393
|
labels.push(child);
|
|
154
394
|
return;
|
|
155
395
|
}
|
|
156
|
-
if (child instanceof
|
|
396
|
+
if (child instanceof Phaser2.GameObjects.Container) {
|
|
157
397
|
child.list.forEach(visit);
|
|
158
398
|
return;
|
|
159
399
|
}
|
|
160
|
-
if (child instanceof
|
|
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
|
|
413
|
+
if (child instanceof Phaser2.GameObjects.BitmapText) {
|
|
174
414
|
labels.push(child);
|
|
175
415
|
return;
|
|
176
416
|
}
|
|
177
|
-
if (child instanceof
|
|
417
|
+
if (child instanceof Phaser2.GameObjects.Container) {
|
|
178
418
|
child.list.forEach(visit);
|
|
179
419
|
return;
|
|
180
420
|
}
|
|
181
|
-
if (child instanceof
|
|
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
|
|
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
|
|
538
|
+
if (object instanceof Phaser3.GameObjects.Container)
|
|
299
539
|
object.list.forEach(visit);
|
|
300
|
-
if (object instanceof
|
|
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
|
|
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
|
|
529
|
-
const registeredScenes = [];
|
|
530
|
-
const visitedScenes = [];
|
|
531
|
-
const restartedScenes = [];
|
|
768
|
+
const tracker = createHeadlessEvidenceTracker();
|
|
532
769
|
const checkpoints = [];
|
|
533
|
-
const
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
866
|
+
Phaser4.Loader.Events.FILE_LOAD_ERROR,
|
|
624
867
|
onFileLoadError
|
|
625
868
|
);
|
|
626
869
|
restoreGuards.push(() => {
|
|
627
870
|
currentScene.load.off(
|
|
628
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(
|
|
1008
|
+
bootedGame.events.on(Phaser4.Core.Events.POST_STEP, checkReadiness);
|
|
755
1009
|
removeReadinessCheck = () => {
|
|
756
1010
|
bootedGame.events.off(
|
|
757
|
-
|
|
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
|
-
|
|
1193
|
+
Phaser4.Input.Keyboard.KeyCodes.LEFT,
|
|
936
1194
|
{ key: "ArrowLeft", code: "ArrowLeft" }
|
|
937
1195
|
],
|
|
938
1196
|
[
|
|
939
|
-
|
|
1197
|
+
Phaser4.Input.Keyboard.KeyCodes.RIGHT,
|
|
940
1198
|
{ key: "ArrowRight", code: "ArrowRight" }
|
|
941
1199
|
],
|
|
942
|
-
[
|
|
1200
|
+
[Phaser4.Input.Keyboard.KeyCodes.UP, { key: "ArrowUp", code: "ArrowUp" }],
|
|
943
1201
|
[
|
|
944
|
-
|
|
1202
|
+
Phaser4.Input.Keyboard.KeyCodes.DOWN,
|
|
945
1203
|
{ key: "ArrowDown", code: "ArrowDown" }
|
|
946
1204
|
],
|
|
947
|
-
[
|
|
948
|
-
[
|
|
949
|
-
[
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|