miaoda-game-devkit 0.2.8 → 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
  );
@@ -328,7 +348,7 @@ function auditGameplayRun(options2, tests) {
328
348
  }
329
349
  for (const test of actualContracts) {
330
350
  const id = test.metadata?.id;
331
- if (id && !options2.contracts.some((contract) => contract.id === id)) {
351
+ if (id && options2.contracts && !options2.contracts.some((contract) => contract.id === id)) {
332
352
  issues.push(
333
353
  `\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
334
354
  );
@@ -531,7 +551,7 @@ describe("gameplay audit", () => {
531
551
  ]
532
552
  });
533
553
  expect(result).toContain(
534
- '\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"
535
555
  );
536
556
  });
537
557
  it("\u5141\u8BB8\u5E26\u7406\u7531\u7684\u975E\u4EA4\u4E92 boot-only \u9879\u76EE", () => {
@@ -551,6 +571,44 @@ describe("gameplay audit", () => {
551
571
  })
552
572
  ).toEqual([]);
553
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
+ });
554
612
  it("\u5F3A\u5236 recovery contract \u540C\u65F6\u58F0\u660E restart \u548C\u4E24\u4E2A checkpoint", () => {
555
613
  const result = validateGameplayAuditOptions({
556
614
  mode: "non-interactive",
@@ -570,26 +628,34 @@ describe("gameplay audit", () => {
570
628
  'recovery contract "BattleScene.recovery" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002'
571
629
  ]));
572
630
  });
573
- 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", () => {
574
632
  const result = validateGameplayAuditOptions({
575
633
  mode: "interactive",
576
634
  scenes: ["MenuScene", "BattleScene"],
577
- contracts: [{
578
- id: "MenuScene.playthrough",
579
- kind: "playthrough",
580
- testFile: "tests/gameplay.test.ts",
581
- scenes: ["MenuScene"],
582
- requireInput: true,
583
- requireFrameAdvance: true,
584
- requireCheckpoint: "progress"
585
- }]
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
+ ]
586
652
  });
587
- 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([]);
588
654
  });
589
- it("\u4E0D\u63A5\u53D7 boot contract \u4F2A\u88C5\u6210\u751F\u4EA7\u8F6C\u573A", () => {
655
+ it("\u4ECD\u8981\u6C42 transition contract \u58F0\u660E\u76EE\u6807", () => {
590
656
  const result = validateGameplayAuditOptions({
591
657
  mode: "interactive",
592
- scenes: ["MenuScene", "BattleScene"],
658
+ scenes: ["MenuScene"],
593
659
  contracts: [
594
660
  {
595
661
  id: "MenuScene.playthrough",
@@ -601,16 +667,15 @@ describe("gameplay audit", () => {
601
667
  requireCheckpoint: "progress"
602
668
  },
603
669
  {
604
- id: "BattleScene.boot",
605
- kind: "boot",
670
+ id: "MenuScene.transition",
671
+ kind: "transition",
606
672
  testFile: "tests/gameplay.test.ts",
607
- scenes: ["BattleScene"],
608
- requireTransition: "BattleScene"
673
+ scenes: ["MenuScene"]
609
674
  }
610
675
  ]
611
676
  });
612
677
  expect(result).toContain(
613
- '\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'
614
679
  );
615
680
  });
616
681
  it("\u58F0\u660E restart flow \u540E\u4ECD\u5F3A\u5236 recovery contract", () => {
@@ -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,32 +126,25 @@ 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'
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"
144
146
  );
145
147
  }
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)
150
- );
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
- }
156
148
  const restartSignal = projectRoot2 ? containsProductionRestart(projectRoot2) : false;
157
149
  if (restartSignal && options.flows?.restart !== true) {
158
150
  issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF1B\u5FC5\u987B\u58F0\u660E flows.restart: true \u5E76\u63D0\u4F9B kind: recovery contract\u3002");
@@ -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
  );
@@ -333,7 +353,7 @@ function auditGameplayRun(options, tests) {
333
353
  }
334
354
  for (const test of actualContracts) {
335
355
  const id = test.metadata?.id;
336
- if (id && !options.contracts.some((contract) => contract.id === id)) {
356
+ if (id && options.contracts && !options.contracts.some((contract) => contract.id === id)) {
337
357
  issues.push(
338
358
  `\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
339
359
  );
@@ -528,7 +548,7 @@ var GameplayAuditReporter = class {
528
548
  (specification) => normalizePath(relative(this.projectRoot, specification.moduleId))
529
549
  )
530
550
  );
531
- for (const contract of this.options.contracts) {
551
+ for (const contract of this.options.contracts ?? []) {
532
552
  const testFile = normalizePath(contract.testFile);
533
553
  if (!scheduledFiles.has(testFile)) {
534
554
  this.reportPreflightIssue(
@@ -572,7 +592,7 @@ var GameplayAuditReporter = class {
572
592
  } else {
573
593
  console.log(
574
594
  `
575
- 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)`
576
596
  );
577
597
  console.log(
578
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>;
@@ -70,6 +70,7 @@ function createGameplayContractMetadata(contract) {
70
70
  }
71
71
  function validateGameplayAuditOptions(options, projectRoot) {
72
72
  const issues = [];
73
+ const contracts = options.contracts;
73
74
  const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
74
75
  const mode = options.mode ?? "interactive";
75
76
  if (mode === "non-interactive") {
@@ -91,7 +92,7 @@ function validateGameplayAuditOptions(options, projectRoot) {
91
92
  const coveredScenes = /* @__PURE__ */ new Set();
92
93
  const kinds = /* @__PURE__ */ new Map();
93
94
  const invalidKindContracts = [];
94
- for (const contract of options.contracts) {
95
+ for (const contract of contracts ?? []) {
95
96
  const id = contract.id.trim();
96
97
  if (!id) {
97
98
  issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
@@ -117,7 +118,6 @@ function validateGameplayAuditOptions(options, projectRoot) {
117
118
  kinds.set(contract.kind, list);
118
119
  }
119
120
  if (contract.kind === "playthrough") {
120
- if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
121
121
  if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
122
122
  if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
123
123
  }
@@ -125,7 +125,6 @@ function validateGameplayAuditOptions(options, projectRoot) {
125
125
  issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
126
126
  }
127
127
  if (contract.kind === "recovery") {
128
- if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
129
128
  if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
130
129
  if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
131
130
  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`);
@@ -147,32 +146,25 @@ function validateGameplayAuditOptions(options, projectRoot) {
147
146
  `\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`
148
147
  );
149
148
  }
150
- for (const scene of scenes) {
151
- if (!coveredScenes.has(scene)) {
152
- issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
149
+ if (contracts) {
150
+ for (const scene of scenes) {
151
+ if (!coveredScenes.has(scene)) {
152
+ issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
153
+ }
153
154
  }
155
+ if (contracts.length === 0)
156
+ 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");
154
157
  }
155
- if (options.contracts.length === 0)
156
- issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
157
- if (mode === "interactive") {
158
- const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
159
- const hasInput = options.contracts.some((contract) => contract.requireInput);
160
- const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
161
- if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
158
+ if (mode === "interactive" && contracts) {
159
+ const hasPlaythrough = (kinds.get("playthrough") ?? []).some(
160
+ (contract) => Boolean(contract.requireFrameAdvance) && normalizeList(contract.requireCheckpoint).length > 0
161
+ );
162
+ const hasInput = contracts.some((contract) => contract.requireInput);
163
+ if (!hasPlaythrough || !hasInput) {
162
164
  issues.push(
163
- '\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'
165
+ "\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"
164
166
  );
165
167
  }
166
- const entryScene = scenes[0];
167
- for (const scene of scenes.slice(1)) {
168
- const transitioned = options.contracts.some(
169
- (contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
170
- );
171
- if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
172
- }
173
- if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
174
- issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
175
- }
176
168
  const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
177
169
  if (restartSignal && options.flows?.restart !== true) {
178
170
  issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF1B\u5FC5\u987B\u58F0\u660E flows.restart: true \u5E76\u63D0\u4F9B kind: recovery contract\u3002");
@@ -233,11 +225,21 @@ function getDefinitionIssues(expected, actual) {
233
225
  }
234
226
  function auditGameplayCollection(options, file, tests) {
235
227
  const normalizedFile = file.replaceAll("\\", "/");
236
- const expected = options.contracts.filter(
228
+ const expected = (options.contracts ?? []).filter(
237
229
  (contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
238
230
  );
239
231
  const actual = tests.filter((test) => test.metadata);
240
232
  const issues = [];
233
+ if (!options.contracts) {
234
+ const actualIds = /* @__PURE__ */ new Set();
235
+ for (const test of actual) {
236
+ const id = test.metadata?.id;
237
+ if (!id) continue;
238
+ if (actualIds.has(id)) issues.push(`contract "${id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`);
239
+ actualIds.add(id);
240
+ }
241
+ return { passed: issues.length === 0, issues };
242
+ }
241
243
  for (const contract of expected) {
242
244
  const matches = actual.filter((test) => test.metadata?.id === contract.id);
243
245
  if (matches.length === 0) {
@@ -310,7 +312,25 @@ function auditGameplayRun(options, tests) {
310
312
  const issues = [];
311
313
  const actualContracts = tests.filter((test) => test.metadata);
312
314
  const observedEvidence = [];
313
- for (const expected of options.contracts) {
315
+ const discoveredContracts = actualContracts.flatMap((test) => {
316
+ const metadata = test.metadata;
317
+ if (!metadata) return [];
318
+ return [{
319
+ id: metadata.id,
320
+ kind: metadata.kind,
321
+ testFile: test.file,
322
+ scenes: metadata.scenes,
323
+ ...metadata.requirements
324
+ }];
325
+ });
326
+ const expectedContracts = options.contracts ?? discoveredContracts;
327
+ if (!options.contracts) {
328
+ const discoveredIssues = validateGameplayAuditOptions(
329
+ { ...options, contracts: discoveredContracts }
330
+ );
331
+ issues.push(...discoveredIssues);
332
+ }
333
+ for (const expected of expectedContracts) {
314
334
  const matches = actualContracts.filter(
315
335
  (test2) => test2.metadata?.id === expected.id
316
336
  );
@@ -353,7 +373,7 @@ function auditGameplayRun(options, tests) {
353
373
  }
354
374
  for (const test of actualContracts) {
355
375
  const id = test.metadata?.id;
356
- if (id && !options.contracts.some((contract) => contract.id === id)) {
376
+ if (id && options.contracts && !options.contracts.some((contract) => contract.id === id)) {
357
377
  issues.push(
358
378
  `\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
359
379
  );
@@ -548,7 +568,7 @@ var GameplayAuditReporter = class {
548
568
  (specification) => normalizePath((0, import_node_path2.relative)(this.projectRoot, specification.moduleId))
549
569
  )
550
570
  );
551
- for (const contract of this.options.contracts) {
571
+ for (const contract of this.options.contracts ?? []) {
552
572
  const testFile = normalizePath(contract.testFile);
553
573
  if (!scheduledFiles.has(testFile)) {
554
574
  this.reportPreflightIssue(
@@ -592,7 +612,7 @@ var GameplayAuditReporter = class {
592
612
  } else {
593
613
  console.log(
594
614
  `
595
- GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
615
+ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${tests.filter((test) => test.metadata).length} contracts, ${this.options.scenes.length} scenes)`
596
616
  );
597
617
  console.log(
598
618
  "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"