miaoda-game-devkit 0.1.0
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 +70 -0
- package/bin/miaoda-game-lint.js +3 -0
- package/biome-config.json +24 -0
- package/dist/cli/lint.js +93 -0
- package/dist/index.d.mts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +827 -0
- package/dist/index.mjs +785 -0
- package/dist/lint/setup.mjs +125 -0
- package/dist/rules/check-image-import-plugin.js +92 -0
- package/dist/rules/check-style-import-plugin.js +92 -0
- package/dist/vitest-config.d.mts +23 -0
- package/dist/vitest-config.d.ts +23 -0
- package/dist/vitest-config.js +83 -0
- package/dist/vitest-config.mjs +60 -0
- package/oxlint-config.json +14 -0
- package/package.json +89 -0
- package/tsconfig-base.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# miaoda-game-devkit
|
|
2
|
+
|
|
3
|
+
妙搭 Phaser 3.90+ 和 Phaser 4 游戏模板的共享开发基础设施。
|
|
4
|
+
|
|
5
|
+
该开发工具包提供:
|
|
6
|
+
|
|
7
|
+
- 支持 DOM 鼠标输入的确定性 Phaser 无头测试宿主;
|
|
8
|
+
- 不依赖渲染器的运行时、交互区域和文字健康检查;
|
|
9
|
+
- 由模板平台维护的通用 Vitest 契约测试;
|
|
10
|
+
- 统一的 `miaoda-game-lint` 命令,聚合 TypeScript、Biome、Tailwind、
|
|
11
|
+
Oxlint 和 Phaser 通用契约检查。
|
|
12
|
+
- 统一的 `defineGameVitestConfig` 配置工厂,固定 Phaser 无头测试依赖的基础配置,
|
|
13
|
+
同时允许每个游戏配置自身的测试范围、覆盖率和超时等业务选项。
|
|
14
|
+
|
|
15
|
+
`createHeadlessGame` 还记录测试期间真实发生的输入、完整帧、物理步和 Scene 切换。
|
|
16
|
+
玩法测试可以调用 `host.assertGameplayEvidence()` 把这些客观事实设为门禁;它不推断
|
|
17
|
+
业务语义,authoritative state 仍必须由测试显式断言。
|
|
18
|
+
|
|
19
|
+
需要将证据绑定到 Vitest 单测试时,使用 `registerGameplayContract` 并传入测试上下文
|
|
20
|
+
中的 `task` 和 `onTestFinished`。验证在测试完成生命周期执行,支持与模板通用
|
|
21
|
+
`afterEach` cleanup 配合;contract id 和 evidence 摘要只通过可序列化 metadata 传递。
|
|
22
|
+
|
|
23
|
+
模板直接调用开发工具包提供的命令行程序:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"scripts": {
|
|
28
|
+
"lint": "miaoda-game-lint"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
开发工具包自行维护工具版本,并从自身依赖中解析可执行文件,使用方不需要重复声明
|
|
34
|
+
Biome、Oxlint 或 tsgo。仓库中保留稳定的命令入口,因此 `dist/` 尚未生成时,pnpm
|
|
35
|
+
也能正确建立命令链接;工作区开发状态下由 Nx 在模板检查前构建开发工具包,发布包则
|
|
36
|
+
直接携带运行时 `dist/`。`dist/lint/**/*.test.*` 仅供开发工具包自身的契约测试使用,
|
|
37
|
+
由发布文件清单排除,不会进入 npm 包。
|
|
38
|
+
|
|
39
|
+
monorepo 中的模板可以放置只包含 `extends: ["miaoda-game-devkit/biome"]` 的薄配置,
|
|
40
|
+
用于让 Biome 从模板 package.json 判断依赖声明。规则本身仍由本开发工具包维护;生成后的
|
|
41
|
+
独立模板位于仓库根目录,不需要这个 workspace 定位配置。
|
|
42
|
+
|
|
43
|
+
模板的 `tsconfig.json` 应继承 `miaoda-game-devkit/tsconfig-base`,只保留源码范围和路径
|
|
44
|
+
别名等项目差异。TypeScript 严格模式、bundler 模块解析和无输出检查等平台基线由开发
|
|
45
|
+
工具包集中维护,`miaoda-game-lint` 会直接检查模板的这个配置。
|
|
46
|
+
|
|
47
|
+
具体游戏的玩法测试仍由模板使用方放在 `tests/`。运行时测试从本开发工具包
|
|
48
|
+
导入 `createHeadlessGame`,通过真实 DOM 输入驱动游戏,并断言有意义的业务状态变化。
|
|
49
|
+
同步帧逻辑使用 `stepFrames()`;`update()` 返回 Promise 时使用
|
|
50
|
+
`await stepFramesAsync()`,其他同步操作启动生命周期 Promise 后使用 `await settle()`。
|
|
51
|
+
|
|
52
|
+
模板的 `vitest.config.ts` 应从 `miaoda-game-devkit/vitest-config` 导入
|
|
53
|
+
`defineGameVitestConfig`。JSDOM 环境、Phaser 浏览器构建别名、通用 setup 和 mock 隔离
|
|
54
|
+
以及 Scene coverage 均由开发工具包统一维护。模板只传入 `projectRoot`;确有需要时,可以
|
|
55
|
+
通过 `aliases`、`additionalSetupFiles`、`testTimeout` 和 `hookTimeout` 增加安全的项目配置,
|
|
56
|
+
不能覆盖测试范围、运行环境或 coverage 基线。
|
|
57
|
+
|
|
58
|
+
## 发布顺序
|
|
59
|
+
|
|
60
|
+
1. 在开发工具包中运行类型检查和测试。
|
|
61
|
+
2. 使用 `pnpm run release:dry-run` 检查将要发布的文件和清理后的 `package.json`。
|
|
62
|
+
3. 根据版本类型运行 `pnpm run release:patch`、`release:minor` 或 `release:major`。
|
|
63
|
+
发布脚本会先测试和构建,再通过 `clean-publish` 从临时目录发布;发布包会移除
|
|
64
|
+
`devDependencies` 和开发脚本,不会修改工作区中的原始 `package.json`。
|
|
65
|
+
4. 也可以使用 `pnpm run release` 发布已经手动设置好版本的开发工具包。
|
|
66
|
+
5. 最后运行模板的 `pnpm run build:template`,让 `output/` 使用已发布版本重新生成锁文件。
|
|
67
|
+
|
|
68
|
+
`pnpm-workspace.yaml` 已将 `miaoda-game-devkit` 加入 `minimumReleaseAgeExclude`,
|
|
69
|
+
因此新版本发布后无需等待 24 小时即可生成模板锁文件。`--skip-install` 仅用于发布前
|
|
70
|
+
检查 `output/` 的产物边界,不能用于正式 CI 构建。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": {
|
|
3
|
+
"includes": ["**/src/**/*.{js,ts}"]
|
|
4
|
+
},
|
|
5
|
+
"linter": {
|
|
6
|
+
"enabled": true,
|
|
7
|
+
"rules": {
|
|
8
|
+
"recommended": false,
|
|
9
|
+
"correctness": {
|
|
10
|
+
"noUndeclaredDependencies": "error",
|
|
11
|
+
"useExhaustiveDependencies": "warn"
|
|
12
|
+
},
|
|
13
|
+
"suspicious": {
|
|
14
|
+
"noRedeclare": "error"
|
|
15
|
+
},
|
|
16
|
+
"style": {
|
|
17
|
+
"noCommonJs": "error"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"formatter": {
|
|
22
|
+
"enabled": false
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/cli/lint.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli/lint.ts
|
|
5
|
+
var import_node_child_process = require("child_process");
|
|
6
|
+
var import_node_fs = require("fs");
|
|
7
|
+
var import_node_module = require("module");
|
|
8
|
+
var import_node_os = require("os");
|
|
9
|
+
var import_node_path = require("path");
|
|
10
|
+
var devkitRoot = (0, import_node_path.resolve)(__dirname, "../..");
|
|
11
|
+
var projectRoot = process.cwd();
|
|
12
|
+
var requireFromDevkit = (0, import_node_module.createRequire)((0, import_node_path.join)(devkitRoot, "package.json"));
|
|
13
|
+
var contractsOnly = process.argv.slice(2).includes("--contracts-only");
|
|
14
|
+
var projectBiomeConfig = (0, import_node_path.join)(projectRoot, "biome.json");
|
|
15
|
+
var biomeConfig = (0, import_node_fs.existsSync)(projectBiomeConfig) ? projectBiomeConfig : (0, import_node_path.join)(devkitRoot, "biome-config.json");
|
|
16
|
+
function resolveBin(packageName, binName) {
|
|
17
|
+
const packageJsonPath = requireFromDevkit.resolve(`${packageName}/package.json`);
|
|
18
|
+
const packageJson = JSON.parse((0, import_node_fs.readFileSync)(packageJsonPath, "utf8"));
|
|
19
|
+
const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
|
|
20
|
+
if (!relativeBin) {
|
|
21
|
+
throw new Error(`${packageName} does not expose the ${binName} executable`);
|
|
22
|
+
}
|
|
23
|
+
return (0, import_node_path.resolve)((0, import_node_path.dirname)(packageJsonPath), relativeBin);
|
|
24
|
+
}
|
|
25
|
+
function run(name, packageName, binName, args) {
|
|
26
|
+
return new Promise((resolveResult) => {
|
|
27
|
+
const child = (0, import_node_child_process.spawn)(process.execPath, [resolveBin(packageName, binName), ...args], {
|
|
28
|
+
cwd: projectRoot,
|
|
29
|
+
env: process.env,
|
|
30
|
+
stdio: "inherit"
|
|
31
|
+
});
|
|
32
|
+
child.once("error", (error) => {
|
|
33
|
+
console.error(`[${name}] ${error.message}`);
|
|
34
|
+
resolveResult({ name, ok: false });
|
|
35
|
+
});
|
|
36
|
+
child.once("exit", (code) => {
|
|
37
|
+
resolveResult({ name, ok: code === 0 });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function runContracts() {
|
|
42
|
+
return run(
|
|
43
|
+
"vitest",
|
|
44
|
+
"vitest",
|
|
45
|
+
"vitest",
|
|
46
|
+
["run", "--config", (0, import_node_path.join)(devkitRoot, "dist/lint/vitest.config.mjs")]
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
async function runAllChecks() {
|
|
50
|
+
if (contractsOnly) {
|
|
51
|
+
return [await runContracts()];
|
|
52
|
+
}
|
|
53
|
+
const [tsgo, biome] = await Promise.all([
|
|
54
|
+
run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
|
|
55
|
+
run("biome", "@biomejs/biome", "biome", [
|
|
56
|
+
"lint",
|
|
57
|
+
"--config-path",
|
|
58
|
+
biomeConfig,
|
|
59
|
+
"src"
|
|
60
|
+
])
|
|
61
|
+
]);
|
|
62
|
+
const tailwindOutput = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
|
|
63
|
+
const tailwind = await run("tailwind", "tailwindcss", "tailwindcss", [
|
|
64
|
+
"-i",
|
|
65
|
+
(0, import_node_path.join)(projectRoot, "src/index.css"),
|
|
66
|
+
"-o",
|
|
67
|
+
tailwindOutput
|
|
68
|
+
]);
|
|
69
|
+
(0, import_node_fs.rmSync)(tailwindOutput, { force: true });
|
|
70
|
+
const oxlint = await run("oxlint", "oxlint", "oxlint", [
|
|
71
|
+
"-c",
|
|
72
|
+
(0, import_node_path.join)(devkitRoot, "oxlint-config.json"),
|
|
73
|
+
"src"
|
|
74
|
+
]);
|
|
75
|
+
const contracts = await runContracts();
|
|
76
|
+
return [tsgo, biome, tailwind, oxlint, contracts];
|
|
77
|
+
}
|
|
78
|
+
async function main() {
|
|
79
|
+
const results = await runAllChecks();
|
|
80
|
+
const failed = results.filter((result) => !result.ok);
|
|
81
|
+
console.log("");
|
|
82
|
+
if (failed.length === 0) {
|
|
83
|
+
console.log("RESULT: ALL CHECKS PASSED - Finished with 0 errors. Found no issues.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
console.error(`RESULT: FAILED - Found errors in: ${failed.map(({ name }) => name).join(", ")}`);
|
|
87
|
+
console.error("Fix all lint errors, then re-run 'pnpm lint'.");
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
90
|
+
main().catch((error) => {
|
|
91
|
+
console.error(error);
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
});
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as Phaser from 'phaser';
|
|
2
|
+
|
|
3
|
+
interface HeadlessGameHost<TScene extends Phaser.Scene> {
|
|
4
|
+
game: Phaser.Game;
|
|
5
|
+
scene: TScene;
|
|
6
|
+
input: HeadlessInputDriver;
|
|
7
|
+
/** 当前隔离 host 实际执行过的客观操作证据。 */
|
|
8
|
+
readonly evidence: Readonly<HeadlessGameplayEvidence>;
|
|
9
|
+
/** 当缺少所要求的输入、时间、物理或转场证据时失败。 */
|
|
10
|
+
assertGameplayEvidence(requirements?: HeadlessGameplayEvidenceRequirements): void;
|
|
11
|
+
settle(): Promise<void>;
|
|
12
|
+
stepFrames(count?: number): void;
|
|
13
|
+
stepFramesAsync(count?: number): Promise<void>;
|
|
14
|
+
stepUntil(condition: () => boolean, maxFrames?: number): number;
|
|
15
|
+
stepPhysics(steps?: number): void;
|
|
16
|
+
assertTextHealth(): void;
|
|
17
|
+
destroy(): void;
|
|
18
|
+
}
|
|
19
|
+
interface HeadlessGameplayEvidence {
|
|
20
|
+
/** 通过 host 公共 API 推进的完整 Phaser 帧数。 */
|
|
21
|
+
frames: number;
|
|
22
|
+
/** 显式执行的 Arcade Physics 单步数。 */
|
|
23
|
+
physicsSteps: number;
|
|
24
|
+
/** 投递到 Phaser canvas 的 DOM 鼠标事件数。 */
|
|
25
|
+
mouseEvents: number;
|
|
26
|
+
/** 通过输入辅助方法投递的复合 click 次数。 */
|
|
27
|
+
clicks: number;
|
|
28
|
+
/** 通过 Scene Plugin 公共 API 观察到的 Scene 操作。 */
|
|
29
|
+
transitions: ReadonlyArray<{
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
method: "start" | "launch" | "switch" | "sleep" | "wake";
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
interface HeadlessGameplayEvidenceRequirements {
|
|
36
|
+
/** 至少要求一次真实 DOM 鼠标事件。 */
|
|
37
|
+
requireInput?: boolean;
|
|
38
|
+
/** 至少要求推进一个完整 Phaser 帧。 */
|
|
39
|
+
requireFrameAdvance?: boolean;
|
|
40
|
+
/** 至少要求执行一次显式 Arcade Physics 单步。 */
|
|
41
|
+
requirePhysicsStep?: boolean;
|
|
42
|
+
/** 要求发生一个或多个目标 key 匹配的 Scene 转场。 */
|
|
43
|
+
requireTransition?: string | string[];
|
|
44
|
+
}
|
|
45
|
+
interface HeadlessCanvasBounds {
|
|
46
|
+
left: number;
|
|
47
|
+
top: number;
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
}
|
|
51
|
+
interface HeadlessInputPoint {
|
|
52
|
+
clientX: number;
|
|
53
|
+
clientY: number;
|
|
54
|
+
}
|
|
55
|
+
interface HeadlessMouseInput {
|
|
56
|
+
move(x: number, y: number, buttons?: number): Promise<void>;
|
|
57
|
+
down(x: number, y: number): Promise<void>;
|
|
58
|
+
up(x: number, y: number): Promise<void>;
|
|
59
|
+
click(x: number, y: number): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
interface HeadlessInputDriver {
|
|
62
|
+
readonly canvas: HTMLCanvasElement;
|
|
63
|
+
readonly mouse: HeadlessMouseInput;
|
|
64
|
+
setCanvasBounds(bounds: HeadlessCanvasBounds): void;
|
|
65
|
+
gameToClient(x: number, y: number): HeadlessInputPoint;
|
|
66
|
+
settle(): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
type HeadlessGameOptions = Omit<Phaser.Types.Core.GameConfig, "type" | "scene" | "callbacks"> & {
|
|
69
|
+
/** Maximum time to wait for Scene creation to complete. */
|
|
70
|
+
bootTimeoutMs?: number;
|
|
71
|
+
/** Scene types that must be registered before the initial Scene starts. */
|
|
72
|
+
additionalScenes?: Phaser.Types.Scenes.SceneType[];
|
|
73
|
+
};
|
|
74
|
+
/** Creates one isolated Phaser game and resolves after the scene has run create(). */
|
|
75
|
+
declare function createHeadlessGame<TScene extends Phaser.Scene>(scene: TScene, options?: HeadlessGameOptions): Promise<HeadlessGameHost<TScene>>;
|
|
76
|
+
|
|
77
|
+
interface GameplayContractOptions extends HeadlessGameplayEvidenceRequirements {
|
|
78
|
+
/** 生产玩法清单中稳定且可读的 contract 标识。 */
|
|
79
|
+
id: string;
|
|
80
|
+
}
|
|
81
|
+
interface GameplayContractHandle<TScene extends Phaser.Scene> {
|
|
82
|
+
/** 将当前 contract 绑定到实际创建的 HEADLESS host。 */
|
|
83
|
+
attachHost(host: HeadlessGameHost<TScene>): void;
|
|
84
|
+
/** 立即验证证据;通常由生命周期回调自动执行。 */
|
|
85
|
+
verify(): void;
|
|
86
|
+
}
|
|
87
|
+
interface GameplayTestContext {
|
|
88
|
+
task: {
|
|
89
|
+
meta: Record<string, unknown>;
|
|
90
|
+
};
|
|
91
|
+
onTestFinished(callback: () => void | Promise<void>): void;
|
|
92
|
+
onTestFailed?: (callback: () => void) => void;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 将一个 gameplay contract 绑定到当前 Vitest 测试。
|
|
96
|
+
*
|
|
97
|
+
* 证据校验在 `onTestFinished` 中执行,晚于 `afterEach`,因此即使测试使用
|
|
98
|
+
* 通用 cleanup 销毁 host,也仍可读取 host 的 renderer-independent evidence。
|
|
99
|
+
* contract 元数据只包含可序列化值,供 reporter 在 worker 之外汇总。
|
|
100
|
+
*/
|
|
101
|
+
declare function registerGameplayContract<TScene extends Phaser.Scene>(context: GameplayTestContext, options: GameplayContractOptions): GameplayContractHandle<TScene>;
|
|
102
|
+
|
|
103
|
+
declare function assertSceneRuntimeHealth(scene: Phaser.Scene): void;
|
|
104
|
+
declare function assertSceneInteractiveHealth(scene: Phaser.Scene): void;
|
|
105
|
+
|
|
106
|
+
/** Returns non-whitespace UTF-16 code units that are absent from a Bitmap Font. */
|
|
107
|
+
declare function collectMissingBitmapGlyphs(text: string, chars: Record<number, unknown>): string[];
|
|
108
|
+
/** Throws an actionable error suitable for CI and coding-agent feedback. */
|
|
109
|
+
declare function assertSceneTextHealth(scene: Phaser.Scene): void;
|
|
110
|
+
|
|
111
|
+
export { type GameplayContractHandle, type GameplayContractOptions, type GameplayTestContext, type HeadlessCanvasBounds, type HeadlessGameHost, type HeadlessGameOptions, type HeadlessGameplayEvidence, type HeadlessGameplayEvidenceRequirements, type HeadlessInputDriver, type HeadlessInputPoint, type HeadlessMouseInput, assertSceneInteractiveHealth, assertSceneRuntimeHealth, assertSceneTextHealth, collectMissingBitmapGlyphs, createHeadlessGame, registerGameplayContract };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as Phaser from 'phaser';
|
|
2
|
+
|
|
3
|
+
interface HeadlessGameHost<TScene extends Phaser.Scene> {
|
|
4
|
+
game: Phaser.Game;
|
|
5
|
+
scene: TScene;
|
|
6
|
+
input: HeadlessInputDriver;
|
|
7
|
+
/** 当前隔离 host 实际执行过的客观操作证据。 */
|
|
8
|
+
readonly evidence: Readonly<HeadlessGameplayEvidence>;
|
|
9
|
+
/** 当缺少所要求的输入、时间、物理或转场证据时失败。 */
|
|
10
|
+
assertGameplayEvidence(requirements?: HeadlessGameplayEvidenceRequirements): void;
|
|
11
|
+
settle(): Promise<void>;
|
|
12
|
+
stepFrames(count?: number): void;
|
|
13
|
+
stepFramesAsync(count?: number): Promise<void>;
|
|
14
|
+
stepUntil(condition: () => boolean, maxFrames?: number): number;
|
|
15
|
+
stepPhysics(steps?: number): void;
|
|
16
|
+
assertTextHealth(): void;
|
|
17
|
+
destroy(): void;
|
|
18
|
+
}
|
|
19
|
+
interface HeadlessGameplayEvidence {
|
|
20
|
+
/** 通过 host 公共 API 推进的完整 Phaser 帧数。 */
|
|
21
|
+
frames: number;
|
|
22
|
+
/** 显式执行的 Arcade Physics 单步数。 */
|
|
23
|
+
physicsSteps: number;
|
|
24
|
+
/** 投递到 Phaser canvas 的 DOM 鼠标事件数。 */
|
|
25
|
+
mouseEvents: number;
|
|
26
|
+
/** 通过输入辅助方法投递的复合 click 次数。 */
|
|
27
|
+
clicks: number;
|
|
28
|
+
/** 通过 Scene Plugin 公共 API 观察到的 Scene 操作。 */
|
|
29
|
+
transitions: ReadonlyArray<{
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
method: "start" | "launch" | "switch" | "sleep" | "wake";
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
interface HeadlessGameplayEvidenceRequirements {
|
|
36
|
+
/** 至少要求一次真实 DOM 鼠标事件。 */
|
|
37
|
+
requireInput?: boolean;
|
|
38
|
+
/** 至少要求推进一个完整 Phaser 帧。 */
|
|
39
|
+
requireFrameAdvance?: boolean;
|
|
40
|
+
/** 至少要求执行一次显式 Arcade Physics 单步。 */
|
|
41
|
+
requirePhysicsStep?: boolean;
|
|
42
|
+
/** 要求发生一个或多个目标 key 匹配的 Scene 转场。 */
|
|
43
|
+
requireTransition?: string | string[];
|
|
44
|
+
}
|
|
45
|
+
interface HeadlessCanvasBounds {
|
|
46
|
+
left: number;
|
|
47
|
+
top: number;
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
}
|
|
51
|
+
interface HeadlessInputPoint {
|
|
52
|
+
clientX: number;
|
|
53
|
+
clientY: number;
|
|
54
|
+
}
|
|
55
|
+
interface HeadlessMouseInput {
|
|
56
|
+
move(x: number, y: number, buttons?: number): Promise<void>;
|
|
57
|
+
down(x: number, y: number): Promise<void>;
|
|
58
|
+
up(x: number, y: number): Promise<void>;
|
|
59
|
+
click(x: number, y: number): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
interface HeadlessInputDriver {
|
|
62
|
+
readonly canvas: HTMLCanvasElement;
|
|
63
|
+
readonly mouse: HeadlessMouseInput;
|
|
64
|
+
setCanvasBounds(bounds: HeadlessCanvasBounds): void;
|
|
65
|
+
gameToClient(x: number, y: number): HeadlessInputPoint;
|
|
66
|
+
settle(): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
type HeadlessGameOptions = Omit<Phaser.Types.Core.GameConfig, "type" | "scene" | "callbacks"> & {
|
|
69
|
+
/** Maximum time to wait for Scene creation to complete. */
|
|
70
|
+
bootTimeoutMs?: number;
|
|
71
|
+
/** Scene types that must be registered before the initial Scene starts. */
|
|
72
|
+
additionalScenes?: Phaser.Types.Scenes.SceneType[];
|
|
73
|
+
};
|
|
74
|
+
/** Creates one isolated Phaser game and resolves after the scene has run create(). */
|
|
75
|
+
declare function createHeadlessGame<TScene extends Phaser.Scene>(scene: TScene, options?: HeadlessGameOptions): Promise<HeadlessGameHost<TScene>>;
|
|
76
|
+
|
|
77
|
+
interface GameplayContractOptions extends HeadlessGameplayEvidenceRequirements {
|
|
78
|
+
/** 生产玩法清单中稳定且可读的 contract 标识。 */
|
|
79
|
+
id: string;
|
|
80
|
+
}
|
|
81
|
+
interface GameplayContractHandle<TScene extends Phaser.Scene> {
|
|
82
|
+
/** 将当前 contract 绑定到实际创建的 HEADLESS host。 */
|
|
83
|
+
attachHost(host: HeadlessGameHost<TScene>): void;
|
|
84
|
+
/** 立即验证证据;通常由生命周期回调自动执行。 */
|
|
85
|
+
verify(): void;
|
|
86
|
+
}
|
|
87
|
+
interface GameplayTestContext {
|
|
88
|
+
task: {
|
|
89
|
+
meta: Record<string, unknown>;
|
|
90
|
+
};
|
|
91
|
+
onTestFinished(callback: () => void | Promise<void>): void;
|
|
92
|
+
onTestFailed?: (callback: () => void) => void;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 将一个 gameplay contract 绑定到当前 Vitest 测试。
|
|
96
|
+
*
|
|
97
|
+
* 证据校验在 `onTestFinished` 中执行,晚于 `afterEach`,因此即使测试使用
|
|
98
|
+
* 通用 cleanup 销毁 host,也仍可读取 host 的 renderer-independent evidence。
|
|
99
|
+
* contract 元数据只包含可序列化值,供 reporter 在 worker 之外汇总。
|
|
100
|
+
*/
|
|
101
|
+
declare function registerGameplayContract<TScene extends Phaser.Scene>(context: GameplayTestContext, options: GameplayContractOptions): GameplayContractHandle<TScene>;
|
|
102
|
+
|
|
103
|
+
declare function assertSceneRuntimeHealth(scene: Phaser.Scene): void;
|
|
104
|
+
declare function assertSceneInteractiveHealth(scene: Phaser.Scene): void;
|
|
105
|
+
|
|
106
|
+
/** Returns non-whitespace UTF-16 code units that are absent from a Bitmap Font. */
|
|
107
|
+
declare function collectMissingBitmapGlyphs(text: string, chars: Record<number, unknown>): string[];
|
|
108
|
+
/** Throws an actionable error suitable for CI and coding-agent feedback. */
|
|
109
|
+
declare function assertSceneTextHealth(scene: Phaser.Scene): void;
|
|
110
|
+
|
|
111
|
+
export { type GameplayContractHandle, type GameplayContractOptions, type GameplayTestContext, type HeadlessCanvasBounds, type HeadlessGameHost, type HeadlessGameOptions, type HeadlessGameplayEvidence, type HeadlessGameplayEvidenceRequirements, type HeadlessInputDriver, type HeadlessInputPoint, type HeadlessMouseInput, assertSceneInteractiveHealth, assertSceneRuntimeHealth, assertSceneTextHealth, collectMissingBitmapGlyphs, createHeadlessGame, registerGameplayContract };
|