Files
youle_cocos/docs/superpowers/plans/2026-06-28-platform-store.md
T
2026-06-28 21:09:33 +08:00

26 KiB
Raw Blame History

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 后缀(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:

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>,emit login(用 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>"

验收标准(完成定义)

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