Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
26 KiB
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<State> 状态 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后缀(tsconfigallowImportingTsExtensions)。git commit的 CRLF 警告可忽略。npm 命令 cwdG:/Works/YouleGamesCocosCreator/cocoscreator_projects;git 命令 cwdG:/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:
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:
export type Unsubscribe = () => void;
export interface ReadonlyReactive<T> {
readonly value: T;
subscribe(fn: (value: T, prev: T) => void): Unsubscribe; // 返回退订函数
}
export interface Reactive<T> extends ReadonlyReactive<T> {
value: T; // 可写
}
/** 极小响应式信号:value 读写 + 订阅。Object.is 判等(值未变不通知);订阅者抛错隔离。 */
export function signal<T>(initial: T): Reactive<T> {
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
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) <noreply@anthropic.com>"
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 注):
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:
import type { ReadonlyReactive } from '../core/reactive.ts';
import type { PlayerState, AppState, RoomState } from './stores/types.ts';
export interface ReadonlyPlayerStore { readonly state: ReadonlyReactive<PlayerState>; }
export interface ReadonlyRoomStore { readonly state: ReadonlyReactive<RoomState>; }
export interface ReadonlyAppStore { readonly state: ReadonlyReactive<AppState>; }
-
Step 3: 类型检查 —
npm run typecheck:framework(exit 0) -
Step 4: Commit
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) <noreply@anthropic.com>"
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:
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:
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<PlayerState>({ ...INITIAL });
get state(): ReadonlyReactive<PlayerState> { 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
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) <noreply@anthropic.com>"
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:
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:
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<AppState>({ ...INITIAL });
get state(): ReadonlyReactive<AppState> { 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
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) <noreply@anthropic.com>"
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:
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:
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<RoomState>({ ...INITIAL });
get state(): ReadonlyReactive<RoomState> { 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
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) <noreply@anthropic.com>"
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<NetClientEvents>,emitlogin(用 Task 12 联调实测的真实 login 响应作 fixture)→ 三 Store 正确填充。
- Step 1: 写失败测试
创建 framework-tests/platform/session.test.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<NetClientEvents>();
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<NetClientEvents>();
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<NetClientEvents>();
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<NetClientEvents>();
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:
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<NetClientEvents>) {
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
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) <noreply@anthropic.com>"
验收标准(完成定义)
npm run test:framework全绿;npm run typecheck:frameworkexit 0。core/reactive.ts:signal 基元(读写/订阅/退订/判等/故障隔离)。- 三个 Store(登录驱动子集):
PlayerStore.applyLogin、AppStore.setIdentity/setServers/setPhase/applyLogin、RoomStore.applyRecovery/clear,均响应式 + 类型化。 PlatformSession:注入NetClient.bus,真实 login 响应 → 三 Store 正确填充、相位流转(connected/loggedIn/reconnecting/slow/kicked/loginFailed),含 roomcode 进房恢复。- 只读视图接口齐备(三 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 交给子游戏。