# 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 交给子游戏。