miaoda-game-devkit 0.2.7 → 0.2.9

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.
@@ -45,6 +45,7 @@ function createGameplayContractMetadata(contract) {
45
45
  }
46
46
  function validateGameplayAuditOptions(options2, projectRoot) {
47
47
  const issues = [];
48
+ const contracts = options2.contracts;
48
49
  const scenes = [...new Set(options2.scenes.map((scene) => scene.trim()).filter(Boolean))];
49
50
  const mode = options2.mode ?? "interactive";
50
51
  if (mode === "non-interactive") {
@@ -66,7 +67,7 @@ function validateGameplayAuditOptions(options2, projectRoot) {
66
67
  const coveredScenes = /* @__PURE__ */ new Set();
67
68
  const kinds = /* @__PURE__ */ new Map();
68
69
  const invalidKindContracts = [];
69
- for (const contract of options2.contracts) {
70
+ for (const contract of contracts ?? []) {
70
71
  const id = contract.id.trim();
71
72
  if (!id) {
72
73
  issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
@@ -92,7 +93,6 @@ function validateGameplayAuditOptions(options2, projectRoot) {
92
93
  kinds.set(contract.kind, list);
93
94
  }
94
95
  if (contract.kind === "playthrough") {
95
- if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
96
96
  if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
97
97
  if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
98
98
  }
@@ -100,7 +100,6 @@ function validateGameplayAuditOptions(options2, projectRoot) {
100
100
  issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
101
101
  }
102
102
  if (contract.kind === "recovery") {
103
- if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
104
103
  if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
105
104
  if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
106
105
  if (normalizeList(contract.requireCheckpoint).length < 2) issues.push(`recovery contract "${id}" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002`);
@@ -122,31 +121,24 @@ function validateGameplayAuditOptions(options2, projectRoot) {
122
121
  `\u4EE5\u4E0B contract \u7F3A\u5C11\u6709\u6548 kind\uFF1A${invalidKindContracts.map((id) => `"${id}"`).join("\u3001")}\u3002\u8BF7\u4E3A\u6BCF\u4E2A contract \u8BBE\u7F6E kind: "boot"\u3001"playthrough"\u3001"transition" \u6216 "recovery"\u3002`
123
122
  );
124
123
  }
125
- for (const scene of scenes) {
126
- if (!coveredScenes.has(scene)) {
127
- issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
124
+ if (contracts) {
125
+ for (const scene of scenes) {
126
+ if (!coveredScenes.has(scene)) {
127
+ issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
128
+ }
128
129
  }
130
+ if (contracts.length === 0)
131
+ issues.push("gameplayAudit.contracts \u5B58\u5728\u65F6\u4E0D\u80FD\u4E3A\u7A7A\uFF1B\u4E0D\u4F7F\u7528\u663E\u5F0F\u6E05\u5355\u8BF7\u7701\u7565\u8BE5\u5B57\u6BB5\u3002");
129
132
  }
130
- if (options2.contracts.length === 0)
131
- issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
132
- if (mode === "interactive") {
133
- const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
134
- const hasInput = options2.contracts.some((contract) => contract.requireInput);
135
- const hasCheckpoint = options2.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
136
- if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
133
+ if (mode === "interactive" && contracts) {
134
+ const hasPlaythrough = (kinds.get("playthrough") ?? []).some(
135
+ (contract) => Boolean(contract.requireFrameAdvance) && normalizeList(contract.requireCheckpoint).length > 0
136
+ );
137
+ const hasInput = contracts.some((contract) => contract.requireInput);
138
+ if (!hasPlaythrough || !hasInput) {
137
139
  issues.push(
138
- '\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A kind: "playthrough" contract \u5FC5\u987B\u540C\u65F6\u58F0\u660E requireInput: true\u3001requireFrameAdvance: true \u548C requireCheckpoint: ["progress"]\u3002'
139
- );
140
- }
141
- const entryScene = scenes[0];
142
- for (const scene of scenes.slice(1)) {
143
- const transitioned = options2.contracts.some(
144
- (contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
140
+ "\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A playthrough contract \u5FC5\u987B\u58F0\u660E requireFrameAdvance \u548C checkpoint\uFF0C\u4E14\u81F3\u5C11\u4E00\u4E2A contract \u5FC5\u987B\u58F0\u660E requireInput: true\u3002"
145
141
  );
146
- if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
147
- }
148
- if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
149
- issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
150
142
  }
151
143
  const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
152
144
  if (restartSignal && options2.flows?.restart !== true) {
@@ -208,11 +200,21 @@ function getDefinitionIssues(expected, actual) {
208
200
  }
209
201
  function auditGameplayCollection(options2, file, tests) {
210
202
  const normalizedFile = file.replaceAll("\\", "/");
211
- const expected = options2.contracts.filter(
203
+ const expected = (options2.contracts ?? []).filter(
212
204
  (contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
213
205
  );
214
206
  const actual = tests.filter((test) => test.metadata);
215
207
  const issues = [];
208
+ if (!options2.contracts) {
209
+ const actualIds = /* @__PURE__ */ new Set();
210
+ for (const test of actual) {
211
+ const id = test.metadata?.id;
212
+ if (!id) continue;
213
+ if (actualIds.has(id)) issues.push(`contract "${id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`);
214
+ actualIds.add(id);
215
+ }
216
+ return { passed: issues.length === 0, issues };
217
+ }
216
218
  for (const contract of expected) {
217
219
  const matches = actual.filter((test) => test.metadata?.id === contract.id);
218
220
  if (matches.length === 0) {
@@ -285,7 +287,25 @@ function auditGameplayRun(options2, tests) {
285
287
  const issues = [];
286
288
  const actualContracts = tests.filter((test) => test.metadata);
287
289
  const observedEvidence = [];
288
- for (const expected of options2.contracts) {
290
+ const discoveredContracts = actualContracts.flatMap((test) => {
291
+ const metadata = test.metadata;
292
+ if (!metadata) return [];
293
+ return [{
294
+ id: metadata.id,
295
+ kind: metadata.kind,
296
+ testFile: test.file,
297
+ scenes: metadata.scenes,
298
+ ...metadata.requirements
299
+ }];
300
+ });
301
+ const expectedContracts = options2.contracts ?? discoveredContracts;
302
+ if (!options2.contracts) {
303
+ const discoveredIssues = validateGameplayAuditOptions(
304
+ { ...options2, contracts: discoveredContracts }
305
+ );
306
+ issues.push(...discoveredIssues);
307
+ }
308
+ for (const expected of expectedContracts) {
289
309
  const matches = actualContracts.filter(
290
310
  (test2) => test2.metadata?.id === expected.id
291
311
  );
@@ -308,7 +328,11 @@ function auditGameplayRun(options2, tests) {
308
328
  }
309
329
  issues.push(...getDefinitionIssues(expected, metadata));
310
330
  if (test.state !== "passed") {
311
- issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
331
+ if (test.error) {
332
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u6D4B\u8BD5\u5931\u8D25\uFF1A${test.error}`);
333
+ } else {
334
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
335
+ }
312
336
  continue;
313
337
  }
314
338
  if (!metadata.verified || !metadata.evidence) {
@@ -324,7 +348,7 @@ function auditGameplayRun(options2, tests) {
324
348
  }
325
349
  for (const test of actualContracts) {
326
350
  const id = test.metadata?.id;
327
- if (id && !options2.contracts.some((contract) => contract.id === id)) {
351
+ if (id && options2.contracts && !options2.contracts.some((contract) => contract.id === id)) {
328
352
  issues.push(
329
353
  `\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
330
354
  );
@@ -383,7 +407,8 @@ function formatGameplayIssues(issues) {
383
407
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
384
408
  registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
385
409
  missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
386
- test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002"
410
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002",
411
+ unhandled: "\u5F02\u6B65\u9519\u8BEF\uFF1A\u5148\u4FEE\u590D\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u9519\u8BEF\uFF1B\u8F93\u5165\u3001\u5E27\u63A8\u8FDB\u548C\u751F\u547D\u5468\u671F\u64CD\u4F5C\u90FD\u8981\u5728\u6B63\u786E\u7684 host \u751F\u547D\u5468\u671F\u5185\u5E76\u4F7F\u7528 await\u3002"
387
412
  };
388
413
  const getCategory = (issue) => {
389
414
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -408,6 +433,14 @@ function formatGameplayIssues(issues) {
408
433
  return { category: "registry", key: `registry:${scene}` };
409
434
  }
410
435
  if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
436
+ if (issue.includes("\u6D4B\u8BD5\u5931\u8D25\uFF1A")) {
437
+ const message = issue.split("\u6D4B\u8BD5\u5931\u8D25\uFF1A")[1] ?? issue;
438
+ return { category: "test", key: `test:${message}` };
439
+ }
440
+ if (issue.includes("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")) {
441
+ const message = issue.split("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")[1] ?? issue;
442
+ return { category: "unhandled", key: `test:${message}` };
443
+ }
411
444
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
412
445
  return { category: "other", key: `other:${issue}` };
413
446
  };
@@ -431,7 +464,7 @@ function formatGameplayIssues(issues) {
431
464
  continue;
432
465
  }
433
466
  const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
434
- const id = compact.match(/contract "([^"]+)"/)?.[1];
467
+ const id = compact.match(/(?:contract|玩法测试) "([^"]+)"/)?.[1];
435
468
  const { category, key } = getCategory(compact);
436
469
  const group = groups.get(key);
437
470
  if (group) {
@@ -518,7 +551,7 @@ describe("gameplay audit", () => {
518
551
  ]
519
552
  });
520
553
  expect(result).toContain(
521
- '\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A kind: "playthrough" contract \u5FC5\u987B\u540C\u65F6\u58F0\u660E requireInput: true\u3001requireFrameAdvance: true \u548C requireCheckpoint: ["progress"]\u3002'
554
+ "\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A playthrough contract \u5FC5\u987B\u58F0\u660E requireFrameAdvance \u548C checkpoint\uFF0C\u4E14\u81F3\u5C11\u4E00\u4E2A contract \u5FC5\u987B\u58F0\u660E requireInput: true\u3002"
522
555
  );
523
556
  });
524
557
  it("\u5141\u8BB8\u5E26\u7406\u7531\u7684\u975E\u4EA4\u4E92 boot-only \u9879\u76EE", () => {
@@ -538,6 +571,44 @@ describe("gameplay audit", () => {
538
571
  })
539
572
  ).toEqual([]);
540
573
  });
574
+ it("\u5141\u8BB8 Telemetry \u957F\u6D41\u7A0B\u4E0E\u72EC\u7ACB\u771F\u5B9E\u8F93\u5165 contract \u5206\u5DE5", () => {
575
+ expect(
576
+ validateGameplayAuditOptions({
577
+ mode: "interactive",
578
+ scenes: ["IndexScene"],
579
+ contracts: [
580
+ {
581
+ id: "IndexScene.telemetry-playthrough",
582
+ kind: "playthrough",
583
+ testFile: "tests/gameplay.test.ts",
584
+ scenes: ["IndexScene"],
585
+ requireFrameAdvance: true,
586
+ requireCheckpoint: "progress"
587
+ },
588
+ {
589
+ id: "IndexScene.input-smoke",
590
+ kind: "playthrough",
591
+ testFile: "tests/gameplay.test.ts",
592
+ scenes: ["IndexScene"],
593
+ requireInput: true,
594
+ requireFrameAdvance: true,
595
+ requireCheckpoint: "input-wired"
596
+ }
597
+ ]
598
+ })
599
+ ).toEqual([]);
600
+ });
601
+ it("\u5141\u8BB8\u7701\u7565\u91CD\u590D inventory \u5E76\u4ECE gameplayTest metadata \u5BA1\u8BA1", () => {
602
+ const inventoryFree = {
603
+ mode: "interactive",
604
+ scenes: ["IndexScene", "BattleScene"]
605
+ };
606
+ expect(validateGameplayAuditOptions(inventoryFree)).toEqual([]);
607
+ expect(auditGameplayRun(inventoryFree, [createPassingTest()])).toEqual({
608
+ passed: true,
609
+ issues: []
610
+ });
611
+ });
541
612
  it("\u5F3A\u5236 recovery contract \u540C\u65F6\u58F0\u660E restart \u548C\u4E24\u4E2A checkpoint", () => {
542
613
  const result = validateGameplayAuditOptions({
543
614
  mode: "non-interactive",
@@ -557,26 +628,34 @@ describe("gameplay audit", () => {
557
628
  'recovery contract "BattleScene.recovery" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002'
558
629
  ]));
559
630
  });
560
- it("\u4EA4\u4E92\u9879\u76EE\u8981\u6C42\u6BCF\u4E2A\u975E\u5165\u53E3 Scene \u6709\u751F\u4EA7\u8F6C\u573A\u8BC1\u636E\u58F0\u660E", () => {
631
+ it("\u5141\u8BB8\u72EC\u7ACB\u8986\u76D6 Scene\uFF0C\u800C\u4E0D\u5F3A\u5236\u5236\u9020\u751F\u4EA7\u8F6C\u573A", () => {
561
632
  const result = validateGameplayAuditOptions({
562
633
  mode: "interactive",
563
634
  scenes: ["MenuScene", "BattleScene"],
564
- contracts: [{
565
- id: "MenuScene.playthrough",
566
- kind: "playthrough",
567
- testFile: "tests/gameplay.test.ts",
568
- scenes: ["MenuScene"],
569
- requireInput: true,
570
- requireFrameAdvance: true,
571
- requireCheckpoint: "progress"
572
- }]
635
+ contracts: [
636
+ {
637
+ id: "MenuScene.playthrough",
638
+ kind: "playthrough",
639
+ testFile: "tests/gameplay.test.ts",
640
+ scenes: ["MenuScene"],
641
+ requireInput: true,
642
+ requireFrameAdvance: true,
643
+ requireCheckpoint: "progress"
644
+ },
645
+ {
646
+ id: "BattleScene.boot",
647
+ kind: "boot",
648
+ testFile: "tests/gameplay.test.ts",
649
+ scenes: ["BattleScene"]
650
+ }
651
+ ]
573
652
  });
574
- expect(result).toContain('\u975E\u5165\u53E3 Scene "BattleScene" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002');
653
+ expect(result).toEqual([]);
575
654
  });
576
- it("\u4E0D\u63A5\u53D7 boot contract \u4F2A\u88C5\u6210\u751F\u4EA7\u8F6C\u573A", () => {
655
+ it("\u4ECD\u8981\u6C42 transition contract \u58F0\u660E\u76EE\u6807", () => {
577
656
  const result = validateGameplayAuditOptions({
578
657
  mode: "interactive",
579
- scenes: ["MenuScene", "BattleScene"],
658
+ scenes: ["MenuScene"],
580
659
  contracts: [
581
660
  {
582
661
  id: "MenuScene.playthrough",
@@ -588,16 +667,15 @@ describe("gameplay audit", () => {
588
667
  requireCheckpoint: "progress"
589
668
  },
590
669
  {
591
- id: "BattleScene.boot",
592
- kind: "boot",
670
+ id: "MenuScene.transition",
671
+ kind: "transition",
593
672
  testFile: "tests/gameplay.test.ts",
594
- scenes: ["BattleScene"],
595
- requireTransition: "BattleScene"
673
+ scenes: ["MenuScene"]
596
674
  }
597
675
  ]
598
676
  });
599
677
  expect(result).toContain(
600
- '\u975E\u5165\u53E3 Scene "BattleScene" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002'
678
+ 'transition contract "MenuScene.transition" \u5FC5\u987B\u58F0\u660E requireTransition\u3002'
601
679
  );
602
680
  });
603
681
  it("\u58F0\u660E restart flow \u540E\u4ECD\u5F3A\u5236 recovery contract", () => {
@@ -817,4 +895,15 @@ describe("gameplay audit", () => {
817
895
  expect(result.repairs).toHaveLength(1);
818
896
  expect(result.repairs[0]).toContain("vitest.config.ts");
819
897
  });
898
+ it("\u4FDD\u7559\u5E76\u5408\u5E76 Vitest \u8FD0\u884C\u65F6\u5F02\u5E38\u6D88\u606F", () => {
899
+ const result = formatGameplayIssues([
900
+ '\u73A9\u6CD5\u6D4B\u8BD5 "Map.transition" \u6D4B\u8BD5\u5931\u8D25\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events',
901
+ '\u73A9\u6CD5\u6D4B\u8BD5 "Editor.transition" \u6D4B\u8BD5\u5931\u8D25\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events',
902
+ "\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events"
903
+ ]);
904
+ expect(result.issues).toEqual([
905
+ '\u73A9\u6CD5\u6D4B\u8BD5 "Map.transition" \u6D4B\u8BD5\u5931\u8D25\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1AEditor.transition\uFF09'
906
+ ]);
907
+ expect(result.repairs[0]).toContain("\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF");
908
+ });
820
909
  });
@@ -1,5 +1,5 @@
1
1
  // src/lint/gameplay-contract.test.ts
2
- import * as Phaser4 from "phaser";
2
+ import * as Phaser5 from "phaser";
3
3
  import { describe, expect, it } from "vitest";
4
4
 
5
5
  // src/phaser-headless-host.ts
@@ -1168,8 +1168,11 @@ function registerGameplayContract(context, options) {
1168
1168
  };
1169
1169
  }
1170
1170
 
1171
+ // src/game-telemetry.ts
1172
+ import * as Phaser4 from "phaser";
1173
+
1171
1174
  // src/lint/gameplay-contract.test.ts
1172
- var InputScene = class extends Phaser4.Scene {
1175
+ var InputScene = class extends Phaser5.Scene {
1173
1176
  hits = 0;
1174
1177
  constructor() {
1175
1178
  super("InputScene");
@@ -50,6 +50,7 @@ function createGameplayContractMetadata(contract) {
50
50
  }
51
51
  function validateGameplayAuditOptions(options, projectRoot2) {
52
52
  const issues = [];
53
+ const contracts = options.contracts;
53
54
  const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
54
55
  const mode = options.mode ?? "interactive";
55
56
  if (mode === "non-interactive") {
@@ -71,7 +72,7 @@ function validateGameplayAuditOptions(options, projectRoot2) {
71
72
  const coveredScenes = /* @__PURE__ */ new Set();
72
73
  const kinds = /* @__PURE__ */ new Map();
73
74
  const invalidKindContracts = [];
74
- for (const contract of options.contracts) {
75
+ for (const contract of contracts ?? []) {
75
76
  const id = contract.id.trim();
76
77
  if (!id) {
77
78
  issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
@@ -97,7 +98,6 @@ function validateGameplayAuditOptions(options, projectRoot2) {
97
98
  kinds.set(contract.kind, list);
98
99
  }
99
100
  if (contract.kind === "playthrough") {
100
- if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
101
101
  if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
102
102
  if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
103
103
  }
@@ -105,7 +105,6 @@ function validateGameplayAuditOptions(options, projectRoot2) {
105
105
  issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
106
106
  }
107
107
  if (contract.kind === "recovery") {
108
- if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
109
108
  if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
110
109
  if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
111
110
  if (normalizeList(contract.requireCheckpoint).length < 2) issues.push(`recovery contract "${id}" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002`);
@@ -127,31 +126,24 @@ function validateGameplayAuditOptions(options, projectRoot2) {
127
126
  `\u4EE5\u4E0B contract \u7F3A\u5C11\u6709\u6548 kind\uFF1A${invalidKindContracts.map((id) => `"${id}"`).join("\u3001")}\u3002\u8BF7\u4E3A\u6BCF\u4E2A contract \u8BBE\u7F6E kind: "boot"\u3001"playthrough"\u3001"transition" \u6216 "recovery"\u3002`
128
127
  );
129
128
  }
130
- for (const scene of scenes) {
131
- if (!coveredScenes.has(scene)) {
132
- issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
129
+ if (contracts) {
130
+ for (const scene of scenes) {
131
+ if (!coveredScenes.has(scene)) {
132
+ issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
133
+ }
133
134
  }
135
+ if (contracts.length === 0)
136
+ issues.push("gameplayAudit.contracts \u5B58\u5728\u65F6\u4E0D\u80FD\u4E3A\u7A7A\uFF1B\u4E0D\u4F7F\u7528\u663E\u5F0F\u6E05\u5355\u8BF7\u7701\u7565\u8BE5\u5B57\u6BB5\u3002");
134
137
  }
135
- if (options.contracts.length === 0)
136
- issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
137
- if (mode === "interactive") {
138
- const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
139
- const hasInput = options.contracts.some((contract) => contract.requireInput);
140
- const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
141
- if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
138
+ if (mode === "interactive" && contracts) {
139
+ const hasPlaythrough = (kinds.get("playthrough") ?? []).some(
140
+ (contract) => Boolean(contract.requireFrameAdvance) && normalizeList(contract.requireCheckpoint).length > 0
141
+ );
142
+ const hasInput = contracts.some((contract) => contract.requireInput);
143
+ if (!hasPlaythrough || !hasInput) {
142
144
  issues.push(
143
- '\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A kind: "playthrough" contract \u5FC5\u987B\u540C\u65F6\u58F0\u660E requireInput: true\u3001requireFrameAdvance: true \u548C requireCheckpoint: ["progress"]\u3002'
144
- );
145
- }
146
- const entryScene = scenes[0];
147
- for (const scene of scenes.slice(1)) {
148
- const transitioned = options.contracts.some(
149
- (contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
145
+ "\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A playthrough contract \u5FC5\u987B\u58F0\u660E requireFrameAdvance \u548C checkpoint\uFF0C\u4E14\u81F3\u5C11\u4E00\u4E2A contract \u5FC5\u987B\u58F0\u660E requireInput: true\u3002"
150
146
  );
151
- if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
152
- }
153
- if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
154
- issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
155
147
  }
156
148
  const restartSignal = projectRoot2 ? containsProductionRestart(projectRoot2) : false;
157
149
  if (restartSignal && options.flows?.restart !== true) {
@@ -213,11 +205,21 @@ function getDefinitionIssues(expected, actual) {
213
205
  }
214
206
  function auditGameplayCollection(options, file, tests) {
215
207
  const normalizedFile = file.replaceAll("\\", "/");
216
- const expected = options.contracts.filter(
208
+ const expected = (options.contracts ?? []).filter(
217
209
  (contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
218
210
  );
219
211
  const actual = tests.filter((test) => test.metadata);
220
212
  const issues = [];
213
+ if (!options.contracts) {
214
+ const actualIds = /* @__PURE__ */ new Set();
215
+ for (const test of actual) {
216
+ const id = test.metadata?.id;
217
+ if (!id) continue;
218
+ if (actualIds.has(id)) issues.push(`contract "${id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`);
219
+ actualIds.add(id);
220
+ }
221
+ return { passed: issues.length === 0, issues };
222
+ }
221
223
  for (const contract of expected) {
222
224
  const matches = actual.filter((test) => test.metadata?.id === contract.id);
223
225
  if (matches.length === 0) {
@@ -290,7 +292,25 @@ function auditGameplayRun(options, tests) {
290
292
  const issues = [];
291
293
  const actualContracts = tests.filter((test) => test.metadata);
292
294
  const observedEvidence = [];
293
- for (const expected of options.contracts) {
295
+ const discoveredContracts = actualContracts.flatMap((test) => {
296
+ const metadata = test.metadata;
297
+ if (!metadata) return [];
298
+ return [{
299
+ id: metadata.id,
300
+ kind: metadata.kind,
301
+ testFile: test.file,
302
+ scenes: metadata.scenes,
303
+ ...metadata.requirements
304
+ }];
305
+ });
306
+ const expectedContracts = options.contracts ?? discoveredContracts;
307
+ if (!options.contracts) {
308
+ const discoveredIssues = validateGameplayAuditOptions(
309
+ { ...options, contracts: discoveredContracts }
310
+ );
311
+ issues.push(...discoveredIssues);
312
+ }
313
+ for (const expected of expectedContracts) {
294
314
  const matches = actualContracts.filter(
295
315
  (test2) => test2.metadata?.id === expected.id
296
316
  );
@@ -313,7 +333,11 @@ function auditGameplayRun(options, tests) {
313
333
  }
314
334
  issues.push(...getDefinitionIssues(expected, metadata));
315
335
  if (test.state !== "passed") {
316
- issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
336
+ if (test.error) {
337
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u6D4B\u8BD5\u5931\u8D25\uFF1A${test.error}`);
338
+ } else {
339
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
340
+ }
317
341
  continue;
318
342
  }
319
343
  if (!metadata.verified || !metadata.evidence) {
@@ -329,7 +353,7 @@ function auditGameplayRun(options, tests) {
329
353
  }
330
354
  for (const test of actualContracts) {
331
355
  const id = test.metadata?.id;
332
- if (id && !options.contracts.some((contract) => contract.id === id)) {
356
+ if (id && options.contracts && !options.contracts.some((contract) => contract.id === id)) {
333
357
  issues.push(
334
358
  `\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
335
359
  );
@@ -370,11 +394,13 @@ function isGameplayContractMetadata(value) {
370
394
  }
371
395
  function toAuditTest(test) {
372
396
  const metadata = test.meta().gameplayContract;
397
+ const firstError = test.result().errors?.[0];
373
398
  return {
374
399
  file: normalizePath(test.module.relativeModuleId),
375
400
  name: test.fullName,
376
401
  state: test.result().state,
377
- metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
402
+ metadata: isGameplayContractMetadata(metadata) ? metadata : void 0,
403
+ error: firstError?.message?.replaceAll(/\s+/g, " ").trim().slice(0, 360) || void 0
378
404
  };
379
405
  }
380
406
  function formatGameplayIssues(issues) {
@@ -405,7 +431,8 @@ function formatGameplayIssues(issues) {
405
431
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
406
432
  registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
407
433
  missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
408
- test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002"
434
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002",
435
+ unhandled: "\u5F02\u6B65\u9519\u8BEF\uFF1A\u5148\u4FEE\u590D\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u9519\u8BEF\uFF1B\u8F93\u5165\u3001\u5E27\u63A8\u8FDB\u548C\u751F\u547D\u5468\u671F\u64CD\u4F5C\u90FD\u8981\u5728\u6B63\u786E\u7684 host \u751F\u547D\u5468\u671F\u5185\u5E76\u4F7F\u7528 await\u3002"
409
436
  };
410
437
  const getCategory = (issue) => {
411
438
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -430,6 +457,14 @@ function formatGameplayIssues(issues) {
430
457
  return { category: "registry", key: `registry:${scene}` };
431
458
  }
432
459
  if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
460
+ if (issue.includes("\u6D4B\u8BD5\u5931\u8D25\uFF1A")) {
461
+ const message = issue.split("\u6D4B\u8BD5\u5931\u8D25\uFF1A")[1] ?? issue;
462
+ return { category: "test", key: `test:${message}` };
463
+ }
464
+ if (issue.includes("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")) {
465
+ const message = issue.split("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")[1] ?? issue;
466
+ return { category: "unhandled", key: `test:${message}` };
467
+ }
433
468
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
434
469
  return { category: "other", key: `other:${issue}` };
435
470
  };
@@ -453,7 +488,7 @@ function formatGameplayIssues(issues) {
453
488
  continue;
454
489
  }
455
490
  const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
456
- const id = compact.match(/contract "([^"]+)"/)?.[1];
491
+ const id = compact.match(/(?:contract|玩法测试) "([^"]+)"/)?.[1];
457
492
  const { category, key } = getCategory(compact);
458
493
  const group = groups.get(key);
459
494
  if (group) {
@@ -513,7 +548,7 @@ var GameplayAuditReporter = class {
513
548
  (specification) => normalizePath(relative(this.projectRoot, specification.moduleId))
514
549
  )
515
550
  );
516
- for (const contract of this.options.contracts) {
551
+ for (const contract of this.options.contracts ?? []) {
517
552
  const testFile = normalizePath(contract.testFile);
518
553
  if (!scheduledFiles.has(testFile)) {
519
554
  this.reportPreflightIssue(
@@ -530,7 +565,7 @@ var GameplayAuditReporter = class {
530
565
  for (const issue of result.issues) this.reportPreflightIssue(issue);
531
566
  }
532
567
  /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
533
- onTestRunEnd(testModules, _unhandledErrors = [], reason = "passed") {
568
+ onTestRunEnd(testModules, unhandledErrors = [], reason = "passed") {
534
569
  const collectionFailed = reason === "failed" && testModules.some(
535
570
  (testModule) => testModule.state() === "failed" && [...testModule.children.allTests()].length === 0
536
571
  );
@@ -545,9 +580,11 @@ var GameplayAuditReporter = class {
545
580
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
546
581
  );
547
582
  const result = auditGameplayRun(this.options, tests);
583
+ const runtimeIssues = unhandledErrors.map((error) => error.message?.replaceAll(/\s+/g, " ").trim()).filter(Boolean).map((message) => `\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A${message}`);
548
584
  const combinedIssues = [
549
585
  ...this.preflightIssues,
550
- ...result.issues
586
+ ...result.issues,
587
+ ...runtimeIssues
551
588
  ];
552
589
  if (combinedIssues.length > 0) {
553
590
  printIssues("FINAL", [...new Set(combinedIssues)]);
@@ -555,7 +592,7 @@ var GameplayAuditReporter = class {
555
592
  } else {
556
593
  console.log(
557
594
  `
558
- GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
595
+ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${tests.filter((test) => test.metadata).length} contracts, ${this.options.scenes.length} scenes)`
559
596
  );
560
597
  console.log(
561
598
  "GAMEPLAY_AUDIT_NOTE: \u73A9\u6CD5\u68C0\u67E5\u5DF2\u901A\u8FC7\uFF1B\u4ECD\u9700\u4EE5\u6700\u540E\u7684 TEST_RESULT \u5224\u65AD\u6574\u4F53\u6210\u529F\u3002"
@@ -1,11 +1,11 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { c as GameplayAuditOptions } from './gameplay-audit-CusQfLYQ.mjs';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-CCJo4Qhk.mjs';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
6
6
  /** 模板项目的绝对根目录,通常传入 `import.meta.dirname`。 */
7
7
  projectRoot: string;
8
- /** 测试前后都必须审计的生产 Scene 和玩法契约清单。 */
8
+ /** 测试前后都必须审计的生产 Scene;contracts 可省略以避免重复声明测试 metadata。 */
9
9
  gameplayAudit: GameplayAuditOptions;
10
10
  /** 项目需要的额外路径别名;`@` 由 devkit 统一维护。 */
11
11
  aliases?: Record<string, string>;
@@ -1,11 +1,11 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { c as GameplayAuditOptions } from './gameplay-audit-CusQfLYQ.js';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-CCJo4Qhk.js';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
6
6
  /** 模板项目的绝对根目录,通常传入 `import.meta.dirname`。 */
7
7
  projectRoot: string;
8
- /** 测试前后都必须审计的生产 Scene 和玩法契约清单。 */
8
+ /** 测试前后都必须审计的生产 Scene;contracts 可省略以避免重复声明测试 metadata。 */
9
9
  gameplayAudit: GameplayAuditOptions;
10
10
  /** 项目需要的额外路径别名;`@` 由 devkit 统一维护。 */
11
11
  aliases?: Record<string, string>;