miaoda-game-devkit 0.2.17 → 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 +14 -9
- 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 +27 -2
- package/dist/react/testing.d.ts +27 -2
- package/dist/react/testing.js +114 -14
- package/dist/react/testing.mjs +113 -13
- package/dist/react/vitest-config.js +118 -17
- package/dist/react/vitest-config.mjs +117 -16
- 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 +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/phaser-headless-host.ts
|
|
2
|
-
import * as
|
|
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/
|
|
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
|
|
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
|
|
347
|
+
if (child instanceof Phaser2.GameObjects.Text) {
|
|
108
348
|
labels.push(child);
|
|
109
349
|
return;
|
|
110
350
|
}
|
|
111
|
-
if (child instanceof
|
|
351
|
+
if (child instanceof Phaser2.GameObjects.Container) {
|
|
112
352
|
child.list.forEach(visit);
|
|
113
353
|
return;
|
|
114
354
|
}
|
|
115
|
-
if (child instanceof
|
|
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
|
|
368
|
+
if (child instanceof Phaser2.GameObjects.BitmapText) {
|
|
129
369
|
labels.push(child);
|
|
130
370
|
return;
|
|
131
371
|
}
|
|
132
|
-
if (child instanceof
|
|
372
|
+
if (child instanceof Phaser2.GameObjects.Container) {
|
|
133
373
|
child.list.forEach(visit);
|
|
134
374
|
return;
|
|
135
375
|
}
|
|
136
|
-
if (child instanceof
|
|
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
|
|
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
|
|
493
|
+
if (object instanceof Phaser3.GameObjects.Container)
|
|
254
494
|
object.list.forEach(visit);
|
|
255
|
-
if (object instanceof
|
|
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
|
|
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
|
|
484
|
-
const registeredScenes = [];
|
|
485
|
-
const visitedScenes = [];
|
|
486
|
-
const restartedScenes = [];
|
|
723
|
+
const tracker = createHeadlessEvidenceTracker();
|
|
487
724
|
const checkpoints = [];
|
|
488
|
-
const
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
821
|
+
Phaser4.Loader.Events.FILE_LOAD_ERROR,
|
|
579
822
|
onFileLoadError
|
|
580
823
|
);
|
|
581
824
|
restoreGuards.push(() => {
|
|
582
825
|
currentScene.load.off(
|
|
583
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(
|
|
963
|
+
bootedGame.events.on(Phaser4.Core.Events.POST_STEP, checkReadiness);
|
|
710
964
|
removeReadinessCheck = () => {
|
|
711
965
|
bootedGame.events.off(
|
|
712
|
-
|
|
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
|
-
|
|
1148
|
+
Phaser4.Input.Keyboard.KeyCodes.LEFT,
|
|
891
1149
|
{ key: "ArrowLeft", code: "ArrowLeft" }
|
|
892
1150
|
],
|
|
893
1151
|
[
|
|
894
|
-
|
|
1152
|
+
Phaser4.Input.Keyboard.KeyCodes.RIGHT,
|
|
895
1153
|
{ key: "ArrowRight", code: "ArrowRight" }
|
|
896
1154
|
],
|
|
897
|
-
[
|
|
1155
|
+
[Phaser4.Input.Keyboard.KeyCodes.UP, { key: "ArrowUp", code: "ArrowUp" }],
|
|
898
1156
|
[
|
|
899
|
-
|
|
1157
|
+
Phaser4.Input.Keyboard.KeyCodes.DOWN,
|
|
900
1158
|
{ key: "ArrowDown", code: "ArrowDown" }
|
|
901
1159
|
],
|
|
902
|
-
[
|
|
903
|
-
[
|
|
904
|
-
[
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|