docs(plan): Room.scene 实施计划 (4 task: script + scene asset + integration + runner)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,547 @@
|
|||||||
|
# Room.scene Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 在 YouleNexus Cocos 工程新建 `Room.scene`(Canvas + 8 个 PlayerSlot + RoomInfoPanel + ActionButtons)+ `RoomSceneStart.ts` 脚本(可被子游戏调用的 setPlayerCount / setSeatLayout / setPlayerData / clearSeats API)。
|
||||||
|
|
||||||
|
**Architecture:** 单文件 TypeScript 脚本 + Cocos scene asset + node:test 单测 + funplay MCP 集成验证。RoomSceneStart 维护 8 个预制 PlayerInfoView 实例(不运行时 instantiate),setPlayerCount 通过 active 控制可见性。setSeatLayout 按 4 种布局(horizontal/2x2/triangle/square)重计算 _lpos。
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript + ESM, Cocos Creator 3.8.8, funplay-cocos-mcp v0.5.1, node:test + node:assert, `@xmldom/xmldom`.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-09-02-room-scene-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- **服务器零改动**(CLAUDE.md 第一准则):本计划不触碰协议。
|
||||||
|
- **数据源权威唯一、下游不兜底**(CLAUDE.md 第二准则):setPlayerCount/SeatLayout 越界 throw 不静默。
|
||||||
|
- **UTF-8 编码**(Ruling 4 FINAL):所有 .ts/.scene/.test.mjs UTF-8。
|
||||||
|
- **Cocos 3.8.8** + **funplay-cocos-mcp v0.5.1**(已部署):所有 scene/prefab 操作走 `mcp__funplay_cocos__*` 工具。
|
||||||
|
- **node:test + node:assert**; no third-party deps(`@xmldom/xmldom` 已依赖)。
|
||||||
|
- **TypeScript**(`assets/scripts/*.ts` 项目惯例)。
|
||||||
|
- **不在范围**:真实网络、子游戏路由、出牌按钮 UI、2x2/triangle/square 布局具体坐标(留 layout 子任务)。
|
||||||
|
- **PlayerInfoView_Runtime 复用现有 prefab** `assets/framework/ui/prefabs/views/PlayerInfoView.prefab`(不修改)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: RoomSceneStart.ts 完整实现 + 单测
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `cocoscreator_projects/YouleNexus/assets/scripts/RoomSceneStart.ts`
|
||||||
|
- Create: `cocoscreator_projects/YouleNexus/framework-tests/room-scene/room-scene-start.test.mjs`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: 现有 `PlayerInfoView.bindTo(session)` API(`assets/scripts/views/PlayerInfoView.ts`),`PlayerState` type(`assets/framework/platform/stores/types.ts`)
|
||||||
|
- Produces:
|
||||||
|
```typescript
|
||||||
|
export type SeatLayout = 'horizontal' | '2x2' | 'triangle' | 'square';
|
||||||
|
export class RoomSceneStart extends Component {
|
||||||
|
@property([Node]) playerSlots: Node[] = [];
|
||||||
|
private currentCount: number;
|
||||||
|
private currentLayout: SeatLayout;
|
||||||
|
start(): void;
|
||||||
|
public setPlayerCount(n: number): void;
|
||||||
|
public setSeatLayout(layout: SeatLayout): void;
|
||||||
|
public setPlayerData(seat: number, data: PlayerState): void;
|
||||||
|
public clearSeats(): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败测试(先 mock 后 import)**
|
||||||
|
|
||||||
|
`cocoscreator_projects/YouleNexus/framework-tests/room-scene/room-scene-start.test.mjs`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
// Mock Cocos cc module BEFORE importing RoomSceneStart (which imports cc).
|
||||||
|
// Use a tiny in-memory cc shim.
|
||||||
|
import { register } from 'node:module';
|
||||||
|
// Load the .ts file directly via tsx-style import (project convention uses tsx for .ts).
|
||||||
|
// But node:test runs .mjs natively. We can't import .ts directly without tsx.
|
||||||
|
// Strategy: write the .ts file, then in test use dynamic import via Node's loader.
|
||||||
|
// For simplicity in this task, write the test such that RoomSceneStart is loaded
|
||||||
|
// via node --import tsx (project convention). Use:
|
||||||
|
// cd cocoscreator_projects && node --import tsx --test framework-tests/room-scene/room-scene-start.test.mjs
|
||||||
|
// And in the test, use dynamic ESM import:
|
||||||
|
// const { RoomSceneStart } = await import('../../YouleNexus/assets/scripts/RoomSceneStart.ts');
|
||||||
|
|
||||||
|
import { test as t, before } from 'node:test';
|
||||||
|
import assert$0 from 'node:assert/strict';
|
||||||
|
|
||||||
|
// Mock cc module via globalThis before RoomSceneStart is imported.
|
||||||
|
// Cocos cc is imported as `import { _decorator, Component, Node } from 'cc';`.
|
||||||
|
// We use a tiny runtime shim: register a CommonJS module 'cc' via createRequire.
|
||||||
|
// (For brevity, we instead test by directly instantiating RoomSceneStart.prototype
|
||||||
|
// after mocking the cc decorators.)
|
||||||
|
|
||||||
|
// The decorator approach makes pure-mock testing hard. We test the public method
|
||||||
|
// *behaviour* by directly calling the prototype methods on a hand-built fake instance.
|
||||||
|
|
||||||
|
t('setPlayerCount(2) → 前 2 个 slot active=true, 其余 false', () => {
|
||||||
|
const slots = Array.from({length: 8}, () => ({ active: true }));
|
||||||
|
// Manually invoke setPlayerCount on a fake `this`.
|
||||||
|
const fake = { playerSlots: slots, currentCount: 4, currentLayout: 'horizontal' };
|
||||||
|
// Simulate method by direct field assignment (test the LOGIC, not the decorator).
|
||||||
|
// We extract the logic into a pure function in RoomSceneStart (or test the closure).
|
||||||
|
// Simplest: test the function we EXPORT from RoomSceneStart (pure helper).
|
||||||
|
// ... see Step 3 implementation — the actual class also reuses this pure helper.
|
||||||
|
assert.fail('TODO: write after Step 3 produces pure helper'); // placeholder
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important**: The above is a placeholder. The implementer must replace
|
||||||
|
> the test with a working version that imports the actual exports. The
|
||||||
|
> reason this step is shown as placeholder is that testing Cocos decorators
|
||||||
|
> in pure Node requires either `tsx` loader OR extracting the layout
|
||||||
|
> logic into a pure helper. **Step 3 chooses one approach and the test
|
||||||
|
> follows it.**
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测试看失败**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
|
||||||
|
node --import tsx --test framework-tests/room-scene/room-scene-start.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL(test fails or RoomSceneStart not yet implemented)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现 RoomSceneStart.ts(含 pure helpers for testability)**
|
||||||
|
|
||||||
|
`cocoscreator_projects/YouleNexus/assets/scripts/RoomSceneStart.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { _decorator, Component, Node } from 'cc';
|
||||||
|
import type { PlayerState } from '../framework/platform/stores/types.ts';
|
||||||
|
|
||||||
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
export type SeatLayout = 'horizontal' | '2x2' | 'triangle' | 'square';
|
||||||
|
|
||||||
|
// Pure helper exported for testability.
|
||||||
|
export function computeActiveFlags(count: number, total: number): boolean[] {
|
||||||
|
if (count < 1 || count > total) {
|
||||||
|
throw new Error(`count must be 1-${total}, got ${count}`);
|
||||||
|
}
|
||||||
|
const flags: boolean[] = new Array(total);
|
||||||
|
for (let i = 0; i < total; i++) flags[i] = (i < count);
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure helper: positions for given count + layout.
|
||||||
|
export function computePositions(layout: SeatLayout, count: number): Array<{x: number, y: number}> {
|
||||||
|
if (count < 1) throw new Error(`count must be >= 1, got ${count}`);
|
||||||
|
if (layout === 'horizontal') {
|
||||||
|
const spacing = 300;
|
||||||
|
const startX = -(count - 1) * spacing / 2;
|
||||||
|
return Array.from({length: count}, (_, i) => ({ x: startX + i * spacing, y: 0 }));
|
||||||
|
}
|
||||||
|
// 2x2 / triangle / square — fallback to horizontal for now (deferred layout sub-task).
|
||||||
|
const spacing = 300;
|
||||||
|
const startX = -(count - 1) * spacing / 2;
|
||||||
|
return Array.from({length: count}, (_, i) => ({ x: startX + i * spacing, y: 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ccclass('RoomSceneStart')
|
||||||
|
export class RoomSceneStart extends Component {
|
||||||
|
@property([Node])
|
||||||
|
playerSlots: Node[] = [];
|
||||||
|
|
||||||
|
private currentCount: number = 4;
|
||||||
|
private currentLayout: SeatLayout = 'horizontal';
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
if (!this.playerSlots || this.playerSlots.length !== 8) {
|
||||||
|
throw new Error(`[RoomSceneStart] playerSlots must have 8 entries, got ${this.playerSlots?.length}`);
|
||||||
|
}
|
||||||
|
this.setPlayerCount(this.currentCount);
|
||||||
|
this.setSeatLayout(this.currentLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPlayerCount(n: number): void {
|
||||||
|
if (n < 1 || n > this.playerSlots.length) {
|
||||||
|
throw new Error(`[RoomSceneStart] setPlayerCount: n must be 1-${this.playerSlots.length}, got ${n}`);
|
||||||
|
}
|
||||||
|
this.currentCount = n;
|
||||||
|
const flags = computeActiveFlags(n, this.playerSlots.length);
|
||||||
|
for (let i = 0; i < this.playerSlots.length; i++) {
|
||||||
|
const slot = this.playerSlots[i];
|
||||||
|
if (slot) slot.active = flags[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSeatLayout(layout: SeatLayout): void {
|
||||||
|
this.currentLayout = layout;
|
||||||
|
const positions = computePositions(layout, this.currentCount);
|
||||||
|
for (let i = 0; i < this.currentCount && i < positions.length; i++) {
|
||||||
|
const slot = this.playerSlots[i];
|
||||||
|
if (slot) slot.setPosition(positions[i].x, positions[i].y, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPlayerData(seat: number, data: PlayerState): void {
|
||||||
|
if (seat < 1 || seat > this.currentCount) {
|
||||||
|
throw new Error(`[RoomSceneStart] setPlayerData: seat ${seat} out of range 1-${this.currentCount}`);
|
||||||
|
}
|
||||||
|
const slot = this.playerSlots[seat - 1];
|
||||||
|
if (!slot) return;
|
||||||
|
const view = slot.getComponent('PlayerInfoView') as any;
|
||||||
|
if (view && typeof view.bindTo === 'function') {
|
||||||
|
view.bindTo({ player: { state: { value: data } } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearSeats(): void {
|
||||||
|
const placeholder: PlayerState = {
|
||||||
|
playerid: 0, nickname: '占位', avatar: '', sex: 0,
|
||||||
|
bean: 0, roomcard: 0, score: 0, charm: 0, taskstate: 0, advanced: 0,
|
||||||
|
bankpower: 0, bank: 0, bankpwd: 0, ip: '', sign: null, tel: null,
|
||||||
|
invitecode: null, initCard: 0, initBean: 0,
|
||||||
|
} as PlayerState;
|
||||||
|
for (let i = 0; i < this.currentCount; i++) {
|
||||||
|
this.setPlayerData(i + 1, placeholder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 用 pure helpers 写真正的测试**
|
||||||
|
|
||||||
|
Replace placeholder test with:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
// Import the .ts file via tsx loader (handled by `node --import tsx`).
|
||||||
|
const { computeActiveFlags, computePositions } = await import(
|
||||||
|
'../../YouleNexus/assets/scripts/RoomSceneStart.ts'
|
||||||
|
);
|
||||||
|
|
||||||
|
test('computeActiveFlags(2, 8) → 前 2 true, 其余 false', () => {
|
||||||
|
assert.deepEqual(computeActiveFlags(2, 8), [true, true, false, false, false, false, false, false]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computeActiveFlags(4, 8) → 前 4 true, 其余 false', () => {
|
||||||
|
assert.deepEqual(computeActiveFlags(4, 8), [true, true, true, true, false, false, false, false]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computeActiveFlags(0) → throw', () => {
|
||||||
|
assert.throws(() => computeActiveFlags(0, 8), /count must be 1-8/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computeActiveFlags(9) → throw (exceeds 8)', () => {
|
||||||
|
assert.throws(() => computeActiveFlags(9, 8), /count must be 1-8/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computePositions(horizontal, 4) → 4 玩家水平排列居中', () => {
|
||||||
|
const positions = computePositions('horizontal', 4);
|
||||||
|
assert.equal(positions.length, 4);
|
||||||
|
// Should be symmetric around 0
|
||||||
|
assert.equal(positions[0].x + positions[3].x, 0);
|
||||||
|
assert.equal(positions[1].x + positions[2].x, 0);
|
||||||
|
// All y = 0
|
||||||
|
assert.ok(positions.every(p => p.y === 0));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 跑测试看通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
|
||||||
|
node --import tsx --test framework-tests/room-scene/room-scene-start.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 5 test cases PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 6: 跑全套 framework tests 确认无回归**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
|
||||||
|
node scripts/run-framework-tests.mjs 2>&1 | tail -8
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 217/217 + 5/5 new = 222/222 PASS (no regression).
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd G:/Works/YouleGamesCocosCreator
|
||||||
|
git add cocoscreator_projects/YouleNexus/assets/scripts/RoomSceneStart.ts cocoscreator_projects/YouleNexus/framework-tests/room-scene/room-scene-start.test.mjs
|
||||||
|
git commit -m "feat(scripts): RoomSceneStart.ts + tests (setPlayerCount/setSeatLayout/setPlayerData/clearSeats API)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: 创建 Room.scene asset (Cocos 编辑器操作)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `cocoscreator_projects/YouleNexus/assets/scenes/Room.scene`
|
||||||
|
- Create: `cocoscreator_projects/YouleNexus/assets/scenes/Room.scene.meta` (auto-generated by Cocos on first refresh)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: 现有 `PlayerInfoView_Runtime` prefab(UUID `df6a5177-07a1-4bff-8b4b-eb993a81ca2f` per Task 1 inspect),`Canvas` 节点结构(参考 Loading.scene / Login.scene / MainMenu.scene 的 cc.Scene schema)
|
||||||
|
- Produces: Room.scene asset 含完整 hierarchy + cc.SceneAsset header + 8 个 PlayerSlot 引用 PlayerInfoView_Runtime prefab + RoomSceneStart script component
|
||||||
|
|
||||||
|
- [ ] **Step 1: funplay 创建场景(empty scene)**
|
||||||
|
|
||||||
|
通过 funplay MCP 创建 empty scene 骨架:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trigger via MCP — these are tool calls, not bash commands
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `mcp__funplay_cocos__create_scene` with:
|
||||||
|
- `target`: `db://assets/scenes/Room.scene`
|
||||||
|
- `sceneName`: `Room`
|
||||||
|
|
||||||
|
Expected: `Room.scene` + `.meta` created in `assets/scenes/`. Status: imported.
|
||||||
|
|
||||||
|
- [ ] **Step 2: refresh_assets 让 Cocos 识别新 scene**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp__funplay_cocos__refresh_assets
|
||||||
|
path: assets/scenes
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: Room.scene appears in asset-db.
|
||||||
|
|
||||||
|
- [ ] **Step 3: open_asset + 验证空 scene**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp__funplay_cocos__open_asset
|
||||||
|
target: db://assets/scenes/Room.scene
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: open OK. Scene contains Camera + Canvas (Cocos auto-creates).
|
||||||
|
|
||||||
|
- [ ] **Step 4: 添加 RoomInfoPanel + 子 Label(房间号 + 状态)**
|
||||||
|
|
||||||
|
通过 funplay `create_node` / `create_label` 在 Canvas 下添加 RoomInfoPanel 节点 + 2 个 Label(RoomCodeLabel, RoomStateLabel)。具体坐标:
|
||||||
|
- RoomInfoPanel: position (0, 320), size (800, 60), anchorPoint (0.5, 0.5)
|
||||||
|
- RoomCodeLabel: position (0, 320), string "房间号: R0001", fontSize 24
|
||||||
|
- RoomStateLabel: position (0, 280), string "等待中", fontSize 20
|
||||||
|
|
||||||
|
- [ ] **Step 5: 添加 ActionButtons(准备 + 退出)**
|
||||||
|
|
||||||
|
`create_node` + `create_button` 在 Canvas 下添加 ActionButtons 容器 + 2 个 Button(ReadyButton, LeaveButton):
|
||||||
|
- ActionButtons: position (0, -320)
|
||||||
|
- ReadyButton: position (-150, -320), text "准备"
|
||||||
|
- LeaveButton: position (150, -320), text "退出"
|
||||||
|
|
||||||
|
- [ ] **Step 6: 实例化 8 个 PlayerSlot 引用 PlayerInfoView_Runtime prefab**
|
||||||
|
|
||||||
|
Use `mcp__funplay_cocos__instantiate_prefab` × 8 次,每次:
|
||||||
|
- `prefabUuid`: `df6a5177-07a1-4bff-8b4b-eb993a81ca2f` (PlayerInfoView_Runtime UUID)
|
||||||
|
- `parentPath`: `Canvas/PlayerSlots`
|
||||||
|
- `name`: `PlayerSlot_${1..8}`
|
||||||
|
|
||||||
|
Then `set_node_transform` × 8 次按 `computePositions('horizontal', 8)` 设初始位置(间距 300, 居中)。
|
||||||
|
|
||||||
|
- [ ] **Step 7: 挂载 RoomSceneStart 脚本到 Room_Node + 配 playerSlots property**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp__funplay_cocos__add_component
|
||||||
|
path: Room_Node
|
||||||
|
componentName: RoomSceneStart
|
||||||
|
|
||||||
|
mcp__funplay_cocos__set_component_property
|
||||||
|
path: Room_Node
|
||||||
|
componentName: RoomSceneStart
|
||||||
|
propertyPath: playerSlots
|
||||||
|
valueJson: '["Canvas/PlayerSlots/PlayerSlot_1", "..."8 elements]'
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**: Cocos scene 序列化时 `playerSlots` 是 Node[] 引用数组。valueJson 格式需用 Cocos 内部 node reference 格式(可能是 UUID 数组或 `[{ "__id__": N }]`)。Implementer 必须 inspect 一个已有场景看 PlayerInfoView_Runtime 引用如何序列化(例如 SceneStart.ts 的 `playerInfoView: Node` 是如何存的)。
|
||||||
|
|
||||||
|
- [ ] **Step 8: save_current_scene + 验证 Room.scene 加载**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp__funplay_cocos__save_current_scene
|
||||||
|
mcp__funplay_cocos__validate_scene
|
||||||
|
includeLogErrors: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: Room.scene saved + 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd G:/Works/YouleGamesCocosCreator
|
||||||
|
git add cocoscreator_projects/YouleNexus/assets/scenes/Room.scene cocoscreator_projects/YouleNexus/assets/scenes/Room.scene.meta
|
||||||
|
git commit -m "feat(scenes): Room.scene + 8 PlayerSlot + RoomInfoPanel + ActionButtons"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: 集成验证 (Cocos 编辑器)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- (无新文件,验证用)
|
||||||
|
|
||||||
|
- [ ] **Step 1: open_asset Room.scene + validate_prefab_references**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp__funplay_cocos__open_asset
|
||||||
|
target: db://assets/scenes/Room.scene
|
||||||
|
mcp__funplay_cocos__validate_prefab_references
|
||||||
|
target: db://assets/scenes/Room.scene
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: imported + 0 missing UUID.
|
||||||
|
|
||||||
|
- [ ] **Step 2: get_hierarchy 验证 8 个 PlayerSlot + Canvas 子节点**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp__funplay_cocos__get_hierarchy
|
||||||
|
rootPath: Room
|
||||||
|
maxDepth: 3
|
||||||
|
includeComponents: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 看到 Canvas → PlayerSlots → [PlayerSlot_1..8] + RoomInfoPanel + ActionButtons + Room_Node (含 RoomSceneStart 组件)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: execute_javascript 调 setPlayerCount(2) → verify 6 个 slot active=false**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp__funplay_cocos__execute_javascript
|
||||||
|
context: scene
|
||||||
|
code: |
|
||||||
|
const roomNode = cc.find('Canvas/Room_Node');
|
||||||
|
const room = roomNode.getComponent('RoomSceneStart');
|
||||||
|
room.setPlayerCount(2);
|
||||||
|
const slots = cc.find('Canvas/PlayerSlots').children;
|
||||||
|
slots.forEach((s, i) => console.log(`slot ${i} active=${s.active}`));
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: slot 0,1 active=true; slot 2-7 active=false.
|
||||||
|
|
||||||
|
- [ ] **Step 4: capture_scene_screenshot 验证 UI 显示**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp__funplay_cocos__capture_scene_screenshot
|
||||||
|
fileName: room-scene-2-players
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 截图含 2 个 PlayerInfoView + 房间号 Label + 2 个按钮。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 调 setPlayerCount(8) → verify 8 个 slot 可见 + horizontal 布局**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp__funplay_cocos__execute_javascript
|
||||||
|
context: scene
|
||||||
|
code: |
|
||||||
|
const roomNode = cc.find('Canvas/Room_Node');
|
||||||
|
roomNode.getComponent('RoomSceneStart').setPlayerCount(8);
|
||||||
|
console.log('set to 8');
|
||||||
|
mcp__funplay_cocos__capture_scene_screenshot
|
||||||
|
fileName: room-scene-8-players
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 截图含 8 个 PlayerInfoView(水平排列)+ 房间号 + 2 个按钮。
|
||||||
|
|
||||||
|
- [ ] **Step 6: search_project_logs 确认无 error**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp__funplay_cocos__search_project_logs
|
||||||
|
query: error|Error
|
||||||
|
regex: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 仅 Room.scene 相关 import 成功 log, 无 error.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit(如果截图 / log 需要保存作为 evidence)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 仅当 capture_scene_screenshot 生成了实际文件需要 commit
|
||||||
|
ls cocoscreator_projects/YouleNexus/temp/mcp-captures/ 2>&1
|
||||||
|
# 如果有截图 → git add + commit
|
||||||
|
# 否则 skip
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: 整合到 run-framework-tests.mjs + 最终 commit
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `cocoscreator_projects/scripts/run-framework-tests.mjs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 读 run-framework-tests.mjs 看现有 test pattern**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat cocoscreator_projects/scripts/run-framework-tests.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 添加 room-scene 测试到 test list**
|
||||||
|
|
||||||
|
Edit `cocoscreator_projects/scripts/run-framework-tests.mjs`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
const allTests = [
|
||||||
|
// ... existing tests
|
||||||
|
'framework-tests/legacy-layer-migration/poc-login.test.mjs',
|
||||||
|
+ 'framework-tests/room-scene/room-scene-start.test.mjs',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Also adjust the runner to use `--import tsx` for the room-scene test (since it imports .ts):
|
||||||
|
|
||||||
|
```diff
|
||||||
|
execSync(
|
||||||
|
- `node --test ${testFiles.join(' ')}`,
|
||||||
|
+ `node --import tsx --test ${testFiles.join(' ')}`,
|
||||||
|
{ stdio: 'inherit' }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the existing `framework-tests/legacy-layer-migration/*.test.mjs` tests still pass with `--import tsx` (they import `.ts` indirectly via `@xmldom/xmldom` which is pure JS, so should be unaffected).
|
||||||
|
|
||||||
|
- [ ] **Step 3: 跑全套测试**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
|
||||||
|
node scripts/run-framework-tests.mjs 2>&1 | tail -8
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 222/222 PASS (217 prior + 5 new room-scene tests).
|
||||||
|
|
||||||
|
- [ ] **Step 4: 最终 commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd G:/Works/YouleGamesCocosCreator
|
||||||
|
git add cocoscreator_projects/scripts/run-framework-tests.mjs
|
||||||
|
git commit -m "feat(scripts): integrate room-scene tests into run-framework-tests runner"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
### Spec coverage
|
||||||
|
|
||||||
|
| Spec section | Plan task |
|
||||||
|
|---|---|
|
||||||
|
| Goal + 成功标准 | Task 3 (Cocos 编辑器验证) |
|
||||||
|
| 场景结构(Canvas + 子节点) | Task 2 |
|
||||||
|
| RoomSceneStart API (5 methods) | Task 1 |
|
||||||
|
| 输入契约 (PlayerInfoView_Runtime, PlayerState) | Task 1 (uses existing types) |
|
||||||
|
| 错误处理 (throw 越界) | Task 1 (computeActiveFlags throws; setPlayerCount throws) |
|
||||||
|
| 测试策略 (单测 + 集成测试) | Task 1 (单测) + Task 3 (集成) + Task 4 (整合) |
|
||||||
|
| Global Constraints | All tasks (UTF-8 / data-source / no-fallback) |
|
||||||
|
| 不覆盖项 | Excluded (deferred to sub-tasks) |
|
||||||
|
|
||||||
|
### Placeholder scan
|
||||||
|
|
||||||
|
- Step 1 Task 1 has "TODO: write after Step 3 produces pure helper" — this is acknowledged in the step itself and replaced in Step 4.
|
||||||
|
- No other TBD/TODO/placeholder patterns.
|
||||||
|
|
||||||
|
### Type consistency
|
||||||
|
|
||||||
|
- `SeatLayout` type defined in Task 1, used in Task 1 + Task 2 (RoomSceneStart.ts) and consistent.
|
||||||
|
- `RoomSceneStart` class name consistent across all tasks.
|
||||||
|
- `playerSlots: Node[]` property signature consistent between Task 1 (script) and Task 2 (scene config via set_component_property).
|
||||||
|
- `computeActiveFlags(count, total)` / `computePositions(layout, count)` pure helpers — both exported from RoomSceneStart.ts, used in Task 1 tests and Task 2 (implicitly via setPlayerCount / setSeatLayout methods).
|
||||||
|
|
||||||
|
### Final verdict
|
||||||
|
|
||||||
|
Plan covers all spec requirements. No placeholders. Type-consistent. Ready for execution.
|
||||||
Reference in New Issue
Block a user