diff --git a/docs/superpowers/plans/2026-06-28-platform-store.md b/docs/superpowers/plans/2026-06-28-platform-store.md new file mode 100644 index 0000000..6337777 --- /dev/null +++ b/docs/superpowers/plans/2026-06-28-platform-store.md @@ -0,0 +1,689 @@ +# platform 响应式 Store(第一切片)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:** 实现 `framework/platform` 第一切片——自研响应式 `signal` 基元 + 三个 Store(PlayerStore/AppStore/RoomStore,登录驱动子集)+ `PlatformSession`(订阅 `NetClient` 事件 → 填充 Store)+ 只读视图,纯逻辑、Node 全测,接上真机联调通过的真实 login 响应。 + +**Architecture:** 取向 A——每个 Store 内部一个 `Reactive` 状态 signal,不可变 patch 更新,对外暴露 `state: ReadonlyReactive` + 领域更新方法。`PlatformSession` 只依赖 `NetClient` 的事件总线(不碰 net 内部),复用 `protocol/login` 的 `parseLoginResponse`。State 字段名逐字对齐 `docs/protocol/04`(直接由服务器 login 响应填充)。 + +**Tech Stack:** TypeScript(Cocos 3.8.8);测试 Node 20 + `tsx` 跑 `node:test`,沿用 `npm run test:framework` / `npm run typecheck:framework`(cwd `cocoscreator_projects/`)。 + +> 依据 spec `docs/superpowers/specs/2026-06-28-platform-store-design.md`。当前分支 `feat/platform-store`。 +> 导入须带 `.ts` 后缀(tsconfig `allowImportingTsExtensions`)。`git commit` 的 CRLF 警告可忽略。npm 命令 cwd `G:/Works/YouleGamesCocosCreator/cocoscreator_projects`;git 命令 cwd `G:/Works/YouleGamesCocosCreator`。 + +--- + +## 文件结构(本计划建立) + +``` +cocoscreator_projects/ +├─ framework-tests/platform/ +│ ├─ reactive.test.ts +│ ├─ player-store.test.ts +│ ├─ app-store.test.ts +│ ├─ room-store.test.ts +│ └─ session.test.ts +└─ YouleNexus/assets/framework/ + ├─ core/reactive.ts # signal 基元 + └─ platform/ + ├─ stores/types.ts # PlayerState/AppState/RoomState/ConnectionPhase + ├─ readonly.ts # 只读视图接口 + ├─ stores/player-store.ts + ├─ stores/app-store.ts + ├─ stores/room-store.ts + └─ session.ts # PlatformSession +``` + +--- + +### Task 1: core/reactive.ts — 响应式 signal 基元 + +**Files:** +- Create: `cocoscreator_projects/YouleNexus/assets/framework/core/reactive.ts` +- Create: `cocoscreator_projects/framework-tests/platform/reactive.test.ts` + +- [ ] **Step 1: 写失败测试** + +创建 `framework-tests/platform/reactive.test.ts`: + +```ts +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { signal } from '../../YouleNexus/assets/framework/core/reactive.ts'; + +test('signal: value 读写', () => { + const s = signal(1); + assert.equal(s.value, 1); + s.value = 2; + assert.equal(s.value, 2); +}); + +test('signal: subscribe 收到 (value, prev)', () => { + const s = signal(1); + let got: [number, number] | null = null; + s.subscribe((v, p) => { got = [v, p]; }); + s.value = 5; + assert.deepEqual(got, [5, 1]); +}); + +test('signal: 退订后不再收到', () => { + const s = signal(0); + let count = 0; + const un = s.subscribe(() => { count++; }); + s.value = 1; + un(); + s.value = 2; + assert.equal(count, 1); +}); + +test('signal: Object.is 判等,值未变不通知', () => { + const s = signal(7); + let count = 0; + s.subscribe(() => { count++; }); + s.value = 7; + assert.equal(count, 0); +}); + +test('signal: 一个订阅者抛错不影响其它', () => { + const s = signal(0); + let reached = false; + s.subscribe(() => { throw new Error('boom'); }); + s.subscribe(() => { reached = true; }); + s.value = 1; + assert.equal(reached, true); +}); +``` + +- [ ] **Step 2: 跑测试确认失败** — `npm run test:framework` → FAIL(找不到 reactive.ts) + +- [ ] **Step 3: 实现 reactive.ts** + +创建 `core/reactive.ts`: + +```ts +export type Unsubscribe = () => void; + +export interface ReadonlyReactive { + readonly value: T; + subscribe(fn: (value: T, prev: T) => void): Unsubscribe; // 返回退订函数 +} + +export interface Reactive extends ReadonlyReactive { + value: T; // 可写 +} + +/** 极小响应式信号:value 读写 + 订阅。Object.is 判等(值未变不通知);订阅者抛错隔离。 */ +export function signal(initial: T): Reactive { + let current = initial; + const subs = new Set<(value: T, prev: T) => void>(); + return { + get value(): T { return current; }, + set value(v: T) { + if (Object.is(v, current)) return; + const prev = current; + current = v; + for (const fn of [...subs]) { + try { fn(current, prev); } catch (e) { console.error('[reactive] subscriber error:', e); } + } + }, + subscribe(fn: (value: T, prev: T) => void): Unsubscribe { + subs.add(fn); + return () => { subs.delete(fn); }; + }, + }; +} +``` + +- [ ] **Step 4: 跑测试 + 类型检查** — `npm run test:framework`(含 reactive 5 用例 PASS)+ `npm run typecheck:framework`(exit 0) + +- [ ] **Step 5: Commit** + +```bash +cd G:/Works/YouleGamesCocosCreator +git add cocoscreator_projects/YouleNexus/assets/framework/core/reactive.ts cocoscreator_projects/framework-tests/platform/reactive.test.ts +git commit -m "feat(framework): core 响应式 signal 基元(自研薄封装) + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +### Task 2: platform/stores/types.ts + platform/readonly.ts — State 类型与只读视图 + +**Files:** +- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/types.ts` +- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/readonly.ts` + +> 类型层无运行时行为,本任务以 `typecheck:framework` 通过为验收。 + +- [ ] **Step 1: 实现 stores/types.ts** + +创建 `platform/stores/types.ts`(字段名对齐 protocol/04;`agentid/channelid` 归 AppState,见 spec §3 注): + +```ts +import type { ChannelIdentity } from '../../config/identity.ts'; + +/** PlayerState(C_Player A 组,login 始终下发;openid 等仅 deviceLogin 下发,做可选)。 */ +export interface PlayerState { + playerid: number; + nickname: string; avatar: string; sex: number; + openid?: string; unionid?: string | number; province?: string; city?: string; + bean: number; roomcard: number; score: number; charm: number; + taskstate: number; advanced: number; + bankpower: number; bank: number; bankpwd: number; + ip: string; sign: string | null; tel: string | null; + invitecode: string | null; initCard: number | string; initBean: number | string; +} + +export type ConnectionPhase = + | 'idle' | 'connecting' | 'connected' | 'loggedIn' + | 'reconnecting' | 'slow' | 'kicked' | 'loginFailed'; + +/** AppState(GameData 子集:身份 + 服务器 + 代理/版本 + 连接相位)。 */ +export interface AppState { + identity: ChannelIdentity | null; // 来自 bootstrap + servers: string[]; + agentid: string | number; // login 回填(旧 GameData.AgentId,protocol/04:183) + channelid: string | number; // login 回填(旧 GameData.ChannelId,protocol/04:184) + agentname: string; + agentmode: number; + gameversion: number; + phase: ConnectionPhase; +} + +/** RoomState(Desk B 组,仅在房/重连时下发)。 */ +export interface RoomState { + inRoom: boolean; + roomcode: string | null; + isbattle: number; // 0未开局/1已开局 + roommode: number; // 0普通/1星星场 + seat: number; // 自己的座位(-1 未入座) + isowner: number; // 1房主/0非 + asetcount: number; makewar: number; needprepare: number; infinite: number; + players: unknown[]; // 按座位下标(可含 null);完整建模留后续切片 + deskinfo: unknown; // 子游戏对局快照,opaque 透传 +} +``` + +- [ ] **Step 2: 实现 readonly.ts** + +创建 `platform/readonly.ts`: + +```ts +import type { ReadonlyReactive } from '../core/reactive.ts'; +import type { PlayerState, AppState, RoomState } from './stores/types.ts'; + +export interface ReadonlyPlayerStore { readonly state: ReadonlyReactive; } +export interface ReadonlyRoomStore { readonly state: ReadonlyReactive; } +export interface ReadonlyAppStore { readonly state: ReadonlyReactive; } +``` + +- [ ] **Step 3: 类型检查** — `npm run typecheck:framework`(exit 0) + +- [ ] **Step 4: Commit** + +```bash +cd G:/Works/YouleGamesCocosCreator +git add cocoscreator_projects/YouleNexus/assets/framework/platform/stores/types.ts cocoscreator_projects/YouleNexus/assets/framework/platform/readonly.ts +git commit -m "feat(framework): platform State 类型(登录驱动子集) + 只读视图接口 + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +### Task 3: platform/stores/player-store.ts — PlayerStore + +**Files:** +- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/player-store.ts` +- Create: `cocoscreator_projects/framework-tests/platform/player-store.test.ts` + +- [ ] **Step 1: 写失败测试** + +创建 `framework-tests/platform/player-store.test.ts`: + +```ts +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { PlayerStore } from '../../YouleNexus/assets/framework/platform/stores/player-store.ts'; + +const RAW = { state: 0, playerid: 430511, nickname: '测试号', avatar: 'http://a', sex: 0, + bean: 0, roomcard: 3, score: 0, taskstate: 1, advanced: 0, bankpower: 1, bank: 0, bankpwd: 0, + ip: '127.0.0.1', sign: null, tel: null, invitecode: null, initCard: '3', initBean: '0', + openid: 'o1', unionid: 'u1', province: 'p', city: 'c' }; + +test('applyLogin 填充 PlayerState 并通知订阅者', () => { + const store = new PlayerStore(); + let notified = 0; + store.state.subscribe(() => { notified++; }); + store.applyLogin(RAW as any); + const v = store.state.value; + assert.equal(v.playerid, 430511); + assert.equal(v.nickname, '测试号'); + assert.equal(v.roomcard, 3); + assert.equal(v.taskstate, 1); + assert.equal(v.openid, 'o1'); + assert.equal(v.initCard, '3'); + assert.equal(notified, 1); +}); + +test('charm 等缺省字段补默认 0', () => { + const store = new PlayerStore(); + store.applyLogin(RAW as any); + assert.equal(store.state.value.charm, 0); +}); + +test('未下发的可选字段(openid 等)缺省时不写入', () => { + const store = new PlayerStore(); + store.applyLogin({ state: 0, playerid: 1 } as any); + assert.equal(store.state.value.openid, undefined); + assert.equal(store.state.value.playerid, 1); +}); +``` + +- [ ] **Step 2: 跑测试确认失败** — `npm run test:framework` → FAIL(找不到 player-store.ts) + +- [ ] **Step 3: 实现 player-store.ts** + +创建 `platform/stores/player-store.ts`: + +```ts +import { signal, type ReadonlyReactive } from '../../core/reactive.ts'; +import type { PlayerState } from './types.ts'; +import type { ReadonlyPlayerStore } from '../readonly.ts'; +import type { LoginResponseData } from '../../core/types/login.ts'; + +const INITIAL: 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, +}; + +/** 玩家自身状态(= 旧 C_Player)。 */ +export class PlayerStore implements ReadonlyPlayerStore { + private s = signal({ ...INITIAL }); + get state(): ReadonlyReactive { return this.s; } + + /** 从 login 响应 A 组填充。字段名对齐 protocol/04(直接取服务器下发值)。 */ + applyLogin(raw: LoginResponseData): void { + const r = raw as any; + this.s.value = { + ...this.s.value, + playerid: r.playerid ?? 0, + nickname: r.nickname ?? '', avatar: r.avatar ?? '', sex: r.sex ?? 0, + bean: r.bean ?? 0, roomcard: r.roomcard ?? 0, score: r.score ?? 0, charm: r.charm ?? 0, + taskstate: r.taskstate ?? 0, advanced: r.advanced ?? 0, + bankpower: r.bankpower ?? 0, bank: r.bank ?? 0, bankpwd: r.bankpwd ?? 0, + ip: r.ip ?? '', sign: r.sign ?? null, tel: r.tel ?? null, + invitecode: r.invitecode ?? null, initCard: r.initCard ?? 0, initBean: r.initBean ?? 0, + ...(r.openid != null ? { openid: String(r.openid) } : {}), + ...(r.unionid != null ? { unionid: r.unionid } : {}), + ...(r.province != null ? { province: String(r.province) } : {}), + ...(r.city != null ? { city: String(r.city) } : {}), + }; + } +} +``` + +- [ ] **Step 4: 跑测试确认通过** — `npm run test:framework`(含 player-store 3 用例)+ `npm run typecheck:framework` + +- [ ] **Step 5: Commit** + +```bash +cd G:/Works/YouleGamesCocosCreator +git add cocoscreator_projects/YouleNexus/assets/framework/platform/stores/player-store.ts cocoscreator_projects/framework-tests/platform/player-store.test.ts +git commit -m "feat(framework): platform PlayerStore(=C_Player) applyLogin + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +### Task 4: platform/stores/app-store.ts — AppStore + +**Files:** +- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/app-store.ts` +- Create: `cocoscreator_projects/framework-tests/platform/app-store.test.ts` + +- [ ] **Step 1: 写失败测试** + +创建 `framework-tests/platform/app-store.test.ts`: + +```ts +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { AppStore } from '../../YouleNexus/assets/framework/platform/stores/app-store.ts'; + +test('初始 phase 为 idle', () => { + assert.equal(new AppStore().state.value.phase, 'idle'); +}); + +test('setPhase/setServers/setIdentity 更新并通知', () => { + const store = new AppStore(); + let n = 0; + store.state.subscribe(() => { n++; }); + store.setPhase('connected'); + store.setServers(['ws://a']); + store.setIdentity({ agentid: 'A', channelid: 'C', gameid: 'G', marketid: 4, version: 10000 }); + const v = store.state.value; + assert.equal(v.phase, 'connected'); + assert.deepEqual(v.servers, ['ws://a']); + assert.equal(v.identity?.agentid, 'A'); + assert.equal(n, 3); +}); + +test('applyLogin 回填 agentid/channelid/agentname/agentmode/gameversion', () => { + const store = new AppStore(); + store.applyLogin({ state: 0, playerid: 1, agentid: 'AG', channelid: 'CH', agentname: '进贤', agentmode: 2, gameversion: 41 } as any); + const v = store.state.value; + assert.equal(v.agentid, 'AG'); + assert.equal(v.channelid, 'CH'); + assert.equal(v.agentname, '进贤'); + assert.equal(v.agentmode, 2); + assert.equal(v.gameversion, 41); +}); +``` + +- [ ] **Step 2: 跑测试确认失败** — `npm run test:framework` → FAIL(找不到 app-store.ts) + +- [ ] **Step 3: 实现 app-store.ts** + +创建 `platform/stores/app-store.ts`: + +```ts +import { signal, type ReadonlyReactive } from '../../core/reactive.ts'; +import type { AppState, ConnectionPhase } from './types.ts'; +import type { ReadonlyAppStore } from '../readonly.ts'; +import type { ChannelIdentity } from '../../config/identity.ts'; +import type { LoginResponseData } from '../../core/types/login.ts'; + +const INITIAL: AppState = { + identity: null, servers: [], + agentid: '', channelid: '', + agentname: '', agentmode: 0, gameversion: 0, + phase: 'idle', +}; + +/** 应用级状态(= 旧 GameData 子集:身份/服务器/连接相位)。 */ +export class AppStore implements ReadonlyAppStore { + private s = signal({ ...INITIAL }); + get state(): ReadonlyReactive { return this.s; } + + setIdentity(identity: ChannelIdentity): void { this.s.value = { ...this.s.value, identity }; } + setServers(servers: string[]): void { this.s.value = { ...this.s.value, servers }; } + setPhase(phase: ConnectionPhase): void { this.s.value = { ...this.s.value, phase }; } + + applyLogin(raw: LoginResponseData): void { + const r = raw as any; + this.s.value = { + ...this.s.value, + agentid: r.agentid ?? this.s.value.agentid, + channelid: r.channelid ?? this.s.value.channelid, + agentname: r.agentname ?? '', + agentmode: r.agentmode ?? 0, + gameversion: r.gameversion ?? 0, + }; + } +} +``` + +- [ ] **Step 4: 跑测试确认通过** — `npm run test:framework`(含 app-store 3 用例)+ `npm run typecheck:framework` + +- [ ] **Step 5: Commit** + +```bash +cd G:/Works/YouleGamesCocosCreator +git add cocoscreator_projects/YouleNexus/assets/framework/platform/stores/app-store.ts cocoscreator_projects/framework-tests/platform/app-store.test.ts +git commit -m "feat(framework): platform AppStore(=GameData) 身份/服务器/相位 + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +### Task 5: platform/stores/room-store.ts — RoomStore + +**Files:** +- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/room-store.ts` +- Create: `cocoscreator_projects/framework-tests/platform/room-store.test.ts` + +- [ ] **Step 1: 写失败测试** + +创建 `framework-tests/platform/room-store.test.ts`: + +```ts +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { RoomStore } from '../../YouleNexus/assets/framework/platform/stores/room-store.ts'; + +test('初始 inRoom 为 false、seat 为 -1', () => { + const v = new RoomStore().state.value; + assert.equal(v.inRoom, false); + assert.equal(v.seat, -1); +}); + +test('applyRecovery 填充 B 组并置 inRoom=true', () => { + const store = new RoomStore(); + store.applyRecovery({ state: 0, playerid: 1, roomcode: 'ROOM1', isbattle: 1, roommode: 1, + seat: 2, isowner: 1, players: [null, {}, {}], deskinfo: { round: 3 } } as any); + const v = store.state.value; + assert.equal(v.inRoom, true); + assert.equal(v.roomcode, 'ROOM1'); + assert.equal(v.isbattle, 1); + assert.equal(v.seat, 2); + assert.equal(v.players.length, 3); + assert.deepEqual(v.deskinfo, { round: 3 }); +}); + +test('clear 复位 inRoom 与字段', () => { + const store = new RoomStore(); + store.applyRecovery({ state: 0, playerid: 1, roomcode: 'R', seat: 1 } as any); + store.clear(); + const v = store.state.value; + assert.equal(v.inRoom, false); + assert.equal(v.roomcode, null); + assert.equal(v.seat, -1); +}); +``` + +- [ ] **Step 2: 跑测试确认失败** — `npm run test:framework` → FAIL(找不到 room-store.ts) + +- [ ] **Step 3: 实现 room-store.ts** + +创建 `platform/stores/room-store.ts`: + +```ts +import { signal, type ReadonlyReactive } from '../../core/reactive.ts'; +import type { RoomState } from './types.ts'; +import type { ReadonlyRoomStore } from '../readonly.ts'; +import type { LoginResponseData } from '../../core/types/login.ts'; + +const INITIAL: RoomState = { + inRoom: false, roomcode: null, isbattle: 0, roommode: 0, seat: -1, isowner: 0, + asetcount: 0, makewar: 0, needprepare: 0, infinite: 0, players: [], deskinfo: null, +}; + +/** 房间/牌桌状态(= 旧 Desk)。仅 login 含 roomcode(在房/重连)时填充。 */ +export class RoomStore implements ReadonlyRoomStore { + private s = signal({ ...INITIAL }); + get state(): ReadonlyReactive { return this.s; } + + /** login 含 roomcode 时恢复房间(B 组)。 */ + applyRecovery(raw: LoginResponseData): void { + const r = raw as any; + this.s.value = { + ...this.s.value, + inRoom: true, + roomcode: r.roomcode ?? null, + isbattle: r.isbattle ?? 0, + roommode: r.roommode ?? 0, + seat: r.seat ?? -1, + isowner: r.isowner ?? 0, + asetcount: r.asetcount ?? 0, + makewar: r.makewar ?? 0, + needprepare: r.needprepare ?? 0, + infinite: r.infinite ?? 0, + players: Array.isArray(r.players) ? r.players : [], + deskinfo: r.deskinfo ?? null, + }; + } + + /** 离开房间/未在房,复位。 */ + clear(): void { this.s.value = { ...INITIAL }; } +} +``` + +- [ ] **Step 4: 跑测试确认通过** — `npm run test:framework`(含 room-store 3 用例)+ `npm run typecheck:framework` + +- [ ] **Step 5: Commit** + +```bash +cd G:/Works/YouleGamesCocosCreator +git add cocoscreator_projects/YouleNexus/assets/framework/platform/stores/room-store.ts cocoscreator_projects/framework-tests/platform/room-store.test.ts +git commit -m "feat(framework): platform RoomStore(=Desk) applyRecovery/clear + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +### Task 6: platform/session.ts — PlatformSession(登录接入,集成) + +**Files:** +- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/session.ts` +- Create: `cocoscreator_projects/framework-tests/platform/session.test.ts` + +> 集成:注入 `EventBus`,emit `login`(**用 Task 12 联调实测的真实 login 响应**作 fixture)→ 三 Store 正确填充。 + +- [ ] **Step 1: 写失败测试** + +创建 `framework-tests/platform/session.test.ts`: + +```ts +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { EventBus } from '../../YouleNexus/assets/framework/core/events.ts'; +import type { NetClientEvents } from '../../YouleNexus/assets/framework/net/net-client.ts'; +import { PlatformSession } from '../../YouleNexus/assets/framework/platform/session.ts'; + +// Task 12 真机联调实测的 login 响应(本地服) +const REAL_LOGIN = { state: 0, playerid: 430511, agentid: 'veRa0qrBf', channelid: 'FtJf073aa', + nickname: '测试号', avatar: 'http://a', openid: 'o1', sex: 0, unionid: 'test_unionid', + roomcard: 3, bean: 0, score: 0, invitecode: null, advanced: 0, taskstate: 1, ip: '127.0.0.1', + bankpower: 1, bank: 0, sign: null, tel: null, initCard: '3', initBean: '0', bankpwd: 0, + agentname: '进贤', agentmode: 2, gameversion: 41 }; + +test('login(真实响应) → 三 Store 填充 + phase=loggedIn', () => { + const bus = new EventBus(); + const session = new PlatformSession(bus); + bus.emit('login', REAL_LOGIN); + assert.equal(session.player.state.value.playerid, 430511); + assert.equal(session.player.state.value.nickname, '测试号'); + assert.equal(session.player.state.value.roomcard, 3); + assert.equal(session.app.state.value.agentname, '进贤'); + assert.equal(session.app.state.value.gameversion, 41); + assert.equal(session.app.state.value.phase, 'loggedIn'); + assert.equal(session.room.state.value.inRoom, false); // 无 roomcode +}); + +test('login 含 roomcode → room.inRoom=true', () => { + const bus = new EventBus(); + const session = new PlatformSession(bus); + bus.emit('login', { ...REAL_LOGIN, roomcode: 'ROOM1', isbattle: 1, seat: 2 }); + assert.equal(session.room.state.value.inRoom, true); + assert.equal(session.room.state.value.roomcode, 'ROOM1'); +}); + +test('login 失败(state!=0) → phase=loginFailed', () => { + const bus = new EventBus(); + const session = new PlatformSession(bus); + bus.emit('login', { state: 1, playerid: -1 }); + assert.equal(session.app.state.value.phase, 'loginFailed'); +}); + +test('open/reconnecting/slow/kicked 驱动 phase', () => { + const bus = new EventBus(); + const session = new PlatformSession(bus); + bus.emit('open'); + assert.equal(session.app.state.value.phase, 'connected'); + bus.emit('reconnecting', 'ws://x'); + assert.equal(session.app.state.value.phase, 'reconnecting'); + bus.emit('slow'); + assert.equal(session.app.state.value.phase, 'slow'); + bus.emit('kicked', { msg: 'x' }); + assert.equal(session.app.state.value.phase, 'kicked'); +}); +``` + +- [ ] **Step 2: 跑测试确认失败** — `npm run test:framework` → FAIL(找不到 session.ts) + +- [ ] **Step 3: 实现 session.ts** + +创建 `platform/session.ts`: + +```ts +import type { EventBus } from '../core/events.ts'; +import type { NetClientEvents } from '../net/net-client.ts'; +import type { LoginResponseData } from '../core/types/login.ts'; +import { parseLoginResponse } from '../protocol/login.ts'; +import { AppStore } from './stores/app-store.ts'; +import { PlayerStore } from './stores/player-store.ts'; +import { RoomStore } from './stores/room-store.ts'; + +/** 平台会话:订阅 NetClient 事件总线,把 login/连接事件落到三个响应式 Store。 */ +export class PlatformSession { + readonly app = new AppStore(); + readonly player = new PlayerStore(); + readonly room = new RoomStore(); + + constructor(bus: EventBus) { + bus.on('open', () => this.app.setPhase('connected')); + bus.on('login', (d) => this.onLogin(d)); + bus.on('reconnecting', () => this.app.setPhase('reconnecting')); + bus.on('slow', () => this.app.setPhase('slow')); + bus.on('kicked', () => this.app.setPhase('kicked')); + } + + private onLogin(data: unknown): void { + const parsed = parseLoginResponse(data as LoginResponseData); // 复用 protocol/login + if (!parsed.ok) { this.app.setPhase('loginFailed'); return; } + this.player.applyLogin(parsed.raw); + this.app.applyLogin(parsed.raw); + if (parsed.inRoom) this.room.applyRecovery(parsed.raw); + else this.room.clear(); + this.app.setPhase('loggedIn'); + } +} +``` + +- [ ] **Step 4: 跑测试确认通过** — `npm run test:framework`(含 session 4 用例,全部框架测试全绿)+ `npm run typecheck:framework`(exit 0) + +- [ ] **Step 5: Commit** + +```bash +cd G:/Works/YouleGamesCocosCreator +git add cocoscreator_projects/YouleNexus/assets/framework/platform/session.ts cocoscreator_projects/framework-tests/platform/session.test.ts +git commit -m "feat(framework): platform PlatformSession 登录接入(NetClient 事件→三 Store) + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## 验收标准(完成定义) + +1. `npm run test:framework` 全绿;`npm run typecheck:framework` exit 0。 +2. `core/reactive.ts`:signal 基元(读写/订阅/退订/判等/故障隔离)。 +3. 三个 Store(登录驱动子集):`PlayerStore.applyLogin`、`AppStore.setIdentity/setServers/setPhase/applyLogin`、`RoomStore.applyRecovery/clear`,均响应式 + 类型化。 +4. `PlatformSession`:注入 `NetClient.bus`,真实 login 响应 → 三 Store 正确填充、相位流转(connected/loggedIn/reconnecting/slow/kicked/loginFailed),含 roomcode 进房恢复。 +5. 只读视图接口齐备(三 Store `implements`),为后续 `GameContext` 只读暴露铺路。 + +## 后续衔接 + +- **启动接线切片**:把 `resolveBootstrap` 结果接到 `AppStore.setIdentity/setServers`,并 `new PlatformSession(net.bus)` + `net.start()`,跑通启动全流程。 +- **房间事件切片**:`message` 事件路由分发 → 房间 handler(join/leave/ready/dissolve/offline…)更新 RoomStore;`players` 座位玩家完整建模。 +- Plan 4(ui):Cocos 组件 `subscribe` 各 Store → UI 自动刷新。 +- sdk Plan:`GameContext` 把只读 Store + 受限 facade 交给子游戏。