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
@@ -0,0 +1,64 @@
import type { RemoteConfig } from '../config/remote-config.ts';
import type { PlatformState } from '../platform/stores/platform-types.ts';
import { selectSelfPlayer } from '../platform/stores/selectors.ts';
// Presentation checks required view inputs without depending on the protocol layer.
function requireNumber(value:unknown,path:string):number {if(typeof value!=='number'||!Number.isFinite(value))throw Error(`${path}: expected number`);return value;}
function requireInteger(value:unknown,path:string):number {const result=requireNumber(value,path);if(!Number.isInteger(result))throw Error(`${path}: expected integer`);return result;}
function requireString(value:unknown,path:string):string {if(typeof value!=='string')throw Error(`${path}: expected string`);return value;}
function requireRecord(value:unknown,path:string):Record<string,unknown> {if(!value||typeof value!=='object'||Array.isArray(value))throw Error(`${path}: expected object`);return value as Record<string,unknown>;}
export function selectLobbyModel(state:PlatformState) {
const self=selectSelfPlayer(state);
if(!self)throw Error('lobby requires authenticated player');
return Object.freeze({...self,
roomcard:requireNumber(self.roomcard,'players.self.roomcard'),
taskstate:requireInteger(self.taskstate,'players.self.taskstate'),
score:requireNumber(self.score,'players.self.score'),
sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~',
canAct:state.app.phase==='logged-in'&&state.room.kind==='outside',
});
}
export type LobbyModel=ReturnType<typeof selectLobbyModel>;
export interface Notice {type:0|1; text:string; image:string; url:string; height:number;}
export interface LobbyPolicy {noticeHours:number; notices:readonly Notice[]; scroll:string; hideTask:boolean; hideShare:boolean;
starName:string; contacts:readonly {label:string;value:string}[];}
/** Source defaults: 04_Data.sysConfig and advertInfo_0/1/2; 12_Logic.get_paravalue. */
export function resolveLobbyPolicy(remote:RemoteConfig):LobbyPolicy {
const interval=remote.getValue('gameNoticeShow');
const noticeHours=interval==null?0:Number(interval);
if(!Number.isFinite(noticeHours)||noticeHours< -1)throw Error('Invalid gameNoticeShow');
const menuValue=remote.getValue('menunotice');
const menu=menuValue==null?{}:requireRecord(menuValue,'menunotice');
const notices:Notice[]=[];
for(let index=0;index<3;index++){
const value=menu[`advert${index}`];
if(value==null){
const message=index===0?remote.getValue('noticemsg'):null;
notices.push({type:0,text:message==null?'':requireString(message,'noticemsg').replace(/#/g,'\n'),image:'',url:'',height:1});
continue;
}
const data=requireRecord(value,`menunotice.advert${index}`);
const type=requireInteger(data.type,`menunotice.advert${index}.type`);
if(type!==0&&type!==1)throw Error(`menunotice.advert${index}.type must be 0 or 1`);
// Empty text and URL explicitly mean no content/action in the source.
const field=(key:string)=>data[key]==null?'':requireString(data[key],`menunotice.advert${index}.${key}`);
notices.push({type,text:type===0?field('msg').replace(/#/g,'\n'):'',image:type===1?field('imgUrl'):'',
url:field(type===0?'url':'imgAdUrl'),height:type===1?requireNumber(data.height,`menunotice.advert${index}.height`):1});
}
const scroll=remote.getValue('scrollmsg');
const star=remote.getValue('starName');
const contacts:Array<{label:string;value:string}>=[];
for(const [key,title] of [['service_wechat','客服微信:'],['service_qq','投诉建议:'],['service_tel','客服手机:']]){
const value=remote.getValue(key);
if(value!=null&&value!=='')contacts.push({label:title,value:requireString(value,key)});
}
return Object.freeze({noticeHours,notices:Object.freeze(notices),scroll:scroll==null?'':requireString(scroll,'scrollmsg'),
starName:star==null?'星星':requireString(star,'starName'),contacts:Object.freeze(contacts),
hideTask:Boolean(remote.getValue('hideTaskBtn')),hideShare:Boolean(remote.getValue('hideWxShare'))});
}
/** Exact JudgeShow expiry: equality is not expired; reconnect never auto-opens. */
export function shouldShowNotice(hours:number,previous:number|null,now:number,reconnect:boolean):boolean {
if(reconnect||hours===-1)return false;
return hours===0||!previous||previous+hours*3600000<now;
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "98247194-1211-4ac7-b8cf-e097983e8a2d",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -32,11 +32,11 @@ export function resolveLoginPolicy(remote:RemoteConfig):LoginPolicy {
showVisitor:flag(hallField('showVisitor'),'showVisitor'),qqLogin:flag(hallField('qqLogin'),'qqLogin'),
hiddenVisitor:LOGIN_DEFAULTS.hiddenVisitor,avatarUrl,avatarRange:Object.freeze([...range])});
}
/** Assignment order is significant: JumpWxAuth applies showVisitor/QQ after cache hiding. */
/** Visitor visibility takes precedence over every other login button. */
export function loginButtons(policy:LoginPolicy, versionState:0|1, cached:boolean) {
const account=versionState===0&&cached;
let wechat=!policy.deviceLogin&&!account;
let visitor=!account;
if(versionState===0){visitor=false;if(policy.showVisitor){wechat=false;visitor=true;}}
return {wechat,visitor,phone:policy.deviceLogin&&!account,quick:false,qq:policy.qqLogin,account};
return {wechat:wechat&&!visitor,visitor,phone:policy.deviceLogin&&!account&&!visitor,quick:false,qq:policy.qqLogin&&!visitor,account};
}