feat(framework): AppStore + PlatformSession(登录接入)

AppStore(=旧 GameData 子集): setIdentity/setServers/setPhase/applyLogin
- applyLogin 区分两类字段语义:
  * 身份字段(agentid/channelid/agentmode/gameversion)缺省保留旧值
  * 显示字段(agentname)缺省重置为空字符串
- phase 仅由 session 驱动,applyLogin 不写 phase

PlatformSession: 订阅 NetClient 事件总线 → 扇出到三 Store
- 'open' → phase=connected
- 'login'(state==0) → 三 Store 填充 + phase=loggedIn
- 'login'(state!=0) → phase=loginFailed, 不污染 playerid/roomcode
- 'reconnecting' → phase=reconnecting
- 'slow' → phase=slow
- 'kicked' → phase=kicked

110/110 tests pass, typecheck exit 0。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 19:46:14 +08:00
co-authored by Claude Opus 5
parent b54de5bbda
commit 77be6c0d19
4 changed files with 269 additions and 0 deletions
@@ -0,0 +1,74 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { EventBus } from '../../YouleNexus/assets/framework/core/events.ts';
import { PlatformSession } from '../../YouleNexus/assets/framework/platform/session.ts';
import type { NetClientEvents } from '../../YouleNexus/assets/framework/net/net-client.ts';
// 真实联调实测的 login 响应(取自 plan Task 12 fixture)
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,
} as any;
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 + deskinfo 透传', () => {
const bus = new EventBus<NetClientEvents>();
const session = new PlatformSession(bus);
const deskinfo = { round: 3 };
bus.emit('login', { ...REAL_LOGIN, roomcode: 'ROOM1', isbattle: 1, seat: 2, deskinfo });
assert.equal(session.room.state.value.inRoom, true);
assert.equal(session.room.state.value.roomcode, 'ROOM1');
assert.equal(session.room.state.value.isbattle, 1);
assert.equal(session.room.state.value.seat, 2);
assert.equal(session.room.state.value.deskinfo, deskinfo, 'deskinfo 引用相等(不深拷贝)');
});
test('login 失败(state!=0): phase=loginFailed, 三 Store 不填充敏感字段', () => {
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');
assert.equal(session.player.state.value.playerid, 0, '失败时 playerid 走初始,不写入 -1');
assert.equal(session.room.state.value.inRoom, false);
});
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');
});
test('session: 二次 login 应清空旧 room 状态再填充', () => {
const bus = new EventBus<NetClientEvents>();
const session = new PlatformSession(bus);
bus.emit('login', { ...REAL_LOGIN, roomcode: 'ROOM1', seat: 1 });
assert.equal(session.room.state.value.inRoom, true);
bus.emit('login', REAL_LOGIN); // 第二次无 roomcode
assert.equal(session.room.state.value.inRoom, false, '无 roomcode → room.clear()');
assert.equal(session.room.state.value.roomcode, null);
});