feat: integrate login flow, lobby UI and bitmap font resources

This commit is contained in:
2026-09-06 13:46:57 +08:00
parent e1bcfd91ec
commit 842c68ef55
141 changed files with 11279 additions and 1717 deletions
@@ -267,6 +267,7 @@ class RecordingScene implements ScenePort {
showRoom(): void { this.record('room'); }
showReconnect(): void { this.record('reconnect'); }
showKicked(data: unknown): void { this.record('kicked', data); }
showTips(message:string,time:number):void { this.record('tips',{message,time}); }
showServerDenial(outcome: unknown): void { this.record('denied', outcome); }
showFatal(error: Error): void { this.record('fatal', error); }
}
@@ -717,22 +718,27 @@ for (const count of [0, 2.5, 2]) {
});
}
test('a nonzero login outcome clears its guard, retains state and permits account retry', async () => {
for(const loginRoute of ['agent','room','platform'])test(`${loginRoute} login denial clears its guard, retains state and permits account retry`, async () => {
const { runtime, wire, scene, clock } = makeRuntime();
await runtime.start();
wire.emit({ type: 'open', server: 'ws://agent' });
const before = runtime.state;
runtime.login(ACCOUNT);
const denial = { state: 7, roomcode: 'unavailable', showerror: 1, error: 'Login denied' };
wire.emit({ type: 'message', message: { route: 'agent', rpc: 'player_login', data: denial } });
const denial = { state: -1, msg: '短信验证码不正确', time: 0 };
assert.equal(wire.sent.at(-1)?.route,'agent','outbound login remains agent');
wire.emit({ type: 'message', message: { route: loginRoute, rpc: 'player_login', data: denial } });
assert.equal(runtime.state, before);
assert.equal(wire.stopCalls, 0);
assert.equal(clock.timers.size, 0);
const outcome = scene.calls.find(call => call.name === 'denied')?.value;
assert.deepEqual(outcome, { rpc: 'player_login', state: 7, data: denial });
assert.deepEqual(outcome, { rpc: 'player_login', state: -1, data: denial });
assert.equal((outcome as { data: unknown }).data, denial);
assert.deepEqual(scene.calls.filter(call => call.name === 'fatal'), []);
const sentBeforeReconnect=wire.sent.length;
wire.emit({type:'reconnecting',server:'ws://agent'});
wire.emit({type:'open',server:'ws://agent'});
assert.equal(wire.sent.length,sentBeforeReconnect,'a denied first login must not be replayed');
runtime.login({ ...ACCOUNT, openid: 'retry-account' });
assert.equal((wire.sent[wire.sent.length - 1]!.data as Record<string, unknown>).openid, 'retry-account');
wire.emit({ type: 'message', message: {
@@ -1570,6 +1576,29 @@ test('slow and reconnecting show reconnect without clearing or replacing room st
);
});
test('kick_offline bypasses pending login and stops late navigation', async () => {
const {runtime,wire,scene}=makeRuntime();
await runtime.start();wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT);
const data={fromOther:true,gameid:'another-game'};
wire.emit({type:'message',message:{route:'agent',rpc:'kick_offline',data}});
assert.equal(scene.calls.at(-1)?.name,'kicked');assert.equal(scene.calls.at(-1)?.value,data);
assert.equal(wire.stopCalls,1);
const count=scene.calls.length;wire.emit({type:'open',server:'ws://agent'});
assert.equal(scene.calls.length,count);
});
test('show_message preserves text and legacy numeric timeout without navigating', async () => {
const {runtime,wire,scene}=makeRuntime();
await runtime.start();wire.emit({type:'open',server:'ws://agent'});
const before=runtime.state;
for(const time of ['3000','0','-1']){
wire.emit({type:'message',message:{route:'agent',rpc:'show_message',data:{msg:'服务器提示#第二行',time}}});
assert.deepEqual(scene.calls.at(-1),{name:'tips',value:{message:'服务器提示#第二行',time:Number(time)}});
assert.equal(runtime.state,before);
}
runtime.stop();
});
test('kick closes the game before showing kicked and stopping the wire', async () => {
const log: string[] = [];
const restored: unknown[] = [];