miaoda-game-devkit 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/dist/{gameplay-audit-CYqLL8gY.d.mts → gameplay-audit-XBGq_jIV.d.mts} +13 -0
- package/dist/{gameplay-audit-CYqLL8gY.d.ts → gameplay-audit-XBGq_jIV.d.ts} +13 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -0
- package/dist/index.mjs +3 -0
- package/dist/lint/gameplay-audit.contract.mjs +360 -13
- package/dist/lint/gameplay-contract.contract.mjs +5 -0
- package/dist/lint/vitest-config.contract.mjs +245 -22
- package/dist/vitest-config.d.mts +1 -1
- package/dist/vitest-config.d.ts +1 -1
- package/dist/vitest-config.js +232 -24
- package/dist/vitest-config.mjs +228 -20
- package/package.json +1 -1
package/dist/vitest-config.js
CHANGED
|
@@ -23,10 +23,12 @@ __export(vitest_config_exports, {
|
|
|
23
23
|
defineGameVitestConfig: () => defineGameVitestConfig
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(vitest_config_exports);
|
|
26
|
-
var
|
|
26
|
+
var import_node_path3 = require("path");
|
|
27
27
|
var import_config = require("vitest/config");
|
|
28
28
|
|
|
29
29
|
// src/gameplay-audit.ts
|
|
30
|
+
var import_node_fs = require("fs");
|
|
31
|
+
var import_node_path = require("path");
|
|
30
32
|
function normalizeList(value) {
|
|
31
33
|
if (value === void 0) return [];
|
|
32
34
|
const values = typeof value === "string" ? [value] : value;
|
|
@@ -60,20 +62,35 @@ function createGameplayContractMetadata(contract) {
|
|
|
60
62
|
const scenes = normalizeSet(contract.scenes);
|
|
61
63
|
return {
|
|
62
64
|
id: contract.id.trim(),
|
|
65
|
+
kind: contract.kind,
|
|
63
66
|
scenes,
|
|
64
67
|
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
65
68
|
verified: false
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
|
-
function validateGameplayAuditOptions(options) {
|
|
71
|
+
function validateGameplayAuditOptions(options, projectRoot) {
|
|
69
72
|
const issues = [];
|
|
70
|
-
const 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
|
+
}
|
|
71
86
|
if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
72
87
|
if (scenes.length !== options.scenes.length) {
|
|
73
88
|
issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
|
|
74
89
|
}
|
|
75
90
|
const contractIds = /* @__PURE__ */ new Set();
|
|
76
91
|
const coveredScenes = /* @__PURE__ */ new Set();
|
|
92
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
93
|
+
const invalidKindContracts = [];
|
|
77
94
|
for (const contract of options.contracts) {
|
|
78
95
|
const id = contract.id.trim();
|
|
79
96
|
if (!id) {
|
|
@@ -92,6 +109,27 @@ function validateGameplayAuditOptions(options) {
|
|
|
92
109
|
);
|
|
93
110
|
}
|
|
94
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
|
+
}
|
|
95
133
|
if (contractScenes.length === 0) {
|
|
96
134
|
issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
|
|
97
135
|
}
|
|
@@ -104,6 +142,11 @@ function validateGameplayAuditOptions(options) {
|
|
|
104
142
|
}
|
|
105
143
|
}
|
|
106
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
|
+
}
|
|
107
150
|
for (const scene of scenes) {
|
|
108
151
|
if (!coveredScenes.has(scene)) {
|
|
109
152
|
issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
|
|
@@ -111,11 +154,71 @@ function validateGameplayAuditOptions(options) {
|
|
|
111
154
|
}
|
|
112
155
|
if (options.contracts.length === 0)
|
|
113
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
|
+
}
|
|
114
185
|
return issues;
|
|
115
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
|
+
}
|
|
116
216
|
function getDefinitionIssues(expected, actual) {
|
|
117
217
|
const issues = [];
|
|
118
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
|
+
}
|
|
119
222
|
if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
|
|
120
223
|
issues.push(
|
|
121
224
|
`contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
|
|
@@ -170,12 +273,15 @@ function auditGameplayCollection(options, file, tests) {
|
|
|
170
273
|
function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
171
274
|
const issues = [];
|
|
172
275
|
const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
|
|
173
|
-
if (requirements.requireInput && inputEvents === 0)
|
|
276
|
+
if (requirements.requireInput && inputEvents === 0) {
|
|
174
277
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
|
|
175
|
-
|
|
278
|
+
}
|
|
279
|
+
if (requirements.requireFrameAdvance && evidence.frames === 0) {
|
|
176
280
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
|
|
177
|
-
|
|
281
|
+
}
|
|
282
|
+
if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
|
|
178
283
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
|
|
284
|
+
}
|
|
179
285
|
for (const target of normalizeList(requirements.requireTransition)) {
|
|
180
286
|
if (!evidence.transitions.some((transition) => transition.to === target)) {
|
|
181
287
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
|
|
@@ -186,21 +292,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
|
186
292
|
issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
|
|
187
293
|
}
|
|
188
294
|
for (const scene of normalizeList(requirements.requireRestart)) {
|
|
189
|
-
if (!evidence.restartedScenes.includes(scene))
|
|
295
|
+
if (!evidence.restartedScenes.includes(scene)) {
|
|
190
296
|
issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
|
|
297
|
+
}
|
|
191
298
|
}
|
|
192
299
|
for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
|
|
193
|
-
if (!evidence.checkpoints.includes(checkpoint))
|
|
300
|
+
if (!evidence.checkpoints.includes(checkpoint)) {
|
|
194
301
|
issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
|
|
302
|
+
}
|
|
195
303
|
}
|
|
196
|
-
if (requirements.requireDestroy && !evidence.destroyed)
|
|
304
|
+
if (requirements.requireDestroy && !evidence.destroyed) {
|
|
197
305
|
issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
|
|
306
|
+
}
|
|
198
307
|
return issues;
|
|
199
308
|
}
|
|
200
309
|
function auditGameplayRun(options, tests) {
|
|
201
310
|
const issues = [];
|
|
202
311
|
const actualContracts = tests.filter((test) => test.metadata);
|
|
203
|
-
const
|
|
312
|
+
const observedEvidence = [];
|
|
204
313
|
for (const expected of options.contracts) {
|
|
205
314
|
const matches = actualContracts.filter(
|
|
206
315
|
(test2) => test2.metadata?.id === expected.id
|
|
@@ -216,6 +325,7 @@ function auditGameplayRun(options, tests) {
|
|
|
216
325
|
const test = matches[0];
|
|
217
326
|
const metadata = test?.metadata;
|
|
218
327
|
if (!test || !metadata) continue;
|
|
328
|
+
if (metadata.evidence) observedEvidence.push(metadata.evidence);
|
|
219
329
|
if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
|
|
220
330
|
issues.push(
|
|
221
331
|
`contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
|
|
@@ -236,7 +346,6 @@ function auditGameplayRun(options, tests) {
|
|
|
236
346
|
metadata.evidence
|
|
237
347
|
);
|
|
238
348
|
issues.push(...evidenceIssues);
|
|
239
|
-
if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
|
|
240
349
|
}
|
|
241
350
|
for (const test of actualContracts) {
|
|
242
351
|
const id = test.metadata?.id;
|
|
@@ -247,14 +356,15 @@ function auditGameplayRun(options, tests) {
|
|
|
247
356
|
}
|
|
248
357
|
}
|
|
249
358
|
const registeredScenes = normalizeSet(
|
|
250
|
-
|
|
359
|
+
observedEvidence.flatMap((evidence) => evidence.registeredScenes)
|
|
251
360
|
);
|
|
252
361
|
const expectedScenes = normalizeSet(options.scenes);
|
|
253
362
|
for (const scene of expectedScenes) {
|
|
254
|
-
if (!registeredScenes.includes(scene))
|
|
363
|
+
if (!registeredScenes.includes(scene)) {
|
|
255
364
|
issues.push(
|
|
256
|
-
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\
|
|
365
|
+
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
|
|
257
366
|
);
|
|
367
|
+
}
|
|
258
368
|
}
|
|
259
369
|
for (const scene of registeredScenes) {
|
|
260
370
|
if (!expectedScenes.includes(scene))
|
|
@@ -266,7 +376,7 @@ function auditGameplayRun(options, tests) {
|
|
|
266
376
|
}
|
|
267
377
|
|
|
268
378
|
// src/gameplay-audit-reporter.ts
|
|
269
|
-
var
|
|
379
|
+
var import_node_path2 = require("path");
|
|
270
380
|
function normalizePath(path) {
|
|
271
381
|
return path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
272
382
|
}
|
|
@@ -284,11 +394,96 @@ function toAuditTest(test) {
|
|
|
284
394
|
metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
|
|
285
395
|
};
|
|
286
396
|
}
|
|
397
|
+
function formatGameplayIssues(issues) {
|
|
398
|
+
const groups = /* @__PURE__ */ new Map();
|
|
399
|
+
const repairKeys = /* @__PURE__ */ new Set();
|
|
400
|
+
const repairs = [];
|
|
401
|
+
const repairText = {
|
|
402
|
+
input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
|
|
403
|
+
frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
|
|
404
|
+
physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
|
|
405
|
+
transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
|
|
406
|
+
visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
|
|
407
|
+
restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
|
|
408
|
+
checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
|
|
409
|
+
destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
|
|
410
|
+
registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
|
|
411
|
+
missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
|
|
412
|
+
test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
|
|
413
|
+
};
|
|
414
|
+
const getCategory = (issue) => {
|
|
415
|
+
if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
|
|
416
|
+
if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
|
|
417
|
+
if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
|
|
418
|
+
if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
|
|
419
|
+
const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
|
|
420
|
+
return { category: "transition", key: `transition:${target}` };
|
|
421
|
+
}
|
|
422
|
+
if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
|
|
423
|
+
const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
424
|
+
return { category: "visit", key: `visit:${scene}` };
|
|
425
|
+
}
|
|
426
|
+
if (issue.includes("\u672A\u91CD\u542F Scene")) {
|
|
427
|
+
const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
428
|
+
return { category: "restart", key: `restart:${scene}` };
|
|
429
|
+
}
|
|
430
|
+
if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
|
|
431
|
+
if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
|
|
432
|
+
if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
|
|
433
|
+
const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
434
|
+
return { category: "registry", key: `registry:${scene}` };
|
|
435
|
+
}
|
|
436
|
+
if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
|
|
437
|
+
if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
|
|
438
|
+
return { category: "other", key: `other:${issue}` };
|
|
439
|
+
};
|
|
440
|
+
for (const rawIssue of [...new Set(issues)]) {
|
|
441
|
+
const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
|
|
442
|
+
const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
|
|
443
|
+
const id = compact.match(/contract "([^"]+)"/)?.[1];
|
|
444
|
+
const { category, key } = getCategory(compact);
|
|
445
|
+
const group = groups.get(key);
|
|
446
|
+
if (group) {
|
|
447
|
+
if (id && !group.ids.includes(id)) group.ids.push(id);
|
|
448
|
+
} else {
|
|
449
|
+
groups.set(key, { text: readable, ids: id ? [id] : [], key });
|
|
450
|
+
}
|
|
451
|
+
if (repairText[category] && !repairKeys.has(category)) {
|
|
452
|
+
repairKeys.add(category);
|
|
453
|
+
repairs.push(repairText[category]);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const grouped = [...groups.values()];
|
|
457
|
+
const visible = grouped.slice(0, 8).map((group) => {
|
|
458
|
+
if (group.ids.length <= 1) return group.text;
|
|
459
|
+
return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
|
|
460
|
+
});
|
|
461
|
+
return {
|
|
462
|
+
issues: visible.map(
|
|
463
|
+
(issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
|
|
464
|
+
),
|
|
465
|
+
repairs: repairs.slice(0, 8),
|
|
466
|
+
omitted: Math.max(0, grouped.length - visible.length)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
287
469
|
function printIssues(stage, issues) {
|
|
288
470
|
if (issues.length === 0) return;
|
|
289
471
|
console.error(`
|
|
290
472
|
GAMEPLAY_AUDIT: ${stage} FAILED`);
|
|
291
|
-
|
|
473
|
+
console.error(
|
|
474
|
+
"\u73A9\u6CD5\u68C0\u67E5\u5931\u8D25\uFF1A\u4EE5\u4E0B\u6BCF\u4E2A\u68C0\u67E5\u9879\u5BF9\u5E94\u4E00\u4E2A\u9700\u8981\u6D4B\u8BD5\u8BC1\u660E\u7684\u6E38\u620F\u884C\u4E3A\u3002"
|
|
475
|
+
);
|
|
476
|
+
const formatted = formatGameplayIssues(issues);
|
|
477
|
+
for (const issue of formatted.issues) console.error(`- ${issue}`);
|
|
478
|
+
if (formatted.repairs.length > 0) {
|
|
479
|
+
console.error("\u4FEE\u590D\u5EFA\u8BAE\uFF08\u540C\u7C7B\u95EE\u9898\u53EA\u5217\u4E00\u6B21\uFF09\uFF1A");
|
|
480
|
+
for (const repair of formatted.repairs) console.error(`- ${repair}`);
|
|
481
|
+
}
|
|
482
|
+
if (formatted.omitted > 0) {
|
|
483
|
+
console.error(
|
|
484
|
+
`- \u5176\u4F59 ${formatted.omitted} \u7C7B\u95EE\u9898\u5DF2\u7701\u7565\uFF1B\u5148\u4FEE\u590D\u4EE5\u4E0A\u95EE\u9898\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
292
487
|
}
|
|
293
488
|
var GameplayAuditReporter = class {
|
|
294
489
|
projectRoot;
|
|
@@ -304,7 +499,7 @@ var GameplayAuditReporter = class {
|
|
|
304
499
|
this.preflightIssues.clear();
|
|
305
500
|
const scheduledFiles = new Set(
|
|
306
501
|
specifications.map(
|
|
307
|
-
(specification) => normalizePath((0,
|
|
502
|
+
(specification) => normalizePath((0, import_node_path2.relative)(this.projectRoot, specification.moduleId))
|
|
308
503
|
)
|
|
309
504
|
);
|
|
310
505
|
for (const contract of this.options.contracts) {
|
|
@@ -329,28 +524,37 @@ var GameplayAuditReporter = class {
|
|
|
329
524
|
(testModule) => [...testModule.children.allTests()].map(toAuditTest)
|
|
330
525
|
);
|
|
331
526
|
const result = auditGameplayRun(this.options, tests);
|
|
332
|
-
|
|
333
|
-
|
|
527
|
+
const combinedIssues = [
|
|
528
|
+
...this.preflightIssues,
|
|
529
|
+
...result.issues
|
|
530
|
+
];
|
|
531
|
+
if (combinedIssues.length > 0) {
|
|
532
|
+
if (this.preflightIssues.size > 0) {
|
|
533
|
+
console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
|
|
534
|
+
}
|
|
535
|
+
printIssues("FINAL", [...new Set(combinedIssues)]);
|
|
334
536
|
process.exitCode = 1;
|
|
335
537
|
} else {
|
|
336
538
|
console.log(
|
|
337
539
|
`
|
|
338
540
|
GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
|
|
339
541
|
);
|
|
542
|
+
console.log(
|
|
543
|
+
"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"
|
|
544
|
+
);
|
|
340
545
|
}
|
|
341
546
|
}
|
|
342
547
|
/** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
|
|
343
548
|
reportPreflightIssue(issue) {
|
|
344
549
|
if (this.preflightIssues.has(issue)) return;
|
|
345
550
|
this.preflightIssues.add(issue);
|
|
346
|
-
printIssues("PREFLIGHT", [issue]);
|
|
347
551
|
process.exitCode = 1;
|
|
348
552
|
}
|
|
349
553
|
};
|
|
350
554
|
|
|
351
555
|
// src/vitest-config.ts
|
|
352
556
|
function defineGameVitestConfig(options) {
|
|
353
|
-
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
|
|
557
|
+
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
|
|
354
558
|
if (auditIssues.length > 0) {
|
|
355
559
|
throw new Error(
|
|
356
560
|
`\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
|
|
@@ -361,7 +565,7 @@ function defineGameVitestConfig(options) {
|
|
|
361
565
|
resolve: {
|
|
362
566
|
alias: {
|
|
363
567
|
...options.aliases,
|
|
364
|
-
"@": (0,
|
|
568
|
+
"@": (0, import_node_path3.resolve)(options.projectRoot, "src")
|
|
365
569
|
}
|
|
366
570
|
},
|
|
367
571
|
// devkit 作为 external ESM 加载时,应用测试也必须 externalize Phaser,
|
|
@@ -393,7 +597,9 @@ function defineGameVitestConfig(options) {
|
|
|
393
597
|
...options.additionalSetupFiles ?? []
|
|
394
598
|
],
|
|
395
599
|
reporters: [
|
|
396
|
-
|
|
600
|
+
// Vitest's minimal reporter is optimized for AI output: passing tests
|
|
601
|
+
// stay quiet while failed tests retain their useful error message.
|
|
602
|
+
"minimal",
|
|
397
603
|
new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
|
|
398
604
|
],
|
|
399
605
|
restoreMocks: true,
|
|
@@ -405,7 +611,9 @@ function defineGameVitestConfig(options) {
|
|
|
405
611
|
"src/scenes/**/*.d.ts",
|
|
406
612
|
"src/scenes/**/*.{spec,test}.{ts,tsx}"
|
|
407
613
|
],
|
|
408
|
-
|
|
614
|
+
// Per-file threshold errors still identify omitted Scene files; the
|
|
615
|
+
// summary avoids printing a large coverage table on every successful run.
|
|
616
|
+
reporter: ["text-summary"],
|
|
409
617
|
thresholds: {
|
|
410
618
|
perFile: true,
|
|
411
619
|
lines: 1,
|