miaoda-game-devkit 0.2.1 → 0.2.3

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.
@@ -162,6 +162,8 @@ declare function createHeadlessGame<TScene extends Phaser.Scene>(scene: TScene,
162
162
 
163
163
  /** 一个必须由指定测试文件证明的玩法契约。 */
164
164
  interface GameplayAuditContract extends HeadlessGameplayEvidenceRequirements {
165
+ /** 契约用途;boot 不得替代交互游戏的 playthrough。 */
166
+ kind: "boot" | "playthrough" | "transition" | "recovery";
165
167
  /** 在整个游戏项目中唯一且稳定的契约标识。 */
166
168
  id: string;
167
169
  /** 相对于游戏项目根目录的 Vitest 测试文件。 */
@@ -171,6 +173,15 @@ interface GameplayAuditContract extends HeadlessGameplayEvidenceRequirements {
171
173
  }
172
174
  /** 游戏项目在一次 Vitest 运行中必须完成的玩法审计清单。 */
173
175
  interface GameplayAuditOptions {
176
+ /** 默认按交互游戏审计;纯演示项目必须显式豁免并说明原因。 */
177
+ mode?: "interactive" | "non-interactive";
178
+ /** non-interactive 模式的可审计豁免理由。 */
179
+ nonInteractiveJustification?: string;
180
+ /** 应用声明的可恢复流程;restart 会触发 recovery contract 基线。 */
181
+ flows?: {
182
+ restart?: boolean;
183
+ reset?: boolean;
184
+ };
174
185
  /** 生产 Scene registry 中预期出现的全部 Scene key。 */
175
186
  scenes: string[];
176
187
  /** 必须定义、执行并满足证据要求的玩法契约。 */
@@ -178,6 +189,8 @@ interface GameplayAuditOptions {
178
189
  }
179
190
  /** 通过 Vitest task metadata 在 worker 与 reporter 之间传递的契约数据。 */
180
191
  interface GameplayContractMetadata {
192
+ /** 契约用途。 */
193
+ kind: GameplayAuditContract["kind"];
181
194
  /** 契约唯一标识。 */
182
195
  id: string;
183
196
  /** 契约负责证明的生产 Scene key。 */
@@ -162,6 +162,8 @@ declare function createHeadlessGame<TScene extends Phaser.Scene>(scene: TScene,
162
162
 
163
163
  /** 一个必须由指定测试文件证明的玩法契约。 */
164
164
  interface GameplayAuditContract extends HeadlessGameplayEvidenceRequirements {
165
+ /** 契约用途;boot 不得替代交互游戏的 playthrough。 */
166
+ kind: "boot" | "playthrough" | "transition" | "recovery";
165
167
  /** 在整个游戏项目中唯一且稳定的契约标识。 */
166
168
  id: string;
167
169
  /** 相对于游戏项目根目录的 Vitest 测试文件。 */
@@ -171,6 +173,15 @@ interface GameplayAuditContract extends HeadlessGameplayEvidenceRequirements {
171
173
  }
172
174
  /** 游戏项目在一次 Vitest 运行中必须完成的玩法审计清单。 */
173
175
  interface GameplayAuditOptions {
176
+ /** 默认按交互游戏审计;纯演示项目必须显式豁免并说明原因。 */
177
+ mode?: "interactive" | "non-interactive";
178
+ /** non-interactive 模式的可审计豁免理由。 */
179
+ nonInteractiveJustification?: string;
180
+ /** 应用声明的可恢复流程;restart 会触发 recovery contract 基线。 */
181
+ flows?: {
182
+ restart?: boolean;
183
+ reset?: boolean;
184
+ };
174
185
  /** 生产 Scene registry 中预期出现的全部 Scene key。 */
175
186
  scenes: string[];
176
187
  /** 必须定义、执行并满足证据要求的玩法契约。 */
@@ -178,6 +189,8 @@ interface GameplayAuditOptions {
178
189
  }
179
190
  /** 通过 Vitest task metadata 在 worker 与 reporter 之间传递的契约数据。 */
180
191
  interface GameplayContractMetadata {
192
+ /** 契约用途。 */
193
+ kind: GameplayAuditContract["kind"];
181
194
  /** 契约唯一标识。 */
182
195
  id: string;
183
196
  /** 契约负责证明的生产 Scene key。 */
package/dist/index.d.mts CHANGED
@@ -1,10 +1,12 @@
1
- import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-CYqLL8gY.mjs';
2
- export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-CYqLL8gY.mjs';
1
+ import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-XBGq_jIV.mjs';
2
+ export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-XBGq_jIV.mjs';
3
3
  import * as Phaser from 'phaser';
4
4
  import { TestContext, TestOptions } from 'vitest';
5
5
 
6
6
  /** 将单个 Vitest 测试绑定到玩法 Ledger 的契约选项。 */
7
7
  interface GameplayContractOptions extends HeadlessGameplayEvidenceRequirements {
8
+ /** 契约用途;交互项目不能只使用 boot。 */
9
+ kind: "boot" | "playthrough" | "transition" | "recovery";
8
10
  /** 生产玩法清单中稳定且唯一的 contract 标识。 */
9
11
  id: string;
10
12
  /** 当前契约负责证明的生产 Scene key。 */
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-CYqLL8gY.js';
2
- export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-CYqLL8gY.js';
1
+ import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-XBGq_jIV.js';
2
+ export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-XBGq_jIV.js';
3
3
  import * as Phaser from 'phaser';
4
4
  import { TestContext, TestOptions } from 'vitest';
5
5
 
6
6
  /** 将单个 Vitest 测试绑定到玩法 Ledger 的契约选项。 */
7
7
  interface GameplayContractOptions extends HeadlessGameplayEvidenceRequirements {
8
+ /** 契约用途;交互项目不能只使用 boot。 */
9
+ kind: "boot" | "playthrough" | "transition" | "recovery";
8
10
  /** 生产玩法清单中稳定且唯一的 contract 标识。 */
9
11
  id: string;
10
12
  /** 当前契约负责证明的生产 Scene key。 */
package/dist/index.js CHANGED
@@ -1089,6 +1089,8 @@ async function createHeadlessGame(scene, options = {}) {
1089
1089
  var import_vitest = require("vitest");
1090
1090
 
1091
1091
  // src/gameplay-audit.ts
1092
+ var import_node_fs = require("fs");
1093
+ var import_node_path = require("path");
1092
1094
  function normalizeList(value) {
1093
1095
  if (value === void 0) return [];
1094
1096
  const values = typeof value === "string" ? [value] : value;
@@ -1119,6 +1121,7 @@ function createGameplayContractMetadata(contract) {
1119
1121
  const scenes = normalizeSet(contract.scenes);
1120
1122
  return {
1121
1123
  id: contract.id.trim(),
1124
+ kind: contract.kind,
1122
1125
  scenes,
1123
1126
  requirements: normalizeGameplayRequirements(contract, scenes),
1124
1127
  verified: false
package/dist/index.mjs CHANGED
@@ -1047,6 +1047,8 @@ async function createHeadlessGame(scene, options = {}) {
1047
1047
  import { test } from "vitest";
1048
1048
 
1049
1049
  // src/gameplay-audit.ts
1050
+ import { readdirSync, readFileSync, statSync } from "fs";
1051
+ import { join } from "path";
1050
1052
  function normalizeList(value) {
1051
1053
  if (value === void 0) return [];
1052
1054
  const values = typeof value === "string" ? [value] : value;
@@ -1077,6 +1079,7 @@ function createGameplayContractMetadata(contract) {
1077
1079
  const scenes = normalizeSet(contract.scenes);
1078
1080
  return {
1079
1081
  id: contract.id.trim(),
1082
+ kind: contract.kind,
1080
1083
  scenes,
1081
1084
  requirements: normalizeGameplayRequirements(contract, scenes),
1082
1085
  verified: false
@@ -2,6 +2,8 @@
2
2
  import { describe, expect, it } from "vitest";
3
3
 
4
4
  // src/gameplay-audit.ts
5
+ import { readdirSync, readFileSync, statSync } from "fs";
6
+ import { join } from "path";
5
7
  function normalizeList(value) {
6
8
  if (value === void 0) return [];
7
9
  const values = typeof value === "string" ? [value] : value;
@@ -35,20 +37,35 @@ function createGameplayContractMetadata(contract) {
35
37
  const scenes = normalizeSet(contract.scenes);
36
38
  return {
37
39
  id: contract.id.trim(),
40
+ kind: contract.kind,
38
41
  scenes,
39
42
  requirements: normalizeGameplayRequirements(contract, scenes),
40
43
  verified: false
41
44
  };
42
45
  }
43
- function validateGameplayAuditOptions(options2) {
46
+ function validateGameplayAuditOptions(options2, projectRoot) {
44
47
  const issues = [];
45
- const scenes = normalizeSet(options2.scenes);
48
+ const scenes = [...new Set(options2.scenes.map((scene) => scene.trim()).filter(Boolean))];
49
+ const mode = options2.mode ?? "interactive";
50
+ if (mode === "non-interactive") {
51
+ const justification = options2.nonInteractiveJustification?.trim() ?? "";
52
+ if (justification.length < 20) {
53
+ issues.push(
54
+ "non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
55
+ );
56
+ }
57
+ }
58
+ if (mode !== "interactive" && mode !== "non-interactive") {
59
+ issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
60
+ }
46
61
  if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
47
62
  if (scenes.length !== options2.scenes.length) {
48
63
  issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
49
64
  }
50
65
  const contractIds = /* @__PURE__ */ new Set();
51
66
  const coveredScenes = /* @__PURE__ */ new Set();
67
+ const kinds = /* @__PURE__ */ new Map();
68
+ const invalidKindContracts = [];
52
69
  for (const contract of options2.contracts) {
53
70
  const id = contract.id.trim();
54
71
  if (!id) {
@@ -67,6 +84,27 @@ function validateGameplayAuditOptions(options2) {
67
84
  );
68
85
  }
69
86
  const contractScenes = normalizeSet(contract.scenes);
87
+ if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
88
+ invalidKindContracts.push(id || "<empty>");
89
+ } else {
90
+ const list = kinds.get(contract.kind) ?? [];
91
+ list.push(contract);
92
+ kinds.set(contract.kind, list);
93
+ }
94
+ if (contract.kind === "playthrough") {
95
+ if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
96
+ if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
97
+ if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
98
+ }
99
+ if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
100
+ issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
101
+ }
102
+ if (contract.kind === "recovery") {
103
+ if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
104
+ if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
105
+ if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
106
+ 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`);
107
+ }
70
108
  if (contractScenes.length === 0) {
71
109
  issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
72
110
  }
@@ -79,6 +117,11 @@ function validateGameplayAuditOptions(options2) {
79
117
  }
80
118
  }
81
119
  }
120
+ if (invalidKindContracts.length > 0) {
121
+ issues.push(
122
+ `\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
+ );
124
+ }
82
125
  for (const scene of scenes) {
83
126
  if (!coveredScenes.has(scene)) {
84
127
  issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
@@ -86,11 +129,71 @@ function validateGameplayAuditOptions(options2) {
86
129
  }
87
130
  if (options2.contracts.length === 0)
88
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) {
137
+ 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)
145
+ );
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
+ }
151
+ const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
152
+ if (restartSignal && options2.flows?.restart !== true) {
153
+ 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");
154
+ } else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
155
+ issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
156
+ } else if ((options2.flows?.restart || options2.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
157
+ issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
158
+ }
159
+ }
89
160
  return issues;
90
161
  }
162
+ function containsProductionRestart(projectRoot) {
163
+ const sceneRoot = join(projectRoot, "src", "scenes");
164
+ const visit = (directory) => {
165
+ let entries;
166
+ try {
167
+ entries = readdirSync(directory);
168
+ } catch {
169
+ return false;
170
+ }
171
+ for (const entry of entries) {
172
+ const path = join(directory, entry);
173
+ let stats;
174
+ try {
175
+ stats = statSync(path);
176
+ } catch {
177
+ continue;
178
+ }
179
+ if (stats.isDirectory() && visit(path)) return true;
180
+ if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
181
+ try {
182
+ const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
183
+ if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
184
+ } catch {
185
+ }
186
+ }
187
+ return false;
188
+ };
189
+ return visit(sceneRoot);
190
+ }
91
191
  function getDefinitionIssues(expected, actual) {
92
192
  const issues = [];
93
193
  const expectedMetadata = createGameplayContractMetadata(expected);
194
+ if (expectedMetadata.kind !== actual.kind) {
195
+ issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
196
+ }
94
197
  if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
95
198
  issues.push(
96
199
  `contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
@@ -242,10 +345,12 @@ function auditGameplayRun(options2, tests) {
242
345
 
243
346
  // src/lint/gameplay-audit.test.ts
244
347
  var options = {
348
+ mode: "interactive",
245
349
  scenes: ["IndexScene", "BattleScene"],
246
350
  contracts: [
247
351
  {
248
352
  id: "gameplay.main-flow",
353
+ kind: "playthrough",
249
354
  testFile: "tests/gameplay.test.ts",
250
355
  scenes: ["IndexScene", "BattleScene"],
251
356
  requireInput: true,
@@ -284,6 +389,123 @@ function createPassingTest() {
284
389
  };
285
390
  }
286
391
  describe("gameplay audit", () => {
392
+ it("\u62D2\u7EDD\u4EA4\u4E92\u9879\u76EE\u53EA\u6709 boot contract", () => {
393
+ const result = validateGameplayAuditOptions({
394
+ mode: "interactive",
395
+ scenes: ["IndexScene"],
396
+ contracts: [
397
+ {
398
+ id: "IndexScene.boot",
399
+ kind: "boot",
400
+ testFile: "tests/gameplay.test.ts",
401
+ scenes: ["IndexScene"]
402
+ }
403
+ ]
404
+ });
405
+ expect(result).toContain(
406
+ '\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'
407
+ );
408
+ });
409
+ it("\u5141\u8BB8\u5E26\u7406\u7531\u7684\u975E\u4EA4\u4E92 boot-only \u9879\u76EE", () => {
410
+ expect(
411
+ validateGameplayAuditOptions({
412
+ mode: "non-interactive",
413
+ nonInteractiveJustification: "\u8FD9\u662F\u53EA\u5C55\u793A\u9759\u6001\u6B22\u8FCE\u753B\u9762\u7684\u6A21\u677F\u5939\u5177\uFF0C\u4E0D\u5305\u542B\u73A9\u5BB6\u8F93\u5165\u3002",
414
+ scenes: ["IndexScene"],
415
+ contracts: [
416
+ {
417
+ id: "IndexScene.boot",
418
+ kind: "boot",
419
+ testFile: "tests/gameplay.test.ts",
420
+ scenes: ["IndexScene"]
421
+ }
422
+ ]
423
+ })
424
+ ).toEqual([]);
425
+ });
426
+ it("\u5F3A\u5236 recovery contract \u540C\u65F6\u58F0\u660E restart \u548C\u4E24\u4E2A checkpoint", () => {
427
+ const result = validateGameplayAuditOptions({
428
+ mode: "non-interactive",
429
+ nonInteractiveJustification: "\u6D4B\u8BD5 recovery contract \u7684\u914D\u7F6E\u6821\u9A8C\uFF0C\u4E0D\u4EE3\u8868\u771F\u5B9E\u4EA7\u54C1\u8C41\u514D\u3002",
430
+ scenes: ["BattleScene"],
431
+ contracts: [{
432
+ id: "BattleScene.recovery",
433
+ kind: "recovery",
434
+ testFile: "tests/gameplay.test.ts",
435
+ scenes: ["BattleScene"],
436
+ requireInput: true,
437
+ requireFrameAdvance: true
438
+ }]
439
+ });
440
+ expect(result).toEqual(expect.arrayContaining([
441
+ 'recovery contract "BattleScene.recovery" \u5FC5\u987B\u58F0\u660E requireRestart\u3002',
442
+ 'recovery contract "BattleScene.recovery" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002'
443
+ ]));
444
+ });
445
+ it("\u4EA4\u4E92\u9879\u76EE\u8981\u6C42\u6BCF\u4E2A\u975E\u5165\u53E3 Scene \u6709\u751F\u4EA7\u8F6C\u573A\u8BC1\u636E\u58F0\u660E", () => {
446
+ const result = validateGameplayAuditOptions({
447
+ mode: "interactive",
448
+ scenes: ["MenuScene", "BattleScene"],
449
+ contracts: [{
450
+ id: "MenuScene.playthrough",
451
+ kind: "playthrough",
452
+ testFile: "tests/gameplay.test.ts",
453
+ scenes: ["MenuScene"],
454
+ requireInput: true,
455
+ requireFrameAdvance: true,
456
+ requireCheckpoint: "progress"
457
+ }]
458
+ });
459
+ expect(result).toContain('\u975E\u5165\u53E3 Scene "BattleScene" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002');
460
+ });
461
+ it("\u4E0D\u63A5\u53D7 boot contract \u4F2A\u88C5\u6210\u751F\u4EA7\u8F6C\u573A", () => {
462
+ const result = validateGameplayAuditOptions({
463
+ mode: "interactive",
464
+ scenes: ["MenuScene", "BattleScene"],
465
+ contracts: [
466
+ {
467
+ id: "MenuScene.playthrough",
468
+ kind: "playthrough",
469
+ testFile: "tests/gameplay.test.ts",
470
+ scenes: ["MenuScene"],
471
+ requireInput: true,
472
+ requireFrameAdvance: true,
473
+ requireCheckpoint: "progress"
474
+ },
475
+ {
476
+ id: "BattleScene.boot",
477
+ kind: "boot",
478
+ testFile: "tests/gameplay.test.ts",
479
+ scenes: ["BattleScene"],
480
+ requireTransition: "BattleScene"
481
+ }
482
+ ]
483
+ });
484
+ expect(result).toContain(
485
+ '\u975E\u5165\u53E3 Scene "BattleScene" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002'
486
+ );
487
+ });
488
+ it("\u58F0\u660E restart flow \u540E\u4ECD\u5F3A\u5236 recovery contract", () => {
489
+ const result = validateGameplayAuditOptions({
490
+ mode: "interactive",
491
+ flows: { restart: true },
492
+ scenes: ["IndexScene"],
493
+ contracts: [
494
+ {
495
+ id: "IndexScene.playthrough",
496
+ kind: "playthrough",
497
+ testFile: "tests/gameplay.test.ts",
498
+ scenes: ["IndexScene"],
499
+ requireInput: true,
500
+ requireFrameAdvance: true,
501
+ requireCheckpoint: "progress"
502
+ }
503
+ ]
504
+ });
505
+ expect(result).toContain(
506
+ "\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002"
507
+ );
508
+ });
287
509
  it("\u62D2\u7EDD\u672A\u88AB contract \u8986\u76D6\u7684\u751F\u4EA7 Scene", () => {
288
510
  expect(
289
511
  validateGameplayAuditOptions({
@@ -299,6 +521,7 @@ describe("gameplay audit", () => {
299
521
  contracts: [
300
522
  {
301
523
  id: " IndexScene.boot ",
524
+ kind: "boot",
302
525
  testFile: "tests/gameplay.test.ts",
303
526
  scenes: ["IndexScene"]
304
527
  }
@@ -312,6 +535,7 @@ describe("gameplay audit", () => {
312
535
  contracts: [
313
536
  {
314
537
  id: "IndexScene.boot",
538
+ kind: "boot",
315
539
  testFile,
316
540
  scenes: ["IndexScene"]
317
541
  }
@@ -1051,6 +1051,8 @@ async function createHeadlessGame(scene, options = {}) {
1051
1051
  import { test } from "vitest";
1052
1052
 
1053
1053
  // src/gameplay-audit.ts
1054
+ import { readdirSync, readFileSync, statSync } from "fs";
1055
+ import { join } from "path";
1054
1056
  function normalizeList(value) {
1055
1057
  if (value === void 0) return [];
1056
1058
  const values = typeof value === "string" ? [value] : value;
@@ -1081,6 +1083,7 @@ function createGameplayContractMetadata(contract) {
1081
1083
  const scenes = normalizeSet(contract.scenes);
1082
1084
  return {
1083
1085
  id: contract.id.trim(),
1086
+ kind: contract.kind,
1084
1087
  scenes,
1085
1088
  requirements: normalizeGameplayRequirements(contract, scenes),
1086
1089
  verified: false
@@ -3,11 +3,12 @@ import { resolve as resolve2 } from "path";
3
3
  import { describe, expect, it } from "vitest";
4
4
 
5
5
  // src/vitest-config.ts
6
- import { createRequire } from "module";
7
- import { dirname, resolve } from "path";
6
+ import { resolve } from "path";
8
7
  import { defineConfig } from "vitest/config";
9
8
 
10
9
  // src/gameplay-audit.ts
10
+ import { readdirSync, readFileSync, statSync } from "fs";
11
+ import { join } from "path";
11
12
  function normalizeList(value) {
12
13
  if (value === void 0) return [];
13
14
  const values = typeof value === "string" ? [value] : value;
@@ -41,20 +42,35 @@ function createGameplayContractMetadata(contract) {
41
42
  const scenes = normalizeSet(contract.scenes);
42
43
  return {
43
44
  id: contract.id.trim(),
45
+ kind: contract.kind,
44
46
  scenes,
45
47
  requirements: normalizeGameplayRequirements(contract, scenes),
46
48
  verified: false
47
49
  };
48
50
  }
49
- function validateGameplayAuditOptions(options) {
51
+ function validateGameplayAuditOptions(options, projectRoot2) {
50
52
  const issues = [];
51
- const scenes = normalizeSet(options.scenes);
53
+ const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
54
+ const mode = options.mode ?? "interactive";
55
+ if (mode === "non-interactive") {
56
+ const justification = options.nonInteractiveJustification?.trim() ?? "";
57
+ if (justification.length < 20) {
58
+ issues.push(
59
+ "non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
60
+ );
61
+ }
62
+ }
63
+ if (mode !== "interactive" && mode !== "non-interactive") {
64
+ issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
65
+ }
52
66
  if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
53
67
  if (scenes.length !== options.scenes.length) {
54
68
  issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
55
69
  }
56
70
  const contractIds = /* @__PURE__ */ new Set();
57
71
  const coveredScenes = /* @__PURE__ */ new Set();
72
+ const kinds = /* @__PURE__ */ new Map();
73
+ const invalidKindContracts = [];
58
74
  for (const contract of options.contracts) {
59
75
  const id = contract.id.trim();
60
76
  if (!id) {
@@ -73,6 +89,27 @@ function validateGameplayAuditOptions(options) {
73
89
  );
74
90
  }
75
91
  const contractScenes = normalizeSet(contract.scenes);
92
+ if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
93
+ invalidKindContracts.push(id || "<empty>");
94
+ } else {
95
+ const list = kinds.get(contract.kind) ?? [];
96
+ list.push(contract);
97
+ kinds.set(contract.kind, list);
98
+ }
99
+ if (contract.kind === "playthrough") {
100
+ if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
101
+ if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
102
+ if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
103
+ }
104
+ if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
105
+ issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
106
+ }
107
+ if (contract.kind === "recovery") {
108
+ if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
109
+ if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
110
+ if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
111
+ 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`);
112
+ }
76
113
  if (contractScenes.length === 0) {
77
114
  issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
78
115
  }
@@ -85,6 +122,11 @@ function validateGameplayAuditOptions(options) {
85
122
  }
86
123
  }
87
124
  }
125
+ if (invalidKindContracts.length > 0) {
126
+ issues.push(
127
+ `\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
+ );
129
+ }
88
130
  for (const scene of scenes) {
89
131
  if (!coveredScenes.has(scene)) {
90
132
  issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
@@ -92,11 +134,71 @@ function validateGameplayAuditOptions(options) {
92
134
  }
93
135
  if (options.contracts.length === 0)
94
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) {
142
+ 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)
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
+ const restartSignal = projectRoot2 ? containsProductionRestart(projectRoot2) : false;
157
+ if (restartSignal && options.flows?.restart !== true) {
158
+ 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");
159
+ } else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
160
+ issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
161
+ } else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
162
+ issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
163
+ }
164
+ }
95
165
  return issues;
96
166
  }
167
+ function containsProductionRestart(projectRoot2) {
168
+ const sceneRoot = join(projectRoot2, "src", "scenes");
169
+ const visit = (directory) => {
170
+ let entries;
171
+ try {
172
+ entries = readdirSync(directory);
173
+ } catch {
174
+ return false;
175
+ }
176
+ for (const entry of entries) {
177
+ const path = join(directory, entry);
178
+ let stats;
179
+ try {
180
+ stats = statSync(path);
181
+ } catch {
182
+ continue;
183
+ }
184
+ if (stats.isDirectory() && visit(path)) return true;
185
+ if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
186
+ try {
187
+ const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
188
+ if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
189
+ } catch {
190
+ }
191
+ }
192
+ return false;
193
+ };
194
+ return visit(sceneRoot);
195
+ }
97
196
  function getDefinitionIssues(expected, actual) {
98
197
  const issues = [];
99
198
  const expectedMetadata = createGameplayContractMetadata(expected);
199
+ if (expectedMetadata.kind !== actual.kind) {
200
+ issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
201
+ }
100
202
  if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
101
203
  issues.push(
102
204
  `contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
@@ -331,28 +433,25 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts
331
433
 
332
434
  // src/vitest-config.ts
333
435
  function defineGameVitestConfig(options) {
334
- const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
436
+ const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
335
437
  if (auditIssues.length > 0) {
336
438
  throw new Error(
337
439
  `\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
338
440
  - ${auditIssues.join("\n- ")}`
339
441
  );
340
442
  }
341
- const projectRequire = createRequire(
342
- resolve(options.projectRoot, "package.json")
343
- );
344
- const phaserPackageRoot = dirname(
345
- projectRequire.resolve("phaser/package.json")
346
- );
347
- const phaserBrowserBuild = resolve(phaserPackageRoot, "dist/phaser.js");
348
443
  return defineConfig({
349
444
  resolve: {
350
445
  alias: {
351
446
  ...options.aliases,
352
- "@": resolve(options.projectRoot, "src"),
353
- phaser: phaserBrowserBuild
447
+ "@": resolve(options.projectRoot, "src")
354
448
  }
355
449
  },
450
+ // devkit 作为 external ESM 加载时,应用测试也必须 externalize Phaser,
451
+ // 否则 Vite alias 与 Node 条件导出会创建两个 Phaser 单例,Scene 无法启动。
452
+ ssr: {
453
+ external: ["phaser"]
454
+ },
356
455
  test: {
357
456
  include: ["tests/**/*.test.ts"],
358
457
  testTimeout: options.testTimeout,
@@ -362,7 +461,8 @@ function defineGameVitestConfig(options) {
362
461
  // devkit 模块需要在配置和审计阶段使用 Node 内置模块。
363
462
  // 如果强制内联,Vite 会把这些模块按浏览器模块转换,
364
463
  // 最终触发 "No such built-in module: node:"。
365
- external: [/miaoda-game-devkit/]
464
+ external: [/miaoda-game-devkit/],
465
+ inline: options.inlineDependencies
366
466
  }
367
467
  },
368
468
  environment: "jsdom",
@@ -403,10 +503,13 @@ function defineGameVitestConfig(options) {
403
503
  // src/lint/vitest-config.test.ts
404
504
  var projectRoot = resolve2(import.meta.dirname, "../..");
405
505
  var gameplayAudit = {
506
+ mode: "non-interactive",
507
+ nonInteractiveJustification: "\u56FA\u5B9A\u5939\u5177\u53EA\u9A8C\u8BC1\u5171\u4EAB host \u57FA\u7EBF\uFF0C\u4E0D\u5305\u542B\u73A9\u6CD5\u8F93\u5165\u3002",
406
508
  scenes: ["InputScene"],
407
509
  contracts: [
408
510
  {
409
511
  id: "InputScene.boot",
512
+ kind: "boot",
410
513
  testFile: "tests/input.test.ts",
411
514
  scenes: ["InputScene"],
412
515
  requireFrameAdvance: true
@@ -417,14 +520,16 @@ describe("defineGameVitestConfig", () => {
417
520
  it("\u63D0\u4F9B Phaser \u8FD0\u884C\u65F6\u6D4B\u8BD5\u9700\u8981\u7684\u56FA\u5B9A\u57FA\u7EBF", () => {
418
521
  const config = defineGameVitestConfig({ projectRoot, gameplayAudit });
419
522
  expect(config.resolve?.alias).toMatchObject({
420
- "@": resolve2(projectRoot, "src"),
421
- phaser: expect.stringContaining("phaser/dist/phaser.js")
523
+ "@": resolve2(projectRoot, "src")
422
524
  });
525
+ expect(config.resolve?.alias).not.toHaveProperty("phaser");
526
+ expect(config.ssr).toMatchObject({ external: ["phaser"] });
423
527
  expect(config.test).toMatchObject({
424
528
  include: ["tests/**/*.test.ts"],
425
529
  server: {
426
530
  deps: {
427
- external: [/miaoda-game-devkit/]
531
+ external: [/miaoda-game-devkit/],
532
+ inline: void 0
428
533
  }
429
534
  },
430
535
  environment: "jsdom",
@@ -454,6 +559,7 @@ describe("defineGameVitestConfig", () => {
454
559
  const config = defineGameVitestConfig({
455
560
  projectRoot,
456
561
  gameplayAudit,
562
+ inlineDependencies: [/phaser4-rex-plugins/],
457
563
  additionalSetupFiles: ["tests/custom-setup.ts"],
458
564
  testTimeout: 5e3,
459
565
  hookTimeout: 2e3
@@ -476,4 +582,16 @@ describe("defineGameVitestConfig", () => {
476
582
  })
477
583
  ).toThrow(/ResultScene.*没有任何 gameplay contract/s);
478
584
  });
585
+ it("\u62D2\u7EDD\u4EA4\u4E92\u9879\u76EE\u53EA\u6709 boot smoke test", () => {
586
+ expect(
587
+ () => defineGameVitestConfig({
588
+ projectRoot,
589
+ gameplayAudit: {
590
+ mode: "interactive",
591
+ scenes: ["InputScene"],
592
+ contracts: gameplayAudit.contracts
593
+ }
594
+ })
595
+ ).toThrow(/缺少最低玩法基线/);
596
+ });
479
597
  });
@@ -1,5 +1,5 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { c as GameplayAuditOptions } from './gameplay-audit-CYqLL8gY.mjs';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-XBGq_jIV.mjs';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
@@ -7,10 +7,12 @@ interface GameVitestConfigOptions {
7
7
  projectRoot: string;
8
8
  /** 测试前后都必须审计的生产 Scene 和玩法契约清单。 */
9
9
  gameplayAudit: GameplayAuditOptions;
10
- /** 项目需要的额外路径别名;`@` 和 `phaser` 由 devkit 统一维护。 */
10
+ /** 项目需要的额外路径别名;`@` 由 devkit 统一维护。 */
11
11
  aliases?: Record<string, string>;
12
12
  /** 在 devkit 基础 setup 之后执行的项目级 setup 文件。 */
13
13
  additionalSetupFiles?: string[];
14
+ /** 需要由 Vite 转换的项目依赖,例如包含无扩展名 import 的插件。 */
15
+ inlineDependencies?: RegExp[];
14
16
  /** 单个游戏运行时测试的超时时间。 */
15
17
  testTimeout?: number;
16
18
  /** 单个游戏测试钩子的超时时间。 */
@@ -1,5 +1,5 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { c as GameplayAuditOptions } from './gameplay-audit-CYqLL8gY.js';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-XBGq_jIV.js';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
@@ -7,10 +7,12 @@ interface GameVitestConfigOptions {
7
7
  projectRoot: string;
8
8
  /** 测试前后都必须审计的生产 Scene 和玩法契约清单。 */
9
9
  gameplayAudit: GameplayAuditOptions;
10
- /** 项目需要的额外路径别名;`@` 和 `phaser` 由 devkit 统一维护。 */
10
+ /** 项目需要的额外路径别名;`@` 由 devkit 统一维护。 */
11
11
  aliases?: Record<string, string>;
12
12
  /** 在 devkit 基础 setup 之后执行的项目级 setup 文件。 */
13
13
  additionalSetupFiles?: string[];
14
+ /** 需要由 Vite 转换的项目依赖,例如包含无扩展名 import 的插件。 */
15
+ inlineDependencies?: RegExp[];
14
16
  /** 单个游戏运行时测试的超时时间。 */
15
17
  testTimeout?: number;
16
18
  /** 单个游戏测试钩子的超时时间。 */
@@ -23,11 +23,12 @@ __export(vitest_config_exports, {
23
23
  defineGameVitestConfig: () => defineGameVitestConfig
24
24
  });
25
25
  module.exports = __toCommonJS(vitest_config_exports);
26
- var import_node_module = require("module");
27
- var import_node_path2 = require("path");
26
+ var import_node_path3 = require("path");
28
27
  var import_config = require("vitest/config");
29
28
 
30
29
  // src/gameplay-audit.ts
30
+ var import_node_fs = require("fs");
31
+ var import_node_path = require("path");
31
32
  function normalizeList(value) {
32
33
  if (value === void 0) return [];
33
34
  const values = typeof value === "string" ? [value] : value;
@@ -61,20 +62,35 @@ function createGameplayContractMetadata(contract) {
61
62
  const scenes = normalizeSet(contract.scenes);
62
63
  return {
63
64
  id: contract.id.trim(),
65
+ kind: contract.kind,
64
66
  scenes,
65
67
  requirements: normalizeGameplayRequirements(contract, scenes),
66
68
  verified: false
67
69
  };
68
70
  }
69
- function validateGameplayAuditOptions(options) {
71
+ function validateGameplayAuditOptions(options, projectRoot) {
70
72
  const issues = [];
71
- const scenes = normalizeSet(options.scenes);
73
+ const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
74
+ const mode = options.mode ?? "interactive";
75
+ if (mode === "non-interactive") {
76
+ const justification = options.nonInteractiveJustification?.trim() ?? "";
77
+ if (justification.length < 20) {
78
+ issues.push(
79
+ "non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
80
+ );
81
+ }
82
+ }
83
+ if (mode !== "interactive" && mode !== "non-interactive") {
84
+ issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
85
+ }
72
86
  if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
73
87
  if (scenes.length !== options.scenes.length) {
74
88
  issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
75
89
  }
76
90
  const contractIds = /* @__PURE__ */ new Set();
77
91
  const coveredScenes = /* @__PURE__ */ new Set();
92
+ const kinds = /* @__PURE__ */ new Map();
93
+ const invalidKindContracts = [];
78
94
  for (const contract of options.contracts) {
79
95
  const id = contract.id.trim();
80
96
  if (!id) {
@@ -93,6 +109,27 @@ function validateGameplayAuditOptions(options) {
93
109
  );
94
110
  }
95
111
  const contractScenes = normalizeSet(contract.scenes);
112
+ if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
113
+ invalidKindContracts.push(id || "<empty>");
114
+ } else {
115
+ const list = kinds.get(contract.kind) ?? [];
116
+ list.push(contract);
117
+ kinds.set(contract.kind, list);
118
+ }
119
+ if (contract.kind === "playthrough") {
120
+ if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
121
+ if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
122
+ if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
123
+ }
124
+ if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
125
+ issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
126
+ }
127
+ if (contract.kind === "recovery") {
128
+ if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
129
+ if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
130
+ if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
131
+ 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`);
132
+ }
96
133
  if (contractScenes.length === 0) {
97
134
  issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
98
135
  }
@@ -105,6 +142,11 @@ function validateGameplayAuditOptions(options) {
105
142
  }
106
143
  }
107
144
  }
145
+ if (invalidKindContracts.length > 0) {
146
+ issues.push(
147
+ `\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
+ );
149
+ }
108
150
  for (const scene of scenes) {
109
151
  if (!coveredScenes.has(scene)) {
110
152
  issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
@@ -112,11 +154,71 @@ function validateGameplayAuditOptions(options) {
112
154
  }
113
155
  if (options.contracts.length === 0)
114
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) {
162
+ 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'
164
+ );
165
+ }
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
+ const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
177
+ if (restartSignal && options.flows?.restart !== true) {
178
+ 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");
179
+ } else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
180
+ issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
181
+ } else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
182
+ issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
183
+ }
184
+ }
115
185
  return issues;
116
186
  }
187
+ function containsProductionRestart(projectRoot) {
188
+ const sceneRoot = (0, import_node_path.join)(projectRoot, "src", "scenes");
189
+ const visit = (directory) => {
190
+ let entries;
191
+ try {
192
+ entries = (0, import_node_fs.readdirSync)(directory);
193
+ } catch {
194
+ return false;
195
+ }
196
+ for (const entry of entries) {
197
+ const path = (0, import_node_path.join)(directory, entry);
198
+ let stats;
199
+ try {
200
+ stats = (0, import_node_fs.statSync)(path);
201
+ } catch {
202
+ continue;
203
+ }
204
+ if (stats.isDirectory() && visit(path)) return true;
205
+ if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
206
+ try {
207
+ const source = (0, import_node_fs.readFileSync)(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
208
+ if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
209
+ } catch {
210
+ }
211
+ }
212
+ return false;
213
+ };
214
+ return visit(sceneRoot);
215
+ }
117
216
  function getDefinitionIssues(expected, actual) {
118
217
  const issues = [];
119
218
  const expectedMetadata = createGameplayContractMetadata(expected);
219
+ if (expectedMetadata.kind !== actual.kind) {
220
+ issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
221
+ }
120
222
  if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
121
223
  issues.push(
122
224
  `contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
@@ -267,7 +369,7 @@ function auditGameplayRun(options, tests) {
267
369
  }
268
370
 
269
371
  // src/gameplay-audit-reporter.ts
270
- var import_node_path = require("path");
372
+ var import_node_path2 = require("path");
271
373
  function normalizePath(path) {
272
374
  return path.replaceAll("\\", "/").replace(/^\.\//, "");
273
375
  }
@@ -305,7 +407,7 @@ var GameplayAuditReporter = class {
305
407
  this.preflightIssues.clear();
306
408
  const scheduledFiles = new Set(
307
409
  specifications.map(
308
- (specification) => normalizePath((0, import_node_path.relative)(this.projectRoot, specification.moduleId))
410
+ (specification) => normalizePath((0, import_node_path2.relative)(this.projectRoot, specification.moduleId))
309
411
  )
310
412
  );
311
413
  for (const contract of this.options.contracts) {
@@ -351,28 +453,25 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts
351
453
 
352
454
  // src/vitest-config.ts
353
455
  function defineGameVitestConfig(options) {
354
- const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
456
+ const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
355
457
  if (auditIssues.length > 0) {
356
458
  throw new Error(
357
459
  `\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
358
460
  - ${auditIssues.join("\n- ")}`
359
461
  );
360
462
  }
361
- const projectRequire = (0, import_node_module.createRequire)(
362
- (0, import_node_path2.resolve)(options.projectRoot, "package.json")
363
- );
364
- const phaserPackageRoot = (0, import_node_path2.dirname)(
365
- projectRequire.resolve("phaser/package.json")
366
- );
367
- const phaserBrowserBuild = (0, import_node_path2.resolve)(phaserPackageRoot, "dist/phaser.js");
368
463
  return (0, import_config.defineConfig)({
369
464
  resolve: {
370
465
  alias: {
371
466
  ...options.aliases,
372
- "@": (0, import_node_path2.resolve)(options.projectRoot, "src"),
373
- phaser: phaserBrowserBuild
467
+ "@": (0, import_node_path3.resolve)(options.projectRoot, "src")
374
468
  }
375
469
  },
470
+ // devkit 作为 external ESM 加载时,应用测试也必须 externalize Phaser,
471
+ // 否则 Vite alias 与 Node 条件导出会创建两个 Phaser 单例,Scene 无法启动。
472
+ ssr: {
473
+ external: ["phaser"]
474
+ },
376
475
  test: {
377
476
  include: ["tests/**/*.test.ts"],
378
477
  testTimeout: options.testTimeout,
@@ -382,7 +481,8 @@ function defineGameVitestConfig(options) {
382
481
  // devkit 模块需要在配置和审计阶段使用 Node 内置模块。
383
482
  // 如果强制内联,Vite 会把这些模块按浏览器模块转换,
384
483
  // 最终触发 "No such built-in module: node:"。
385
- external: [/miaoda-game-devkit/]
484
+ external: [/miaoda-game-devkit/],
485
+ inline: options.inlineDependencies
386
486
  }
387
487
  },
388
488
  environment: "jsdom",
@@ -1,9 +1,10 @@
1
1
  // src/vitest-config.ts
2
- import { createRequire } from "module";
3
- import { dirname, resolve } from "path";
2
+ import { resolve } from "path";
4
3
  import { defineConfig } from "vitest/config";
5
4
 
6
5
  // src/gameplay-audit.ts
6
+ import { readdirSync, readFileSync, statSync } from "fs";
7
+ import { join } from "path";
7
8
  function normalizeList(value) {
8
9
  if (value === void 0) return [];
9
10
  const values = typeof value === "string" ? [value] : value;
@@ -37,20 +38,35 @@ function createGameplayContractMetadata(contract) {
37
38
  const scenes = normalizeSet(contract.scenes);
38
39
  return {
39
40
  id: contract.id.trim(),
41
+ kind: contract.kind,
40
42
  scenes,
41
43
  requirements: normalizeGameplayRequirements(contract, scenes),
42
44
  verified: false
43
45
  };
44
46
  }
45
- function validateGameplayAuditOptions(options) {
47
+ function validateGameplayAuditOptions(options, projectRoot) {
46
48
  const issues = [];
47
- const scenes = normalizeSet(options.scenes);
49
+ const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
50
+ const mode = options.mode ?? "interactive";
51
+ if (mode === "non-interactive") {
52
+ const justification = options.nonInteractiveJustification?.trim() ?? "";
53
+ if (justification.length < 20) {
54
+ issues.push(
55
+ "non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
56
+ );
57
+ }
58
+ }
59
+ if (mode !== "interactive" && mode !== "non-interactive") {
60
+ issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
61
+ }
48
62
  if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
49
63
  if (scenes.length !== options.scenes.length) {
50
64
  issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
51
65
  }
52
66
  const contractIds = /* @__PURE__ */ new Set();
53
67
  const coveredScenes = /* @__PURE__ */ new Set();
68
+ const kinds = /* @__PURE__ */ new Map();
69
+ const invalidKindContracts = [];
54
70
  for (const contract of options.contracts) {
55
71
  const id = contract.id.trim();
56
72
  if (!id) {
@@ -69,6 +85,27 @@ function validateGameplayAuditOptions(options) {
69
85
  );
70
86
  }
71
87
  const contractScenes = normalizeSet(contract.scenes);
88
+ if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
89
+ invalidKindContracts.push(id || "<empty>");
90
+ } else {
91
+ const list = kinds.get(contract.kind) ?? [];
92
+ list.push(contract);
93
+ kinds.set(contract.kind, list);
94
+ }
95
+ if (contract.kind === "playthrough") {
96
+ if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
97
+ if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
98
+ if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
99
+ }
100
+ if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
101
+ issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
102
+ }
103
+ if (contract.kind === "recovery") {
104
+ if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
105
+ if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
106
+ if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
107
+ 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`);
108
+ }
72
109
  if (contractScenes.length === 0) {
73
110
  issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
74
111
  }
@@ -81,6 +118,11 @@ function validateGameplayAuditOptions(options) {
81
118
  }
82
119
  }
83
120
  }
121
+ if (invalidKindContracts.length > 0) {
122
+ issues.push(
123
+ `\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`
124
+ );
125
+ }
84
126
  for (const scene of scenes) {
85
127
  if (!coveredScenes.has(scene)) {
86
128
  issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
@@ -88,11 +130,71 @@ function validateGameplayAuditOptions(options) {
88
130
  }
89
131
  if (options.contracts.length === 0)
90
132
  issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
133
+ if (mode === "interactive") {
134
+ const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
135
+ const hasInput = options.contracts.some((contract) => contract.requireInput);
136
+ const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
137
+ if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
138
+ issues.push(
139
+ '\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'
140
+ );
141
+ }
142
+ const entryScene = scenes[0];
143
+ for (const scene of scenes.slice(1)) {
144
+ const transitioned = options.contracts.some(
145
+ (contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
146
+ );
147
+ if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
148
+ }
149
+ if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
150
+ issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
151
+ }
152
+ const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
153
+ if (restartSignal && options.flows?.restart !== true) {
154
+ 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");
155
+ } else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
156
+ issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
157
+ } else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
158
+ issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
159
+ }
160
+ }
91
161
  return issues;
92
162
  }
163
+ function containsProductionRestart(projectRoot) {
164
+ const sceneRoot = join(projectRoot, "src", "scenes");
165
+ const visit = (directory) => {
166
+ let entries;
167
+ try {
168
+ entries = readdirSync(directory);
169
+ } catch {
170
+ return false;
171
+ }
172
+ for (const entry of entries) {
173
+ const path = join(directory, entry);
174
+ let stats;
175
+ try {
176
+ stats = statSync(path);
177
+ } catch {
178
+ continue;
179
+ }
180
+ if (stats.isDirectory() && visit(path)) return true;
181
+ if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
182
+ try {
183
+ const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
184
+ if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
185
+ } catch {
186
+ }
187
+ }
188
+ return false;
189
+ };
190
+ return visit(sceneRoot);
191
+ }
93
192
  function getDefinitionIssues(expected, actual) {
94
193
  const issues = [];
95
194
  const expectedMetadata = createGameplayContractMetadata(expected);
195
+ if (expectedMetadata.kind !== actual.kind) {
196
+ issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
197
+ }
96
198
  if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
97
199
  issues.push(
98
200
  `contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
@@ -327,28 +429,25 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts
327
429
 
328
430
  // src/vitest-config.ts
329
431
  function defineGameVitestConfig(options) {
330
- const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
432
+ const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
331
433
  if (auditIssues.length > 0) {
332
434
  throw new Error(
333
435
  `\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
334
436
  - ${auditIssues.join("\n- ")}`
335
437
  );
336
438
  }
337
- const projectRequire = createRequire(
338
- resolve(options.projectRoot, "package.json")
339
- );
340
- const phaserPackageRoot = dirname(
341
- projectRequire.resolve("phaser/package.json")
342
- );
343
- const phaserBrowserBuild = resolve(phaserPackageRoot, "dist/phaser.js");
344
439
  return defineConfig({
345
440
  resolve: {
346
441
  alias: {
347
442
  ...options.aliases,
348
- "@": resolve(options.projectRoot, "src"),
349
- phaser: phaserBrowserBuild
443
+ "@": resolve(options.projectRoot, "src")
350
444
  }
351
445
  },
446
+ // devkit 作为 external ESM 加载时,应用测试也必须 externalize Phaser,
447
+ // 否则 Vite alias 与 Node 条件导出会创建两个 Phaser 单例,Scene 无法启动。
448
+ ssr: {
449
+ external: ["phaser"]
450
+ },
352
451
  test: {
353
452
  include: ["tests/**/*.test.ts"],
354
453
  testTimeout: options.testTimeout,
@@ -358,7 +457,8 @@ function defineGameVitestConfig(options) {
358
457
  // devkit 模块需要在配置和审计阶段使用 Node 内置模块。
359
458
  // 如果强制内联,Vite 会把这些模块按浏览器模块转换,
360
459
  // 最终触发 "No such built-in module: node:"。
361
- external: [/miaoda-game-devkit/]
460
+ external: [/miaoda-game-devkit/],
461
+ inline: options.inlineDependencies
362
462
  }
363
463
  },
364
464
  environment: "jsdom",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Shared lint and deterministic Phaser HEADLESS testing tools for Miaoda games",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -43,6 +43,18 @@
43
43
  "!dist/**/*.map",
44
44
  "!dist/lint/**/*.test.*"
45
45
  ],
46
+ "scripts": {
47
+ "build": "tsup",
48
+ "prepack": "pnpm run build",
49
+ "release": "pnpm run test && clean-publish",
50
+ "release:dry-run": "pnpm run test && clean-publish --dry-run",
51
+ "release:beta": "pnpm run test && npm version prerelease --preid=beta && clean-publish --tag beta",
52
+ "release:patch": "pnpm run test && npm version patch && clean-publish",
53
+ "release:minor": "pnpm run test && npm version minor && clean-publish",
54
+ "release:major": "pnpm run test && npm version major && clean-publish",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "pnpm run build && ./bin/miaoda-game-lint.js --contracts-only"
57
+ },
46
58
  "nx": {
47
59
  "tags": [
48
60
  "type:package",
@@ -82,8 +94,23 @@
82
94
  "peerDependencies": {
83
95
  "phaser": ">=4.0.0 <5"
84
96
  },
97
+ "devDependencies": {
98
+ "@types/eslint": "^9.6.1",
99
+ "@types/estree": "^1.0.9",
100
+ "@types/node": "^24.13.3",
101
+ "clean-publish": "^6.0.5",
102
+ "phaser": "4.0.0",
103
+ "tsup": "^8.5.1",
104
+ "typescript": "^5.9.3"
105
+ },
85
106
  "publishConfig": {
86
107
  "registry": "https://registry.npmjs.org/",
87
108
  "access": "public"
109
+ },
110
+ "clean-publish": {
111
+ "fields": [
112
+ "devDependencies",
113
+ "scripts"
114
+ ]
88
115
  }
89
116
  }