miaoda-game-devkit 0.2.8 → 0.2.10
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 +39 -13
- package/dist/cli/lint.js +32 -1
- package/dist/game-clock-suUidZdT.d.mts +11 -0
- package/dist/game-clock-suUidZdT.d.ts +11 -0
- package/dist/{gameplay-audit-CusQfLYQ.d.mts → gameplay-audit-BwAobjIX.d.mts} +3 -3
- package/dist/{gameplay-audit-CusQfLYQ.d.ts → gameplay-audit-BwAobjIX.d.ts} +3 -3
- package/dist/index.d.mts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +52 -0
- package/dist/index.mjs +49 -0
- package/dist/lint/game-telemetry.contract.mjs +1241 -0
- package/dist/lint/gameplay-audit.contract.mjs +111 -46
- package/dist/lint/gameplay-contract.contract.mjs +5 -2
- package/dist/lint/vitest-config.contract.mjs +262 -37
- package/dist/phaser-facade.mjs +156 -0
- package/dist/react/index.d.mts +13 -0
- package/dist/react/index.d.ts +13 -0
- package/dist/react/index.js +57 -0
- package/dist/react/index.mjs +29 -0
- package/dist/react/testing.d.mts +21 -0
- package/dist/react/testing.d.ts +21 -0
- package/dist/react/testing.js +82 -0
- package/dist/react/testing.mjs +57 -0
- package/dist/react/vitest-config.d.mts +14 -0
- package/dist/react/vitest-config.d.ts +14 -0
- package/dist/react/vitest-config.js +57 -0
- package/dist/react/vitest-config.mjs +32 -0
- package/dist/react/vitest-setup.d.mts +2 -0
- package/dist/react/vitest-setup.d.ts +2 -0
- package/dist/react/vitest-setup.js +35 -0
- package/dist/react/vitest-setup.mjs +33 -0
- package/dist/vite.d.mts +22 -0
- package/dist/vite.d.ts +22 -0
- package/dist/vite.js +93 -0
- package/dist/vite.mjs +67 -0
- package/dist/vitest-config.d.mts +2 -2
- package/dist/vitest-config.d.ts +2 -2
- package/dist/vitest-config.js +95 -31
- package/dist/vitest-config.mjs +95 -31
- package/package.json +72 -2
package/dist/vitest-config.js
CHANGED
|
@@ -23,7 +23,7 @@ __export(vitest_config_exports, {
|
|
|
23
23
|
defineGameVitestConfig: () => defineGameVitestConfig
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(vitest_config_exports);
|
|
26
|
-
var
|
|
26
|
+
var import_node_path4 = require("path");
|
|
27
27
|
var import_config = require("vitest/config");
|
|
28
28
|
|
|
29
29
|
// src/gameplay-audit.ts
|
|
@@ -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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const hasInput =
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (${
|
|
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"
|
|
@@ -607,6 +627,49 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts
|
|
|
607
627
|
}
|
|
608
628
|
};
|
|
609
629
|
|
|
630
|
+
// src/vite.ts
|
|
631
|
+
var import_node_fs2 = require("fs");
|
|
632
|
+
var import_node_module = require("module");
|
|
633
|
+
var import_node_path3 = require("path");
|
|
634
|
+
var PHASER_MODULE_ID = "phaser";
|
|
635
|
+
function resolvePhaserFacade(projectRoot) {
|
|
636
|
+
const requireFromProject = (0, import_node_module.createRequire)((0, import_node_path3.join)(projectRoot, "package.json"));
|
|
637
|
+
const viteEntry = requireFromProject.resolve("miaoda-game-devkit/vite");
|
|
638
|
+
const facade = (0, import_node_path3.join)((0, import_node_path3.dirname)(viteEntry), "phaser-facade.mjs");
|
|
639
|
+
if (!(0, import_node_fs2.existsSync)(facade)) {
|
|
640
|
+
throw new Error(
|
|
641
|
+
`miaoda-game-devkit \u7F3A\u5C11 Phaser facade\uFF1A${facade}`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
return facade;
|
|
645
|
+
}
|
|
646
|
+
function isInsideDirectory(filePath, directory) {
|
|
647
|
+
const normalizedFile = (0, import_node_path3.normalize)(filePath);
|
|
648
|
+
const normalizedDirectory = `${(0, import_node_path3.normalize)(directory)}${import_node_path3.sep}`;
|
|
649
|
+
return normalizedFile.startsWith(normalizedDirectory);
|
|
650
|
+
}
|
|
651
|
+
function phaserRexUIScenePlugin(projectRoot) {
|
|
652
|
+
let scenesRoot = projectRoot ? (0, import_node_path3.resolve)(projectRoot, "src/scenes") : void 0;
|
|
653
|
+
let phaserFacade = projectRoot ? resolvePhaserFacade(projectRoot) : void 0;
|
|
654
|
+
return {
|
|
655
|
+
name: "miaoda-phaser-rexui-scene",
|
|
656
|
+
enforce: "pre",
|
|
657
|
+
configResolved(config) {
|
|
658
|
+
const resolvedProjectRoot = projectRoot ?? config.root;
|
|
659
|
+
scenesRoot ??= (0, import_node_path3.resolve)(resolvedProjectRoot, "src/scenes");
|
|
660
|
+
phaserFacade ??= resolvePhaserFacade(resolvedProjectRoot);
|
|
661
|
+
},
|
|
662
|
+
/** 查询参数属于构建工具元数据,去除后才能稳定判断真实源码位置。 */
|
|
663
|
+
resolveId(source, importer) {
|
|
664
|
+
if (source !== PHASER_MODULE_ID || !importer || !scenesRoot) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const cleanImporter = importer.split("?", 1)[0] ?? importer;
|
|
668
|
+
return isInsideDirectory(cleanImporter, scenesRoot) ? phaserFacade ?? null : null;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
610
673
|
// src/vitest-config.ts
|
|
611
674
|
function defineGameVitestConfig(options) {
|
|
612
675
|
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
|
|
@@ -617,10 +680,11 @@ function defineGameVitestConfig(options) {
|
|
|
617
680
|
);
|
|
618
681
|
}
|
|
619
682
|
return (0, import_config.defineConfig)({
|
|
683
|
+
plugins: [phaserRexUIScenePlugin(options.projectRoot)],
|
|
620
684
|
resolve: {
|
|
621
685
|
alias: {
|
|
622
686
|
...options.aliases,
|
|
623
|
-
"@": (0,
|
|
687
|
+
"@": (0, import_node_path4.resolve)(options.projectRoot, "src")
|
|
624
688
|
}
|
|
625
689
|
},
|
|
626
690
|
// devkit 作为 external ESM 加载时,应用测试也必须 externalize Phaser,
|
package/dist/vitest-config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/vitest-config.ts
|
|
2
|
-
import { resolve } from "path";
|
|
2
|
+
import { resolve as resolve2 } from "path";
|
|
3
3
|
import { defineConfig } from "vitest/config";
|
|
4
4
|
|
|
5
5
|
// src/gameplay-audit.ts
|
|
@@ -46,6 +46,7 @@ function createGameplayContractMetadata(contract) {
|
|
|
46
46
|
}
|
|
47
47
|
function validateGameplayAuditOptions(options, projectRoot) {
|
|
48
48
|
const issues = [];
|
|
49
|
+
const contracts = options.contracts;
|
|
49
50
|
const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
|
|
50
51
|
const mode = options.mode ?? "interactive";
|
|
51
52
|
if (mode === "non-interactive") {
|
|
@@ -67,7 +68,7 @@ function validateGameplayAuditOptions(options, projectRoot) {
|
|
|
67
68
|
const coveredScenes = /* @__PURE__ */ new Set();
|
|
68
69
|
const kinds = /* @__PURE__ */ new Map();
|
|
69
70
|
const invalidKindContracts = [];
|
|
70
|
-
for (const contract of
|
|
71
|
+
for (const contract of contracts ?? []) {
|
|
71
72
|
const id = contract.id.trim();
|
|
72
73
|
if (!id) {
|
|
73
74
|
issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
|
|
@@ -93,7 +94,6 @@ function validateGameplayAuditOptions(options, projectRoot) {
|
|
|
93
94
|
kinds.set(contract.kind, list);
|
|
94
95
|
}
|
|
95
96
|
if (contract.kind === "playthrough") {
|
|
96
|
-
if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
97
97
|
if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
98
98
|
if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
|
|
99
99
|
}
|
|
@@ -101,7 +101,6 @@ function validateGameplayAuditOptions(options, projectRoot) {
|
|
|
101
101
|
issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
|
|
102
102
|
}
|
|
103
103
|
if (contract.kind === "recovery") {
|
|
104
|
-
if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
105
104
|
if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
106
105
|
if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
|
|
107
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`);
|
|
@@ -123,31 +122,24 @@ function validateGameplayAuditOptions(options, projectRoot) {
|
|
|
123
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`
|
|
124
123
|
);
|
|
125
124
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
if (contracts) {
|
|
126
|
+
for (const scene of scenes) {
|
|
127
|
+
if (!coveredScenes.has(scene)) {
|
|
128
|
+
issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
|
|
129
|
+
}
|
|
129
130
|
}
|
|
131
|
+
if (contracts.length === 0)
|
|
132
|
+
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");
|
|
130
133
|
}
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const hasInput =
|
|
136
|
-
|
|
137
|
-
if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
|
|
134
|
+
if (mode === "interactive" && contracts) {
|
|
135
|
+
const hasPlaythrough = (kinds.get("playthrough") ?? []).some(
|
|
136
|
+
(contract) => Boolean(contract.requireFrameAdvance) && normalizeList(contract.requireCheckpoint).length > 0
|
|
137
|
+
);
|
|
138
|
+
const hasInput = contracts.some((contract) => contract.requireInput);
|
|
139
|
+
if (!hasPlaythrough || !hasInput) {
|
|
138
140
|
issues.push(
|
|
139
|
-
|
|
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)
|
|
141
|
+
"\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"
|
|
146
142
|
);
|
|
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
143
|
}
|
|
152
144
|
const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
|
|
153
145
|
if (restartSignal && options.flows?.restart !== true) {
|
|
@@ -209,11 +201,21 @@ function getDefinitionIssues(expected, actual) {
|
|
|
209
201
|
}
|
|
210
202
|
function auditGameplayCollection(options, file, tests) {
|
|
211
203
|
const normalizedFile = file.replaceAll("\\", "/");
|
|
212
|
-
const expected = options.contracts.filter(
|
|
204
|
+
const expected = (options.contracts ?? []).filter(
|
|
213
205
|
(contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
|
|
214
206
|
);
|
|
215
207
|
const actual = tests.filter((test) => test.metadata);
|
|
216
208
|
const issues = [];
|
|
209
|
+
if (!options.contracts) {
|
|
210
|
+
const actualIds = /* @__PURE__ */ new Set();
|
|
211
|
+
for (const test of actual) {
|
|
212
|
+
const id = test.metadata?.id;
|
|
213
|
+
if (!id) continue;
|
|
214
|
+
if (actualIds.has(id)) issues.push(`contract "${id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`);
|
|
215
|
+
actualIds.add(id);
|
|
216
|
+
}
|
|
217
|
+
return { passed: issues.length === 0, issues };
|
|
218
|
+
}
|
|
217
219
|
for (const contract of expected) {
|
|
218
220
|
const matches = actual.filter((test) => test.metadata?.id === contract.id);
|
|
219
221
|
if (matches.length === 0) {
|
|
@@ -286,7 +288,25 @@ function auditGameplayRun(options, tests) {
|
|
|
286
288
|
const issues = [];
|
|
287
289
|
const actualContracts = tests.filter((test) => test.metadata);
|
|
288
290
|
const observedEvidence = [];
|
|
289
|
-
|
|
291
|
+
const discoveredContracts = actualContracts.flatMap((test) => {
|
|
292
|
+
const metadata = test.metadata;
|
|
293
|
+
if (!metadata) return [];
|
|
294
|
+
return [{
|
|
295
|
+
id: metadata.id,
|
|
296
|
+
kind: metadata.kind,
|
|
297
|
+
testFile: test.file,
|
|
298
|
+
scenes: metadata.scenes,
|
|
299
|
+
...metadata.requirements
|
|
300
|
+
}];
|
|
301
|
+
});
|
|
302
|
+
const expectedContracts = options.contracts ?? discoveredContracts;
|
|
303
|
+
if (!options.contracts) {
|
|
304
|
+
const discoveredIssues = validateGameplayAuditOptions(
|
|
305
|
+
{ ...options, contracts: discoveredContracts }
|
|
306
|
+
);
|
|
307
|
+
issues.push(...discoveredIssues);
|
|
308
|
+
}
|
|
309
|
+
for (const expected of expectedContracts) {
|
|
290
310
|
const matches = actualContracts.filter(
|
|
291
311
|
(test2) => test2.metadata?.id === expected.id
|
|
292
312
|
);
|
|
@@ -329,7 +349,7 @@ function auditGameplayRun(options, tests) {
|
|
|
329
349
|
}
|
|
330
350
|
for (const test of actualContracts) {
|
|
331
351
|
const id = test.metadata?.id;
|
|
332
|
-
if (id && !options.contracts.some((contract) => contract.id === id)) {
|
|
352
|
+
if (id && options.contracts && !options.contracts.some((contract) => contract.id === id)) {
|
|
333
353
|
issues.push(
|
|
334
354
|
`\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
|
|
335
355
|
);
|
|
@@ -524,7 +544,7 @@ var GameplayAuditReporter = class {
|
|
|
524
544
|
(specification) => normalizePath(relative(this.projectRoot, specification.moduleId))
|
|
525
545
|
)
|
|
526
546
|
);
|
|
527
|
-
for (const contract of this.options.contracts) {
|
|
547
|
+
for (const contract of this.options.contracts ?? []) {
|
|
528
548
|
const testFile = normalizePath(contract.testFile);
|
|
529
549
|
if (!scheduledFiles.has(testFile)) {
|
|
530
550
|
this.reportPreflightIssue(
|
|
@@ -568,7 +588,7 @@ var GameplayAuditReporter = class {
|
|
|
568
588
|
} else {
|
|
569
589
|
console.log(
|
|
570
590
|
`
|
|
571
|
-
GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${
|
|
591
|
+
GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${tests.filter((test) => test.metadata).length} contracts, ${this.options.scenes.length} scenes)`
|
|
572
592
|
);
|
|
573
593
|
console.log(
|
|
574
594
|
"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"
|
|
@@ -583,6 +603,49 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts
|
|
|
583
603
|
}
|
|
584
604
|
};
|
|
585
605
|
|
|
606
|
+
// src/vite.ts
|
|
607
|
+
import { existsSync } from "fs";
|
|
608
|
+
import { createRequire } from "module";
|
|
609
|
+
import { dirname, join as join2, normalize, resolve, sep } from "path";
|
|
610
|
+
var PHASER_MODULE_ID = "phaser";
|
|
611
|
+
function resolvePhaserFacade(projectRoot) {
|
|
612
|
+
const requireFromProject = createRequire(join2(projectRoot, "package.json"));
|
|
613
|
+
const viteEntry = requireFromProject.resolve("miaoda-game-devkit/vite");
|
|
614
|
+
const facade = join2(dirname(viteEntry), "phaser-facade.mjs");
|
|
615
|
+
if (!existsSync(facade)) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
`miaoda-game-devkit \u7F3A\u5C11 Phaser facade\uFF1A${facade}`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
return facade;
|
|
621
|
+
}
|
|
622
|
+
function isInsideDirectory(filePath, directory) {
|
|
623
|
+
const normalizedFile = normalize(filePath);
|
|
624
|
+
const normalizedDirectory = `${normalize(directory)}${sep}`;
|
|
625
|
+
return normalizedFile.startsWith(normalizedDirectory);
|
|
626
|
+
}
|
|
627
|
+
function phaserRexUIScenePlugin(projectRoot) {
|
|
628
|
+
let scenesRoot = projectRoot ? resolve(projectRoot, "src/scenes") : void 0;
|
|
629
|
+
let phaserFacade = projectRoot ? resolvePhaserFacade(projectRoot) : void 0;
|
|
630
|
+
return {
|
|
631
|
+
name: "miaoda-phaser-rexui-scene",
|
|
632
|
+
enforce: "pre",
|
|
633
|
+
configResolved(config) {
|
|
634
|
+
const resolvedProjectRoot = projectRoot ?? config.root;
|
|
635
|
+
scenesRoot ??= resolve(resolvedProjectRoot, "src/scenes");
|
|
636
|
+
phaserFacade ??= resolvePhaserFacade(resolvedProjectRoot);
|
|
637
|
+
},
|
|
638
|
+
/** 查询参数属于构建工具元数据,去除后才能稳定判断真实源码位置。 */
|
|
639
|
+
resolveId(source, importer) {
|
|
640
|
+
if (source !== PHASER_MODULE_ID || !importer || !scenesRoot) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const cleanImporter = importer.split("?", 1)[0] ?? importer;
|
|
644
|
+
return isInsideDirectory(cleanImporter, scenesRoot) ? phaserFacade ?? null : null;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
586
649
|
// src/vitest-config.ts
|
|
587
650
|
function defineGameVitestConfig(options) {
|
|
588
651
|
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
|
|
@@ -593,10 +656,11 @@ function defineGameVitestConfig(options) {
|
|
|
593
656
|
);
|
|
594
657
|
}
|
|
595
658
|
return defineConfig({
|
|
659
|
+
plugins: [phaserRexUIScenePlugin(options.projectRoot)],
|
|
596
660
|
resolve: {
|
|
597
661
|
alias: {
|
|
598
662
|
...options.aliases,
|
|
599
|
-
"@":
|
|
663
|
+
"@": resolve2(options.projectRoot, "src")
|
|
600
664
|
}
|
|
601
665
|
},
|
|
602
666
|
// devkit 作为 external ESM 加载时,应用测试也必须 externalize Phaser,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miaoda-game-devkit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Shared lint and deterministic Phaser HEADLESS testing tools for Miaoda games",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,7 +30,48 @@
|
|
|
30
30
|
"default": "./dist/vitest-config.js"
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
|
+
"./vite": {
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/vite.d.mts",
|
|
36
|
+
"default": "./dist/vite.mjs"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/vite.d.ts",
|
|
40
|
+
"default": "./dist/vite.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
33
43
|
"./vitest-setup": "./dist/lint/setup.mjs",
|
|
44
|
+
"./react": {
|
|
45
|
+
"import": {
|
|
46
|
+
"types": "./dist/react/index.d.mts",
|
|
47
|
+
"default": "./dist/react/index.mjs"
|
|
48
|
+
},
|
|
49
|
+
"require": {
|
|
50
|
+
"types": "./dist/react/index.d.ts",
|
|
51
|
+
"default": "./dist/react/index.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"./react/testing": {
|
|
55
|
+
"import": {
|
|
56
|
+
"types": "./dist/react/testing.d.mts",
|
|
57
|
+
"default": "./dist/react/testing.mjs"
|
|
58
|
+
},
|
|
59
|
+
"require": {
|
|
60
|
+
"types": "./dist/react/testing.d.ts",
|
|
61
|
+
"default": "./dist/react/testing.js"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"./react/vitest-config": {
|
|
65
|
+
"import": {
|
|
66
|
+
"types": "./dist/react/vitest-config.d.mts",
|
|
67
|
+
"default": "./dist/react/vitest-config.mjs"
|
|
68
|
+
},
|
|
69
|
+
"require": {
|
|
70
|
+
"types": "./dist/react/vitest-config.d.ts",
|
|
71
|
+
"default": "./dist/react/vitest-config.js"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"./react/vitest-setup": "./dist/react/vitest-setup.mjs",
|
|
34
75
|
"./biome": "./biome-config.json",
|
|
35
76
|
"./tsconfig-base": "./tsconfig-base.json"
|
|
36
77
|
},
|
|
@@ -80,7 +121,36 @@
|
|
|
80
121
|
"vitest": "^4.1.10"
|
|
81
122
|
},
|
|
82
123
|
"peerDependencies": {
|
|
83
|
-
"
|
|
124
|
+
"@testing-library/jest-dom": ">=6 <8",
|
|
125
|
+
"@testing-library/react": ">=16 <17",
|
|
126
|
+
"phaser": ">=4.0.0 <5",
|
|
127
|
+
"phaser4-rex-plugins": ">=4.0.0 <5",
|
|
128
|
+
"react": ">=19 <20",
|
|
129
|
+
"react-dom": ">=19 <20",
|
|
130
|
+
"vite": ">=6 <9"
|
|
131
|
+
},
|
|
132
|
+
"peerDependenciesMeta": {
|
|
133
|
+
"@testing-library/jest-dom": {
|
|
134
|
+
"optional": true
|
|
135
|
+
},
|
|
136
|
+
"@testing-library/react": {
|
|
137
|
+
"optional": true
|
|
138
|
+
},
|
|
139
|
+
"phaser": {
|
|
140
|
+
"optional": true
|
|
141
|
+
},
|
|
142
|
+
"phaser4-rex-plugins": {
|
|
143
|
+
"optional": true
|
|
144
|
+
},
|
|
145
|
+
"react": {
|
|
146
|
+
"optional": true
|
|
147
|
+
},
|
|
148
|
+
"react-dom": {
|
|
149
|
+
"optional": true
|
|
150
|
+
},
|
|
151
|
+
"vite": {
|
|
152
|
+
"optional": true
|
|
153
|
+
}
|
|
84
154
|
},
|
|
85
155
|
"publishConfig": {
|
|
86
156
|
"registry": "https://registry.npmjs.org/",
|