feat: wire lobby task and VIP prefab flows and sync UI assets

This commit is contained in:
2026-09-06 18:46:37 +08:00
parent 842c68ef55
commit 9c4510697c
184 changed files with 8348 additions and 19133 deletions
@@ -0,0 +1,121 @@
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});
}
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};
}
/** 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:'反馈:尚未接入意见提交',
});
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "d2a93860-e9ff-4974-a20c-5bf88fe85257",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -20,11 +20,14 @@ export function selectLobbyModel(state:PlatformState) {
}
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}[];}
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');
@@ -53,9 +56,8 @@ export function resolveLobbyPolicy(remote:RemoteConfig):LobbyPolicy {
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'))});
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 {
@@ -0,0 +1,19 @@
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};
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7a56e715-a9aa-4a2e-a5d3-f180ad8d8fda",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,20 @@
/** get_player_task.tasks fields; rendering mirrors 11_GameUI.OpenTask. */
import type { TaskData } from '../core/types/task.ts';
export type { TaskData } from '../core/types/task.ts';
export function taskItemModel(task:TaskData,index:number,roomCardName:string){
if(!task||typeof task!=='object')throw Error('Invalid task');
if(typeof task.title!=='string')throw Error('Invalid task.title');
if((typeof task.taskid!=='number'&&typeof task.taskid!=='string')||task.taskid==='')throw Error('Invalid task.taskid');
for(const name of ['award','finish','total','state'] as const){
if(typeof task[name]!=='number'||!Number.isFinite(task[name])||task[name]<0)throw Error(`Invalid task.${name}`);
}
if(!Number.isInteger(task.state))throw Error('Invalid task.state');
if(!Number.isInteger(index)||index<0)throw Error('Invalid task index');
if(typeof roomCardName!=='string'||!roomCardName)throw Error('Invalid roomCardName');
if(task.state===0&&task.total<=0)throw Error('Invalid task.total for incomplete task');
let title='';
for(let i=0;i<task.title.length;i++){title+=task.title[i];if((i+1)%8===0)title+='\n';}
return {taskid:task.taskid,title,reward:`奖励${roomCardName},数量:${task.award}`,
progress:task.state===0?Math.min(1,task.finish/task.total):1,progressText:`${task.finish}/${task.total}`,
showProgress:task.state===0,showDo:task.state===0&&index===0,showClaim:task.state===1,showClaimed:task.state!==0&&task.state!==1};
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "67c0a233-c87e-44f1-b39d-7e7617938a60",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,6 @@
import type {VipCode,VipRoom} from '../core/types/vip-room.ts';
import {sameVipCode} from '../core/types/vip-room.ts';
export function orderedVipRooms(rooms:readonly VipRoom[],codes:readonly VipCode[]):readonly VipRoom[]{
return codes.flatMap(code=>{const room=rooms.find(r=>sameVipCode(r.shortCode,code));return room?[room]:[];});
}
export const occupiedSeats=(players:readonly unknown[])=>players.filter(Boolean).length;
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "67e4fadf-563b-48d8-94d0-7cbfa50a0b19",
"files": [],
"subMetas": {},
"userData": {}
}