override spike-first(创始人 06-23 裁),全栈铺 Phaser tier2 引擎。recon→契约→6模块并行→集成 workflow 产出 ~60 文件,全 6c6g 静态校验过、红线零碰 Tier0/1 产线: - 契约(6):tier2-source-project.schema / tier2-verdict.schema(fork·round放开·三层校验+富游戏门)/ toolkit 签名 / 探针钩子 / boot-phaser-host.d.ts / mini-肥鹅 fixture 规格 - M1 Phaser scaffold+装载 host+引擎能力面(19;logic-smoke 13/13:五耦合点真接线+赢输双路径+latch不回弹) - M2 CDP 探针 Phaser 重写+business-sim driver+九门+富游戏三门(5;verdict 过 schema) - M3 Python 单写 ReAct agent loop+9 工具 toolkit+M3 Anthropic接法+四熔断(9;mini-desktop 真 2.0.2 venv 验:import/9工具/中间件注册 OK) - M4 datatable schema+金标+资产占位(4)/ M5 prompt+Phaser skill+rag(13)/ M6 成本 RecordingChatModel+trace adapter(3) - 集成:run_engine.py 入口 + 接线断点已修 + RUN-ON-MINI-DESKTOP.md 待 mini-desktop 真跑(esbuild build + CDP 九门 + M3 生成 = 本质即 0号 spike)。AgentScope 2.0.2 API 逐条对 /root/oss/agentscope 源码核验。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
157 lines
12 KiB
Markdown
157 lines
12 KiB
Markdown
---
|
||
name: phaser-rich-game
|
||
description: 教 agent 在 Phaser 3 上写多系统经营富游戏(合成/订单/资源耦合)——scene 树搭建、输入命中、纯逻辑系统与渲染分离、与 tier2 第二装载分支宿主契约对接(boot/readState/destroy、latch 终态、语义 state 导出)、esbuild 多文件构建、避开十类常见崩点。tier2 单写 ReAct agent 写 Phaser 工程时按名取它。
|
||
---
|
||
|
||
# 在 Phaser 3 上写经营富游戏
|
||
|
||
这份 skill 教你把一款多系统经营富游戏(合成 / 订单 / 资源相互咬合)写成一个 **Phaser 3 多文件 src/ 源工程**,并让它对接 tier2 第二装载分支宿主、过验收门。它假设你已经读了 `code-writer-system` 的工作纪律(九工具、ReAct 循环、五条铁律);本 skill 补的是「具体到 Phaser 怎么写」那一层。
|
||
|
||
拿不准某个 Phaser API 的确切签名时,查本 skill 的 `rag/` 语料(精选的 Phaser 关键 API 与范式片段),别凭记忆编不存在的 API——Phaser 的 API 面很大,编出来的 API 跑不起来会浪费一整轮真玩。
|
||
|
||
## 一、工程骨架长什么样
|
||
|
||
tier2 的 Phaser 工程是多文件 `src/`,入口恒为 `src/main.js`。`scaffold_init` 已经给你铺好先天过 boot 的骨架(scene 树脚手架 + 三系统骨架 + 留空数据表)。典型文件树:
|
||
|
||
```
|
||
src/
|
||
main.js # 入口:esbuild 从这里起手。默认导出工厂 (opts?) => PhaserGameInstance
|
||
scenes/
|
||
BootScene.js # 载资产、建系统、置全局上下文,然后 start PlayScene
|
||
PlayScene.js # 主玩法画面 + 输入(棋盘、点击命中、合成动画)
|
||
UIScene.js # 叠层 HUD / 订单面板(与 PlayScene 并行运行,不是切换)
|
||
systems/
|
||
resource-system.js # 纯逻辑:货币 + 库存,命令式 API(addCoins/spendCoins/consume/add)
|
||
merge-system.js # 纯逻辑:3×3 棋盘合成判定,真调 resource 命令
|
||
order-system.js # 纯逻辑:订单 patience 倒计时,完单真调 resource.addCoins
|
||
data/
|
||
tables.js # 数据表:mergeChains / orders / 物品 / 平衡数值
|
||
GameInstance.js # 实现宿主契约 boot/readState/injectInput?/destroy 的工厂主体
|
||
```
|
||
|
||
`role` 字段(fileTree):main.js=entry,scenes/*=scene,systems/*=system,data/*=config,GameInstance.js=lib。
|
||
|
||
## 二、最该守住的一条:纯逻辑系统与渲染彻底分离
|
||
|
||
这是富游戏写对的命门,也是三联动门、经济门、latch 门能不能过的根。
|
||
|
||
**systems/ 下的文件是纯逻辑**:零 DOM、零 canvas、零 Phaser import、零裸随机。它们维护游戏状态、暴露命令式 API、返回 `{ok, reason}`,全程不知道有没有屏幕。这样三件事才成立:
|
||
|
||
1. **真接线可被机器验证**:合成消耗真调 `resource.consumeIngredient(itemId, n)`、完单真调 `resource.addCoins(n)`——系统内部记调用计数,三联动门据计数证「真调了资源系统」,而非自绘伪装。如果你在 scene 里直接 `this.coins += 10`,三联动门会判你没真接线。
|
||
2. **确定性可重放**:随机经注入的受控源(`{ next: () => number }`),不裸 `Math.random()`。harness 用固定 seed 驱动,每次跑出一样的结果。
|
||
3. **逻辑可静态检查**:数据表可达性(每个订单要的物品都能被合成链产出)、合成 DAG 无环,都能脱离渲染静态算出来。
|
||
|
||
**scenes/ 下的文件是渲染 + 输入**:画棋盘格、画 HUD、把点击坐标映射到棋盘格、播合成动画。scene 调系统的命令式 API(`mergeSystem.mergeAt(i, j)`),拿返回值决定怎么播反馈,**绝不在 scene 里重写逻辑或直接改系统内部状态**。
|
||
|
||
参照 `tier2/fixtures/mini-fei-e/src/systems/` 三个已预建的纯逻辑系统——它们就是这条分离的样板(命令式 API + `{ok,reason}` + 调用计数 + 受控随机)。你写表现层时调它们,不重写它们。
|
||
|
||
## 三、scene 树怎么搭
|
||
|
||
```js
|
||
// src/main.js —— 工厂:返回实现宿主契约的实例。esbuild 入口。
|
||
import Phaser from 'phaser';
|
||
import { createGameInstance } from './GameInstance.js';
|
||
|
||
// 默认导出工厂;esbuild 打包后挂 buildProfile.globalName,宿主取它建实例。
|
||
export default function gameFactory(opts) {
|
||
return createGameInstance(opts);
|
||
}
|
||
```
|
||
|
||
三个要点:
|
||
|
||
- **BootScene 建系统、置全局上下文**:在 BootScene 里 `new` 出三个纯逻辑系统,按 `game.registry`(Phaser 的跨 scene 数据仓)或挂到一个共享上下文对象上,让 PlayScene / UIScene 都拿得到同一组系统实例。系统是 single source of truth,多个 scene 共享它。
|
||
- **UIScene 叠层并行,不是切换**:`this.scene.launch('UIScene')` 让 UIScene 与 PlayScene 同时运行(HUD 浮在玩法画面上);`this.scene.start('Xxx')` 是切换(停当前、起新的)。订单面板 / 金币 HUD 用 launch 叠层。
|
||
- **资产在 preload 载、create 里建对象**:`preload()` 里 `this.load.image(key, url)`,`create()` 里 `this.add.image/sprite/rectangle`。资产从 boot 注入的 `assets` 来(按 ref 键),缺失则降级用 `this.add.rectangle` 画色块占位——别因为缺图就崩。
|
||
|
||
## 四、输入命中:把点击坐标映射到棋盘格
|
||
|
||
经营富游戏最常见的输入是「点棋盘格」「点订单」。Phaser 的命中有两条路,挑一条用:
|
||
|
||
- **交互对象路(推荐)**:给每个棋盘格建一个 `this.add.rectangle(x, y, w, h).setInteractive()`,监听 `'pointerdown'`,回调里已知这是哪个格(闭包捕获 i,j)。命中由 Phaser 算,不用自己做坐标反算。
|
||
- **全局指针路**:监听 `this.input.on('pointerdown', p => ...)`,用 `p.x, p.y` 自己反算落在哪个格(`col = Math.floor((p.x - boardX) / cellW)`)。灵活但易错,注意 Phaser 指针坐标已是逻辑像素(scale 处理过),别再除像素比。
|
||
|
||
命中到格后,调系统命令(`mergeSystem.selectCell(i,j)` 或 `mergeSystem.mergeAt(...)`),据返回值播反馈。**点击只触发逻辑命令,不直接改状态。**
|
||
|
||
逻辑像素 390×844 竖屏:Phaser scale 用 `{ mode: Phaser.Scale.FIT, width: 390, height: 844 }`,这样你按 390×844 布局,Phaser 自动缩放适配真实屏幕,指针坐标也归一到这个逻辑空间。
|
||
|
||
## 五、与宿主契约对接(A4 第二装载分支)
|
||
|
||
宿主契约在 `tier2/contracts/boot-phaser-host.d.ts`。你的 `GameInstance.js` 要返回一个实现这四个方法的对象:
|
||
|
||
```js
|
||
// src/GameInstance.js —— 实现 PhaserGameInstance 契约。
|
||
export function createGameInstance(opts) {
|
||
let phaserGame = null;
|
||
let systems = null; // { resource, merge, order } —— 纯逻辑系统,readState 的真相源
|
||
|
||
return {
|
||
// boot(ctx):一次性启动。ctx = { game?, canvas, seed, assets? }。
|
||
// 注意:tier2 宿主可能注入它已建好的 Phaser.Game(ctx.game),也可能让你自建——
|
||
// 按宿主实际约定来(看 boot-phaser-host.js)。用 ctx.seed 派生所有随机,禁裸 Math.random。
|
||
async boot(ctx) {
|
||
systems = buildSystems(ctx.seed); // 建三个纯逻辑系统(注入受控随机)
|
||
phaserGame = new Phaser.Game({ // 或用 ctx.game(按宿主约定)
|
||
type: Phaser.AUTO,
|
||
scale: { mode: Phaser.Scale.FIT, width: 390, height: 844 },
|
||
scene: [BootScene, PlayScene, UIScene],
|
||
parent: ctx.canvas?.parentElement,
|
||
callbacks: { postBoot: g => g.registry.set('systems', systems) }, // 共享系统给各 scene
|
||
});
|
||
},
|
||
|
||
// readState():返回可观测语义 state。phase 是跨品类终态不变量;其余 per-品类。
|
||
// 只读、只供测试——渲染层与玩家界面绝不可显示它、不得据它给提示。
|
||
readState() {
|
||
return {
|
||
phase: systems.order.phase, // 'title'|'play'|'win'|'lose'|'gameover'(latch 终态)
|
||
coins: systems.resource.state.coins, // per-品类语义字段
|
||
ingredients: { ...systems.resource.state.ingredients },
|
||
orders: systems.order.snapshot(),
|
||
};
|
||
},
|
||
|
||
// injectInput(ev)(可选):harness 驱动入口。若 scene 已订阅 Phaser input,可省。
|
||
injectInput(ev) { /* 把 tap/key/drag 转成对系统命令的调用,或经 Phaser input 投递 */ },
|
||
|
||
// destroy():卸载卫生,幂等。必须 game.destroy(true) 释放 Phaser 资源(多游戏复位/session 切换)。
|
||
destroy() { if (phaserGame) { phaserGame.destroy(true); phaserGame = null; } },
|
||
};
|
||
}
|
||
```
|
||
|
||
## 六、latch 终态(游戏无 emit 通道,硬约束)
|
||
|
||
游戏没有 emit 通道向宿主报「我赢了」。终态走 latch:
|
||
|
||
- 跑到赢(金币达阈值)或输(连续 3 订单流失)时,把 `readState().phase` **焊成**终态值(`'win'`/`'lose'`),并**驻留不回弹**——后续帧 readState 读到的 phase 恒定,绝不改回 `'play'`。
|
||
- 宿主每帧轮询 `readState().phase` 判终态落定。latch 门会多帧轮询验「终态不回弹」。
|
||
- 实现上:在 order-system 里维护一个 `_phase`,落定终态后加锁(`if (this._phase === 'win' || this._phase === 'lose') return;` 在更新开头早返回),后续任何输入都不改它。
|
||
|
||
## 七、esbuild 多文件构建
|
||
|
||
`build` 工具用 esbuild 把 `src/main.js` 起手的多文件工程打成一个 iife bundle(入口 main.js,挂 `buildProfile.globalName`,默认沿用 `__GameBundle`)。你只管写对 import 关系:
|
||
|
||
- 用 ES module import(`import Phaser from 'phaser'`、`import { createMergeSystem } from '../systems/merge-system.js'`)。相对路径要对、带 `.js` 后缀(esbuild 默认不补)。
|
||
- import 图必须无环(A → B → A 会让 headless_check 挂)。系统之间用依赖注入(merge 收 resource 作为构造参数),别互相 import 成环。
|
||
- phaser 是外部依赖(depLock.phaser 锁版本,如 `3.80.1`),由 buildProfile 决定打进 bundle 还是外置。别 import 不在 depLock 里的包。
|
||
|
||
构建失败时 `build` 回你完整 esbuild 报错——按报错的文件:行定位,改 import 路径 / 语法,别整文件重写。
|
||
|
||
## 八、十类常见崩点(踩过的坑,绕开)
|
||
|
||
1. **在 scene 里写逻辑 / 直接改系统状态** → 三联动门挂。逻辑只在 systems/,scene 只调命令。
|
||
2. **裸 `Math.random()`** → 确定性重放失败、harness 驱动不可复现。随机从注入 seed 派生。
|
||
3. **import 路径漏 `.js` 后缀或写错相对层级** → esbuild 报 "Could not resolve"。
|
||
4. **import 成环**(系统互相 import)→ headless_check 挂。用依赖注入破环。
|
||
5. **终态 phase 回弹**(落定 win 后又被更新改回 play)→ latch 门挂。终态加锁早返回。
|
||
6. **`scene.start` 当 `scene.launch` 用**(HUD 切换掉了玩法画面)→ 画面只剩 HUD。叠层用 launch。
|
||
7. **系统实例没在 scene 间共享**(各 scene 各 new 一份)→ HUD 显示的金币和玩法里的对不上。系统建一次,经 registry / 共享上下文给所有 scene。
|
||
8. **readState 暴露给玩家界面 / 据它给提示** → 违反「只供测试」铁律。语义 state 渲染层不显示。
|
||
9. **缺资产就崩**(preload 的图加载失败未兜底)→ A_boot 挂。缺图降级用 `add.rectangle` 色块占位。
|
||
10. **destroy 不幂等 / 不 `game.destroy(true)`** → 多游戏切换内存泄漏、canvas 残留。destroy 必须幂等且真释放 Phaser.Game。
|
||
|
||
## 九、靶子参照
|
||
|
||
mini-肥鹅 是 0号 spike 的对照靶子(`tier2/contracts/mini-fei-e-fixture-spec.md`):三系统(资源 / 合成 / 订单)+ 五耦合点 + 12 物品 / 6 合成链 / 5 订单,赢=金币 20→100,输=连续 3 流失。`tier2/fixtures/mini-fei-e/src/systems/` 是它三个纯逻辑系统的预建样板——你写 Phaser 表现层时调它们,照它的命令式 API + `{ok,reason}` + 调用计数范式,把这套分离推广到任意经营题面。
|