feat: integrate platform room entry, UI migration and erqiwang documentation
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/** Game_Surface_3 Logic.InputRoomCode/pasteRoomCode and GameUI Spirit602. */
|
||||
export class JoinRoomInput {
|
||||
private value='';
|
||||
private busy=false;
|
||||
constructor(private readonly submit:(code:number)=>void){}
|
||||
get code():string{return this.value;}
|
||||
setBusy(value:boolean):void{this.busy=value;}
|
||||
prefill(value:string):void{
|
||||
if(!/^\d{0,6}$/.test(value))throw Error('Invalid saved room shortcode');
|
||||
this.value=value;
|
||||
}
|
||||
digit(value:number):void{
|
||||
if(!Number.isInteger(value)||value<0||value>9)throw Error('Invalid room digit');
|
||||
if(this.busy)return;
|
||||
if(this.value.length<6)this.value+=value;
|
||||
if(this.value.length===6)this.submit(Number(this.value));
|
||||
}
|
||||
remove():void{if(!this.busy)this.value=this.value.slice(0,-1);}
|
||||
clear():void{if(!this.busy)this.value='';}
|
||||
paste(value:string):void{
|
||||
if(this.busy)return;this.value=value.replace(/[^0-9]/g,'').slice(0,6);
|
||||
if(this.value.length===6)this.submit(Number(this.value));
|
||||
}
|
||||
confirm():void{if(!this.busy&&Number(this.value)>0)this.submit(Number(this.value));}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "9301c0f4-8779-44f8-8d19-5f540b499e82",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
+2
File diff suppressed because one or more lines are too long
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "a834856f-7a53-445b-b517-6588ad346e59",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,121 +1,131 @@
|
||||
import type { RemoteConfig } from '../config/remote-config.ts';
|
||||
|
||||
export interface LobbyTemplate {
|
||||
isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean;
|
||||
}
|
||||
/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */
|
||||
export const LOBBY_TEMPLATE:Readonly<LobbyTemplate>=Object.freeze({
|
||||
isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false,
|
||||
});
|
||||
export interface LobbyContext {versionState:0|1;launchMode:0|1;}
|
||||
const HALL_DEFAULTS=Object.freeze({androidPay:0,rankList:0 as number|string,storeUrl:'',bagUrl:'',actData:0 as unknown,worldRoomList:0 as number|string,wareHouse:0 as number|string});
|
||||
const SYS_DEFAULTS=Object.freeze({hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1,
|
||||
ofcShortCode:null as unknown,hideSpid:null as unknown});
|
||||
function object(value:unknown,name:string):Record<string,unknown>{
|
||||
if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`);
|
||||
return value as Record<string,unknown>;
|
||||
}
|
||||
/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */
|
||||
export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly<LobbyTemplate>=LOBBY_TEMPLATE){
|
||||
const rawHall=remote.getValue('hall_config');
|
||||
const merged=rawHall==null?{}:object(rawHall,'hall_config');
|
||||
const hall={...HALL_DEFAULTS,...merged};
|
||||
for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){
|
||||
// 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData.
|
||||
if(key==='actData')continue;
|
||||
const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value});
|
||||
}
|
||||
for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){
|
||||
if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`);
|
||||
if(key==='androidPay')hall[key]=Number(hall[key]);
|
||||
}
|
||||
for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`);
|
||||
const sys={...SYS_DEFAULTS};
|
||||
for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof SYS_DEFAULTS)[]){
|
||||
const value=remote.getValue(key);if(value!=null)Object.assign(sys,{[key]:value});
|
||||
}
|
||||
for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){
|
||||
if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`);
|
||||
if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]);
|
||||
}
|
||||
if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid');
|
||||
const rawAgent=remote.getValue('agentmode');
|
||||
const agentmode=rawAgent==null?1:Number(rawAgent);
|
||||
if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode');
|
||||
return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template});
|
||||
import type { RemoteConfig } from '../config/remote-config.ts';
|
||||
|
||||
export interface LobbyTemplate {
|
||||
isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean;
|
||||
}
|
||||
/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */
|
||||
export const LOBBY_TEMPLATE:Readonly<LobbyTemplate>=Object.freeze({
|
||||
isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false,
|
||||
});
|
||||
export interface LobbyContext {versionState:0|1;launchMode:0|1;}
|
||||
const HALL_DEFAULTS=Object.freeze({androidPay:0,rankList:0 as number|string,isReSetOwnerNote:0 as number|string,storeUrl:'',bagUrl:'',actData:0 as unknown,worldRoomList:0 as number|string,wareHouse:0 as number|string});
|
||||
const SYS_DEFAULTS=Object.freeze({vipRankLimit:10,hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1,
|
||||
ofcShortCode:null as unknown,hideSpid:null as unknown});
|
||||
function object(value:unknown,name:string):Record<string,unknown>{
|
||||
if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`);
|
||||
return value as Record<string,unknown>;
|
||||
}
|
||||
/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */
|
||||
export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly<LobbyTemplate>=LOBBY_TEMPLATE){
|
||||
const rawHall=remote.getValue('hall_config');
|
||||
const merged=rawHall==null?{}:object(rawHall,'hall_config');
|
||||
const hall={...HALL_DEFAULTS,...merged};
|
||||
for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){
|
||||
// 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData.
|
||||
if(key==='actData')continue;
|
||||
const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value});
|
||||
}
|
||||
for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){
|
||||
if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`);
|
||||
if(key==='androidPay')hall[key]=Number(hall[key]);
|
||||
}
|
||||
for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`);
|
||||
const sys={...SYS_DEFAULTS};
|
||||
for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof SYS_DEFAULTS)[]){
|
||||
const value=remote.getValue(key);if(value!=null)Object.assign(sys,{[key]:value});
|
||||
}
|
||||
if(!Number.isInteger(Number(sys.vipRankLimit))||Number(sys.vipRankLimit)<=0)throw Error('Invalid vipRankLimit');
|
||||
sys.vipRankLimit=Number(sys.vipRankLimit);
|
||||
for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){
|
||||
if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`);
|
||||
if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]);
|
||||
}
|
||||
if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid');
|
||||
const rawAgent=remote.getValue('agentmode');
|
||||
const agentmode=rawAgent==null?1:Number(rawAgent);
|
||||
if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode');
|
||||
// Original GameUI draw cases 568/483/3139: missing vipConfig means unopened;
|
||||
// the earlier matchhtml condition is commented out in the source.
|
||||
const vipConfig=remote.getValue('vipConfig');
|
||||
const locked=Object.freeze({568:!hall.worldRoomList,483:!vipConfig,3139:!sys.ofcShortCode});
|
||||
return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template,locked});
|
||||
}
|
||||
export type LobbyMenuConfig=ReturnType<typeof resolveLobbyMenuConfig>;
|
||||
export interface LobbyMenuState {visible:Record<number,boolean>;left:Record<number,number>;top:Record<number,number>;}
|
||||
|
||||
/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */
|
||||
export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
|
||||
const {hall,sys,template,agentmode}=config;
|
||||
const visible:Record<number,boolean>={},left:Record<number,number>={},top:Record<number,number>={};
|
||||
const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;};
|
||||
// Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X.
|
||||
if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;}
|
||||
hide([219,220]);
|
||||
if(context.versionState===1){
|
||||
if(context.launchMode===0)hide([234]);
|
||||
hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604,
|
||||
286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]);
|
||||
}else{
|
||||
if(agentmode===1)hide([10,509]);
|
||||
visible[235]=false;
|
||||
if(hall.androidPay===1){visible[10]=true;visible[509]=true;}
|
||||
visible[3018]=!template.isGameHall&&Array.isArray(hall.actData);
|
||||
visible[3016]=Boolean(hall.storeUrl);
|
||||
if(template.isGameHall||template.vipInfinite)hide([3139,679]);
|
||||
if(template.mainMenuButton)hide([11,12,568,483]);
|
||||
if(template.hideNotice)hide([16,180,193,194,682,178,179,145]);
|
||||
if(sys.hideTaskBtn)hide([17]);
|
||||
if(sys.miniPro||sys.hideWxShare)hide([14]);
|
||||
if(!template.isGameHall){
|
||||
const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false);
|
||||
shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);});
|
||||
}
|
||||
visible[3233]=Boolean(hall.rankList);
|
||||
visible[3256]=false; // Both original deviceLogin branches hide this entry.
|
||||
if(template.isGameHall){
|
||||
hide([14,13,17,3120,99,3016,76,234,3057,145,3139,3232,3233,604]);
|
||||
visible[235]=agentmode!==1;visible[15]=true;visible[5]=true;
|
||||
const shown=[235,15,5,7].filter(id=>visible[id]!==false);
|
||||
shown.forEach((id,index)=>{left[id]=300+520*index/(shown.length-1);top[id]=642;});
|
||||
}
|
||||
}
|
||||
if(sys.hideRoomCard===2)hide([286,10,287,3161]);
|
||||
visible[3264]=sys.cdKey===2;
|
||||
return {visible,left,top};
|
||||
/** Original click messages intentionally use different comma characters. */
|
||||
export function lobbyLockedTip(config:LobbyMenuConfig,action:'world-rooms'|'match'|'official-rooms'):string|null{
|
||||
if(action==='world-rooms')return config.locked[568]?'暂未开放,敬请期待':null;
|
||||
return config.locked[action==='match'?483:3139]?'暂未开放,敬请期待':null;
|
||||
}
|
||||
|
||||
/** openPallet runs its own visibility pass each time before placing visible entries. */
|
||||
export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
|
||||
const visible:Record<number,boolean>={15:false,604:Boolean(config.hall.rankList),
|
||||
235:config.agentmode!==1&&!config.sys.miniPro,3017:Boolean(config.hall.bagUrl),3057:Boolean(config.hall.wareHouse),5:true};
|
||||
const left:Record<number,number>={3121:200},top:Record<number,number>={};
|
||||
if(context.versionState===1)return {visible:Object.fromEntries(Object.keys(visible).map(id=>[id,false])),left,top};
|
||||
const shown=[15,604,235,3017,3057,5].filter(id=>visible[id]);
|
||||
shown.forEach((id,index)=>{left[id]=216+155*index;top[id]=540;});
|
||||
return {visible,left,top};
|
||||
}
|
||||
|
||||
/** IDs remain source traceability only; Cocos owns hit testing and prefab lifetimes. */
|
||||
export const LOBBY_MENU_ACTIONS=Object.freeze({
|
||||
7:'settings',9:'profile',11:'create-room',12:'join-room',13:'records',14:'share',17:'tasks',679:'vip-rooms',
|
||||
10:'buy-roomcard',286:'buy-roomcard',287:'buy-roomcard',3161:'buy-roomcard',
|
||||
508:'buy-bean',509:'buy-bean',496:'buy-bean',3162:'buy-bean',
|
||||
483:'match',568:'world-rooms',3016:'store',3018:'activity',99:'my-rooms',76:'qrcode',
|
||||
3139:'official-rooms',234:'more-games',3120:'tray',3233:'rank',3232:'vip-rank',3256:'bind-phone',3264:'cdkey',
|
||||
5:'notice',3017:'bag',235:'agent',604:'rank',3057:'warehouse',15:'feedback',
|
||||
} as const);
|
||||
export type LobbyAction=typeof LOBBY_MENU_ACTIONS[keyof typeof LOBBY_MENU_ACTIONS];
|
||||
export const LOBBY_UNAVAILABLE:Readonly<Partial<Record<LobbyAction,string>>>=Object.freeze({
|
||||
'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块',
|
||||
'buy-roomcard':'购买房卡:尚未接入支付模块','buy-bean':'购买星星:尚未接入支付模块',
|
||||
match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口',
|
||||
activity:'活动:尚未接入活动身份参数与原生接口','my-rooms':'我的房间:尚未接入房间记录与进房',qrcode:'二维码:尚未接入分享模块',
|
||||
'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口',
|
||||
rank:'排行:尚未接入排行榜', 'vip-rank':'VIP 排行:尚未接入排行榜','bind-phone':'绑定手机:尚未接入绑定流程',
|
||||
cdkey:'兑换码:尚未接入兑换请求',bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口',
|
||||
warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交',
|
||||
});
|
||||
export interface LobbyMenuState {visible:Record<number,boolean>;left:Record<number,number>;top:Record<number,number>;}
|
||||
|
||||
/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */
|
||||
export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
|
||||
const {hall,sys,template,agentmode}=config;
|
||||
const visible:Record<number,boolean>={},left:Record<number,number>={},top:Record<number,number>={};
|
||||
const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;};
|
||||
// Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X.
|
||||
if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;}
|
||||
hide([219,220]);
|
||||
if(context.versionState===1){
|
||||
if(context.launchMode===0)hide([234]);
|
||||
hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604,
|
||||
286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]);
|
||||
}else{
|
||||
if(agentmode===1)hide([10,509]);
|
||||
visible[235]=false;
|
||||
if(hall.androidPay===1){visible[10]=true;visible[509]=true;}
|
||||
visible[3018]=!template.isGameHall&&Array.isArray(hall.actData);
|
||||
visible[3016]=Boolean(hall.storeUrl);
|
||||
if(template.isGameHall||template.vipInfinite)hide([3139,679]);
|
||||
if(template.mainMenuButton)hide([11,12,568,483]);
|
||||
if(template.hideNotice)hide([16,180,193,194,682,178,179,145]);
|
||||
if(sys.hideTaskBtn)hide([17]);
|
||||
if(sys.miniPro||sys.hideWxShare)hide([14]);
|
||||
if(!template.isGameHall){
|
||||
const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false);
|
||||
shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);});
|
||||
}
|
||||
visible[3233]=Boolean(hall.rankList);
|
||||
visible[3256]=false; // Both original deviceLogin branches hide this entry.
|
||||
if(template.isGameHall){
|
||||
hide([14,13,17,3120,99,3016,76,234,3057,145,3139,3232,3233,604]);
|
||||
visible[235]=agentmode!==1;visible[15]=true;visible[5]=true;
|
||||
const shown=[235,15,5,7].filter(id=>visible[id]!==false);
|
||||
shown.forEach((id,index)=>{left[id]=300+520*index/(shown.length-1);top[id]=642;});
|
||||
}
|
||||
}
|
||||
if(sys.hideRoomCard===2)hide([286,10,287,3161]);
|
||||
visible[3264]=sys.cdKey===2;
|
||||
return {visible,left,top};
|
||||
}
|
||||
|
||||
/** openPallet runs its own visibility pass each time before placing visible entries. */
|
||||
export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
|
||||
const visible:Record<number,boolean>={15:false,604:Boolean(config.hall.rankList),
|
||||
235:config.agentmode!==1&&!config.sys.miniPro,3017:Boolean(config.hall.bagUrl),3057:Boolean(config.hall.wareHouse),5:true};
|
||||
const left:Record<number,number>={3121:200},top:Record<number,number>={};
|
||||
if(context.versionState===1)return {visible:Object.fromEntries(Object.keys(visible).map(id=>[id,false])),left,top};
|
||||
const shown=[15,604,235,3017,3057,5].filter(id=>visible[id]);
|
||||
shown.forEach((id,index)=>{left[id]=216+155*index;top[id]=540;});
|
||||
return {visible,left,top};
|
||||
}
|
||||
|
||||
/** IDs remain source traceability only; Cocos owns hit testing and prefab lifetimes. */
|
||||
export const LOBBY_MENU_ACTIONS=Object.freeze({
|
||||
7:'settings',9:'profile',11:'create-room',12:'join-room',13:'records',14:'share',17:'tasks',679:'vip-rooms',
|
||||
10:'buy-roomcard',286:'buy-roomcard',287:'buy-roomcard',3161:'buy-roomcard',
|
||||
508:'buy-bean',509:'buy-bean',496:'buy-bean',3162:'buy-bean',
|
||||
483:'match',568:'world-rooms',3016:'store',3018:'activity',99:'my-rooms',76:'qrcode',
|
||||
3139:'official-rooms',234:'more-games',3120:'tray',3233:'rank',3232:'vip-rank',3256:'bind-phone',3264:'cdkey',
|
||||
5:'notice',3017:'bag',235:'agent',604:'rank',3057:'warehouse',15:'feedback',
|
||||
} as const);
|
||||
export type LobbyAction=typeof LOBBY_MENU_ACTIONS[keyof typeof LOBBY_MENU_ACTIONS];
|
||||
export const LOBBY_UNAVAILABLE:Readonly<Partial<Record<LobbyAction,string>>>=Object.freeze({
|
||||
'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块',
|
||||
match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口',
|
||||
activity:'活动:尚未接入活动身份参数与原生接口',
|
||||
'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口',
|
||||
'bind-phone':'绑定手机:尚未接入绑定流程',
|
||||
bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口',
|
||||
warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交',
|
||||
});
|
||||
|
||||
@@ -1,66 +1,74 @@
|
||||
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',
|
||||
});
|
||||
}
|
||||
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;
|
||||
starName:string; roomCardName: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 {
|
||||
// 04_Data starts with 房卡; getConfig_Succ replaces only a truthy roomCardName.
|
||||
const cardName=remote.getValue('roomCardName');
|
||||
const roomCardName=cardName?requireString(cardName,'roomCardName'):'房卡';
|
||||
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({roomCardName,noticeHours,notices:Object.freeze(notices),scroll:scroll==null?'':requireString(scroll,'scrollmsg'),
|
||||
starName:star==null?'星星':requireString(star,'starName'),contacts:Object.freeze(contacts)});
|
||||
}
|
||||
/** 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;
|
||||
/** Game_Config.Notice: speed 0.1 px/ms, 15 px per gblen byte. */
|
||||
export function announcementMotion(text:string,width:number,elapsedSeconds:number){
|
||||
if(!Number.isFinite(width)||width<=0)throw Error('Invalid announcement width');
|
||||
if(!Number.isFinite(elapsedSeconds)||elapsedSeconds<0)throw Error('Invalid announcement elapsed time');
|
||||
const textWidth=text.replace(/[^\x00-\xff]/g,'**').length*15;
|
||||
const duration=(width+textWidth)/100;
|
||||
return {visible:text.length>0,textWidth,duration,left:width-(elapsedSeconds%duration)*100};
|
||||
}
|
||||
export interface Notice {type:0|1; text:string; image:string; url:string; height:number;}
|
||||
export interface LobbyPolicy {noticeHours:number; notices:readonly Notice[]; scroll:string;
|
||||
starName:string; roomCardName: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 {
|
||||
// 04_Data starts with 房卡; getConfig_Succ replaces only a truthy roomCardName.
|
||||
const cardName=remote.getValue('roomCardName');
|
||||
const roomCardName=cardName?requireString(cardName,'roomCardName'):'房卡';
|
||||
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({roomCardName,noticeHours,notices:Object.freeze(notices),scroll:scroll==null?'':requireString(scroll,'scrollmsg'),
|
||||
starName:star==null?'星星':requireString(star,'starName'),contacts:Object.freeze(contacts)});
|
||||
}
|
||||
/** 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,24 @@
|
||||
import {LEGACY_AUTH_DISTRICTS} from './legacy-auth-districts.ts';
|
||||
|
||||
/** Game_Surface_3 05_Func.IDValidator + 11_GameUI cases 484/493.
|
||||
* Local format validation only. Deliberately retain the source's 15-digit,
|
||||
* parent-district and month/day range semantics; this is not server identity proof.
|
||||
*/
|
||||
export function validateProfileAuth(name:string,id:string):'name'|'id'|null {
|
||||
if(name.length<=1)return 'name';
|
||||
const code=id.toUpperCase();
|
||||
if(code.length!==15&&code.length!==18)return 'id';
|
||||
const body=code.length===18?code.slice(0,17):code;
|
||||
const address=body.slice(0,6);
|
||||
const hasAddress=(key:string)=>Object.prototype.hasOwnProperty.call(LEGACY_AUTH_DISTRICTS,key);
|
||||
if(!hasAddress(address)&&!hasAddress(address.slice(0,4)+'00')&&!hasAddress(address.slice(0,2)+'0000'))return 'id';
|
||||
const birth=code.length===18?body.slice(6,14):body.slice(6,12);
|
||||
const month=parseInt(birth.slice(-4,-2),10),day=parseInt(birth.slice(-2),10);
|
||||
if(month>12||month===0||day>31||day===0)return 'id';
|
||||
if(code.length===15)return null;
|
||||
let sum=0;
|
||||
for(let index=0;index<body.length;index++)sum+=parseInt(body[index],10)*(Math.pow(2,17-index)%11);
|
||||
const value=12-sum%11;
|
||||
const check=value===10?'X':String(value>10?value%11:value);
|
||||
return check===code.slice(-1)?null:'id';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "11f5830e-5f4b-46fb-9e06-b2b65edbed89",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export type LoadingRpc='get_player_task'|'get_task_award'|'getVipRankList';
|
||||
/** Wire receipt, rather than a successful model update, completes request loading. */
|
||||
export class RequestLoading {
|
||||
private readonly pending=new Set<LoadingRpc>();
|
||||
constructor(private readonly show:(visible:boolean)=>void){}
|
||||
begin(rpc:LoadingRpc):void{const empty=this.pending.size===0;this.pending.add(rpc);if(empty)this.show(true);}
|
||||
cancel(rpc:LoadingRpc):void{if(this.pending.delete(rpc)&&this.pending.size===0)this.show(false);}
|
||||
received(message:{route:string;rpc:string;data?:unknown}):void{
|
||||
if(message.route!=='agent')return;
|
||||
if(message.rpc==='show_message'){this.clear();return;}
|
||||
if(message.rpc==='get_player_task'||message.rpc==='get_task_award'||message.rpc==='getVipRankList')this.cancel(message.rpc);
|
||||
}
|
||||
clear():void{if(this.pending.size){this.pending.clear();this.show(false);}}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8bb6a294-bfcf-4126-98f1-0845b58c72db",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,19 +1,27 @@
|
||||
import type { RuntimeConfig } from '../config/runtime-config.ts';
|
||||
/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */
|
||||
export function resolveSharePolicy(config:RuntimeConfig){
|
||||
const remote=config.remoteConfig,raw=remote.getValue('hall_config');
|
||||
if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config');
|
||||
const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record<string,unknown>;
|
||||
for(const key of Object.keys(hall)){const value=remote.getValue(key);if(value!=null)hall[key]=value;}
|
||||
const text=(value:unknown,name:string)=>{if(typeof value!=='string')throw Error(`Invalid ${name}`);return value;};
|
||||
const download=remote.getValue('url_download'),name=remote.getValue('sharename'),id=config.identity;
|
||||
return {url:download?`${text(download,'url_download')}?agentid=${id.agentid}&channelid=${id.channelid}&gameid=${id.gameid}&marketid=${id.marketid}`:'',
|
||||
title:name?text(name,'sharename'):'',description:hall.shareDesc?text(hall.shareDesc,'shareDesc'):'hello world',
|
||||
background:text(hall.shareBgUrl,'shareBgUrl'),image:text(hall.shareImgUrl,'shareImgUrl'),imageTimeline:!!hall.shareType};
|
||||
}
|
||||
export function sharePayload(policy:ReturnType<typeof resolveSharePolicy>,friend:1|2){
|
||||
if(friend===2&&!policy.imageTimeline)return null; // Legacy case 75 has no link-sharing call.
|
||||
return {sharefriend:friend,type:friend===1?1:3,webpageUrl:friend===1?policy.url:policy.image||policy.url,
|
||||
title:friend===1?policy.title||policy.title+policy.description:policy.title+policy.description,
|
||||
description:friend===1?policy.description:undefined,sharetype:0};
|
||||
/** Open_ewm uses this exact config key; absent configuration intentionally does nothing. */
|
||||
export function resolveQrCodePolicy(config:RuntimeConfig){
|
||||
const url=config.remoteConfig.getValue('WechatPublicNo_ewm');
|
||||
if(!url)return null;
|
||||
if(typeof url!=='string')throw Error('Invalid WechatPublicNo_ewm');
|
||||
const policy=resolveSharePolicy(config);
|
||||
return {url,share:{sharefriend:1,type:3,webpageUrl:url,title:policy.title+policy.description,description:undefined,sharetype:0}};
|
||||
}
|
||||
/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */
|
||||
export function resolveSharePolicy(config:RuntimeConfig){
|
||||
const remote=config.remoteConfig,raw=remote.getValue('hall_config');
|
||||
if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config');
|
||||
const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record<string,unknown>;
|
||||
for(const key of Object.keys(hall)){const value=remote.getValue(key);if(value!=null)hall[key]=value;}
|
||||
const text=(value:unknown,name:string)=>{if(typeof value!=='string')throw Error(`Invalid ${name}`);return value;};
|
||||
const download=remote.getValue('url_download'),name=remote.getValue('sharename'),id=config.identity;
|
||||
return {url:download?`${text(download,'url_download')}?agentid=${id.agentid}&channelid=${id.channelid}&gameid=${id.gameid}&marketid=${id.marketid}`:'',
|
||||
title:name?text(name,'sharename'):'',description:hall.shareDesc?text(hall.shareDesc,'shareDesc'):'hello world',
|
||||
background:text(hall.shareBgUrl,'shareBgUrl'),image:text(hall.shareImgUrl,'shareImgUrl'),imageTimeline:!!hall.shareType};
|
||||
}
|
||||
export function sharePayload(policy:ReturnType<typeof resolveSharePolicy>,friend:1|2){
|
||||
if(friend===2&&!policy.imageTimeline)return null; // Legacy case 75 has no link-sharing call.
|
||||
return {sharefriend:friend,type:friend===1?1:3,webpageUrl:friend===1?policy.url:policy.image||policy.url,
|
||||
title:friend===1?policy.title||policy.title+policy.description:policy.title+policy.description,
|
||||
description:friend===1?policy.description:undefined,sharetype:0};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user