feat: 百人赛和小程序福利图标移动到首页右侧

This commit is contained in:
guanchao 2026-09-28 20:08:04 +08:00
parent b183bf7e08
commit 3d8a7bbdc4
11 changed files with 30740 additions and 30616 deletions

View File

@ -26,6 +26,8 @@ export default class CloudRiseView extends cc.Component {
private queuedElimination = "";
private eliminationDone: () => void = null;
private expiryPending = false;
private dailyReminderQueued = false;
private nextDailyReminderAt = 0;
/** All win continuations present pending results; only the activity overview starts another stage. */
static async continueFromNextLevel(host: cc.Node): Promise<boolean> {
@ -165,6 +167,35 @@ export default class CloudRiseView extends cc.Component {
void this.open("result").then(() => { if (!this.overlay) this.finishPopup(); }, () => this.finishPopup());
});
}
this.queueDailyReminder();
}
private queueDailyReminder(): void {
if (this.map || !this.entry.active || this.busy || this.overlay || this.dailyReminderQueued
|| Date.now() < this.nextDailyReminderAt || CloudRiseService.elimination()
|| CloudRiseService.hasPendingLoss() || CloudRiseService.unseenResult()) return;
const reminder = ActivityScheduleService.cloudRiseReminder();
if (!reminder) return;
this.dailyReminderQueued = true;
HomePopupQueue.get(this.host).enqueue("cloudRiseDailyReminder", 10, done => {
void (async () => {
try {
const current = ActivityScheduleService.cloudRiseReminder();
if (!cc.isValid(this.node, true) || !this.entry.active || !current
|| current.key !== reminder.key || current.day !== reminder.day
|| CloudRiseService.elimination() || CloudRiseService.hasPendingLoss() || CloudRiseService.unseenResult()) return;
await this.open("overview");
if (this.overlay && cc.isValid(this.node, true)) {
ActivityScheduleService.markCloudRiseReminderShown(reminder);
this.popupDone = done;
}
} finally {
this.dailyReminderQueued = false;
this.nextDailyReminderAt = Date.now() + 30000;
if (this.popupDone !== done) done();
}
})().catch(error => cc.warn(error));
});
}
private async showElimination(reuseCurrent = false): Promise<void> {

View File

@ -1,7 +1,7 @@
/** One owner for home activity positions; visibility is still decided by each activity. */
export default class HomeActivityEntryLayout {
private static signatures = new WeakMap<cc.Node, string>();
private static leftNames = ["shop", "yicon", "redeemBtn", "passBtn", "jungle", "xinshou", "chengxu", "cloudRise"];
private static leftNames = ["shop", "yicon", "redeemBtn", "passBtn", "jungle", "xinshou"];
private static rightNames = ["rank", "gacha", "day", "hammer"];
static refresh(top: cc.Node): void {
@ -10,26 +10,44 @@ export default class HomeActivityEntryLayout {
const footer = top.parent.getChildByName("shezhiBtn");
if (!first || !right || !footer) return;
const left = this.leftNames.map(name => top.getChildByName(name)).filter(node => !!node);
const rightActivities = ["cloudRise", "chengxu"].map(name => top.getChildByName(name)).filter(node => !!node);
const fixedRight = this.rightNames.map(name => top.getChildByName(name)).filter(node => !!node);
// Adaptation sets the same steady scale on Load and the side buttons. Ignore temporary button press zoom.
const scale = Math.abs(top.parent.scaleY) || 1;
const signature = JSON.stringify([first.x, first.y, right.x, top.y, scale, footer.y, footer.height,
...left.map(n => [n.name, n.active, n.width, n.height]),
...rightActivities.map(n => [n.name, n.active, n.width, n.height]),
...fixedRight.map(n => [n.name, n.active, n.x, n.y, n.width, n.height])]);
if (this.signatures.get(top) === signature) return;
this.signatures.set(top, signature);
const footerTop = footer.y + footer.height * (1 - footer.anchorY) * scale - top.y + 16;
let leftY = first.y + 220, leftBottom = Infinity;
const visibleLeft = left.filter(n => n.active);
const leftBounds = visibleLeft.map(n => this.extent(n));
const minimumSteps = leftBounds.slice(1).map((bounds, i) =>
(bounds.top - leftBounds[i].bottom) * scale + 16);
const minimumSpan = minimumSteps.reduce((sum, step) => sum + step, 0);
const extraSpan = minimumSteps.reduce((sum, step) => sum + Math.max(0, 220 - step), 0);
const availableSpan = leftBounds.length ? first.y + leftBounds[leftBounds.length - 1].bottom * scale - footerTop : 0;
// Tighten only the spare gaps when all left entries are visible, keeping the bottom controls clear.
const gapRatio = extraSpan ? Math.max(0, Math.min(1, (availableSpan - minimumSpan) / extraSpan)) : 1;
let leftY = first.y;
const occupiedRight = fixedRight.filter(n => n.active);
let rightY = occupiedRight.length ? Math.min(...occupiedRight.map(n => n.y)) : first.y + 220;
let rightBottom = occupiedRight.length
? Math.min(...occupiedRight.map(n => n.y + this.extent(n).bottom * scale)) : Infinity;
for (const node of left.filter(n => n.active)) {
const extent = this.extent(node), above = extent.top * scale, below = extent.bottom * scale;
const y = Math.min(leftY - 220, leftBottom - 16 - above);
for (const node of rightActivities.filter(n => n.active)) {
const bounds = this.extent(node);
rightY = Math.min(rightY - 220, rightBottom - 16 - bounds.top * scale);
node.setPosition(right.x, rightY);
rightBottom = rightY + bounds.bottom * scale;
}
for (let i = 0; i < visibleLeft.length; i++) {
const node = visibleLeft[i], extent = leftBounds[i], above = extent.top * scale, below = extent.bottom * scale;
const step = i ? minimumSteps[i - 1] : 0;
const y = i ? leftY - step - Math.max(0, 220 - step) * gapRatio : first.y;
if (y + below >= footerTop) {
node.setPosition(first.x, y); leftY = y; leftBottom = y + below;
node.setPosition(first.x, y); leftY = y;
} else {
// Use the free lower right slots instead of covering the bottom-left Settings button.
const nextY = Math.min(rightY - 220, rightBottom - 16 - above);

View File

@ -1389,14 +1389,14 @@ export default class JiaZai extends cc.Component {
"rank",
"day",
"hammer",
"cloudRise",
"chengxu",
"Coin"
];
const rightElements2 = [
"shop",
"yicon",
"passBtn",
"chengxu",
"cloudRise",
"redeemBtn",
"xinshou",
"jungle",

View File

@ -55,6 +55,20 @@ export default class ActivityScheduleService {
private static cacheKey(): string { return "activitySchedule:v2:" + this.environment + ":" + this.uid; }
static now(): number { this.ensure(); return Date.now() + this.offset; }
static hasSchedule(): boolean { return !!this.data(); }
/** One successfully displayed home reminder per account/environment and Beijing calendar day. */
static cloudRiseReminder(): { key: string; day: number } {
const period = this.active("cloudRise");
if (!this.uid || !period || Number(cc.fx.GameConfig.GM_INFO.level) < period.unlockLevel) return null;
const key = "cloudRiseDailyReminder:" + this.environment + ":" + this.uid;
const day = Math.floor((this.now() + 8 * 3600000) / 86400000);
try { if (cc.fx.StorageMessage.getStorage(key) === day) return null; } catch (_) {}
return { key, day };
}
static markCloudRiseReminderShown(reminder: { key: string; day: number }): void {
const current = this.cloudRiseReminder();
if (!current || current.key !== reminder.key || current.day !== reminder.day) return;
try { cc.fx.StorageMessage.setStorage(reminder.key, reminder.day); } catch (_) {}
}
private static data(): Schedule {
this.ensure();
return this.snapshot && this.now() < this.snapshot.windowEndsAt ? this.snapshot : null;

View File

@ -13,7 +13,7 @@ import {
//@ts-ignore
//最大工具类 各种公共方法,以及处理上传,获取后端接口数据
var GameTool = {
MAX_NORMAL_LEVEL: 2000,
MAX_NORMAL_LEVEL: 1980,
starterPackReactivationUid: "",
starterPackQueuedReason: "",
MEMORY_WARNING_EVENT: "game-memory-warning",

View File

@ -12401,8 +12401,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 235,
"height": 218
"width": 300,
"height": 209
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@ -12454,10 +12454,10 @@
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "895c2c81-f34c-4d7a-880d-e1eaabfafc41"
"__uuid__": "7ce90c89-a092-48a9-b22c-5f0431746477"
},
"_type": 0,
"_sizeMode": 0,
"_sizeMode": 1,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
@ -12762,8 +12762,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 23.207450980392156,
"height": 62
"width": 50,
"height": 57
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@ -12774,7 +12774,7 @@
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
11.603725490196078,
25,
0,
0,
0,
@ -12869,8 +12869,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 23.207450980392156,
"height": 62
"width": 50,
"height": 57
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@ -12881,7 +12881,7 @@
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
11.603725490196078,
75,
0,
0,
0,
@ -12976,8 +12976,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 23.207450980392156,
"height": 62
"width": 50,
"height": 57
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@ -12988,7 +12988,7 @@
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
11.603725490196078,
125,
0,
0,
0,
@ -13083,8 +13083,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 23.207450980392156,
"height": 62
"width": 50,
"height": 57
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@ -13095,7 +13095,7 @@
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
11.603725490196078,
175,
0,
0,
0,
@ -13190,8 +13190,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 23.207450980392156,
"height": 62
"width": 50,
"height": 57
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@ -13202,7 +13202,7 @@
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
11.603725490196078,
225,
0,
0,
0,
@ -19628,7 +19628,7 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 31.200000000000003,
"width": 37.787234042553195,
"height": 48
},
"_anchorPoint": {
@ -19640,7 +19640,7 @@
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
-19.893617021276597,
0,
0,
0,
@ -19681,7 +19681,7 @@
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "8144ce9e-4873-45df-9859-929000facf8e"
"__uuid__": "1aa86b1f-52b9-4891-af59-1b41872ca1bd"
},
"_type": 0,
"_sizeMode": 0,
@ -19735,7 +19735,7 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 31.200000000000003,
"width": 37.787234042553195,
"height": 48
},
"_anchorPoint": {
@ -19747,7 +19747,7 @@
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
19.893617021276597,
0,
0,
0,
@ -19788,7 +19788,7 @@
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "8144ce9e-4873-45df-9859-929000facf8e"
"__uuid__": "1aa86b1f-52b9-4891-af59-1b41872ca1bd"
},
"_type": 0,
"_sizeMode": 0,
@ -19823,7 +19823,7 @@
"__id__": 523
},
"_children": [],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 531
@ -22563,4 +22563,4 @@
"fileId": "",
"sync": false
}
]
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,26 @@ function fixture(storage = new Map()) {
}
const flush = async () => {for (let i = 0; i < 12; i++) await Promise.resolve();};
test('CloudRise daily reminder uses server Beijing date and persists separately per account/environment',async()=>{
const f=fixture();f.cc.fx.GameConfig.GM_INFO.level=100;
const serverNow=16*3600000-1000;
async function sync(){const task=f.service.sync(true);f.requests.at(-1).resolve(response([period('cloudRise',0,3*86400000)],serverNow));await task;}
assert.equal(f.service.cloudRiseReminder(),null);await sync();
const first=f.service.cloudRiseReminder();assert.equal(first.day,0);
f.service.markCloudRiseReminderShown(first);assert.equal(f.service.cloudRiseReminder(),null);
await sync();assert.equal(f.service.cloudRiseReminder(),null,'refresh does not repeat today');
const restarted=fixture(f.storage);restarted.cc.fx.GameConfig.GM_INFO.level=100;
const task=restarted.service.sync();restarted.requests[0].resolve(response([period('cloudRise',0,3*86400000)],serverNow));await task;
assert.equal(restarted.service.cloudRiseReminder(),null,'relogin keeps daily marker');
f.setNow(11000);assert.equal(f.service.cloudRiseReminder().day,1,'server midnight opens the next day');
f.cc.fx.GameConfig.GM_INFO.level=99;assert.equal(f.service.cloudRiseReminder(),null);
f.cc.fx.GameConfig.GM_INFO.level=100;f.cc.fx.GameConfig.GM_INFO.userId=1002;await sync();
assert.notEqual(f.service.cloudRiseReminder().key,first.key);
f.service.markCloudRiseReminderShown(first);assert(f.service.cloudRiseReminder(),'stale account cannot mark current account');
f.cc.fx.GameConfig.GM_INFO.userId=1001;f.setEnv('production/');await sync();assert.notEqual(f.service.cloudRiseReminder().key,first.key);
f.setNow(4*86400000);assert.equal(f.service.cloudRiseReminder(),null,'expired activity is not promoted');
});
test('cache environment resolver exactly matches POST routing in development, trial and production', () => {
let version, opened;
const cc = {fx: {GameTool: {getWechatGameVersion: () => version}, GameConfig: {GM_INFO: {}}}, loader: {

View File

@ -21,7 +21,7 @@ function fixture() {
String, isValid:()=>true, warn(){}, view:{getVisibleSize:()=>({width:720,height:1280})},
fx:{GameConfig:{GM_INFO:{userId:1001,coin:0}}}};
function load(file, imports) {
imports = {'./module/Config/ActivityScheduleService': {default:{active:()=>null,hasSchedule:()=>false,now:()=>now}}, ...imports};
imports = {'./module/Config/ActivityScheduleService': {default:{active:()=>null,hasSchedule:()=>false,now:()=>now,cloudRiseReminder:()=>null}}, ...imports};
const exports={};
vm.runInNewContext(ts.transpileModule(fs.readFileSync(file,'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,
target:ts.ScriptTarget.ES2017,experimentalDecorators:true}}).outputText,
@ -74,6 +74,30 @@ function continuationFixture() {
return {...f,View,view,host,popups,notices,setMatch:fn=>match=fn};
}
test('daily home reminder waits in the queue, marks only a displayed overview and releases on close',async()=>{
const f=fixture(),tasks=[];let marked=0,done=0;
const reminder={key:'account',day:1};let due=reminder;
const View=f.load('assets/Script/CloudRiseView.ts',{
'./module/Config/CloudRiseService':{default:f.bridge},
'./module/Config/ActivityScheduleService':{default:{cloudRiseReminder:()=>due,markCloudRiseReminderShown:()=>{marked++;due=null;}}},
'./Sdk/MiniGameSdk':{MiniGameSdk:{}},
'../home_popup_queue/HomePopupQueue':{default:{get:()=>({enqueue:(key,priority,run)=>tasks.push({key,priority,run})})}}
});
const view=new View();view.node={};view.entry={active:true};view.host={};
let opens=0,succeed=false;view.open=async mode=>{assert.equal(mode,'overview');opens++;if(succeed)view.overlay={destroy(){}};};
view.queueDailyReminder();view.queueDailyReminder();assert.equal(tasks.length,1);assert.equal(opens,0);
tasks[0].run(()=>done++);for(let i=0;i<8;i++)await Promise.resolve();
assert.equal(marked,0);assert.equal(done,1,'failed load releases the queue');
view.queueDailyReminder();assert.equal(tasks.length,1,'failed requests are throttled');
f.setNow(40000);succeed=true;view.queueDailyReminder();tasks[1].run(()=>done++);
for(let i=0;i<8;i++)await Promise.resolve();assert.equal(marked,1);assert.equal(done,1,'visible reminder retains queue');
view.close();assert.equal(done,2);f.setNow(80000);view.queueDailyReminder();assert.equal(tasks.length,2);
due=reminder;view.map={};view.queueDailyReminder();assert.equal(tasks.length,2,'never enqueue in GameScene');
view.map=null;f.service.hasPendingLoss=()=>true;view.queueDailyReminder();assert.equal(tasks.length,2,'settlement goes first');
f.service.hasPendingLoss=()=>false;view.queueDailyReminder();due=null;tasks[2].run(()=>done++);
for(let i=0;i<8;i++)await Promise.resolve();assert.equal(opens,2,'expired queued activity is skipped');assert.equal(done,3);
});
test('Next never automatically matches another stage, including waiting stages 1 and 2',async()=>{
for(const stage of [1,2]) {
const f=continuationFixture();f.run('waiting');f.data.run.stage=stage;
@ -637,7 +661,7 @@ test('home icon appears at cached start without relogin or a successful private
const View=f.load('assets/Script/CloudRiseView.ts',{
'./module/Config/CloudRiseService':{default:f.bridge},
'./module/Config/ActivityScheduleService':{default:schedules},
'./Sdk/MiniGameSdk':{MiniGameSdk:{API:{showToast(){}}}},'../home_popup_queue/HomePopupQueue':{default:{}}});
'./Sdk/MiniGameSdk':{MiniGameSdk:{API:{showToast(){}}}},'../home_popup_queue/HomePopupQueue':{default:{get:()=>({enqueue(){}})}}});
const view=new View();view.node={};view.entry={};view.displayedCoin=0;
const unsubscribe=schedules.subscribe(()=>view.tick());
view.tick();assert.equal(view.entry.active,false);const initial=f.calls.sync;
@ -745,7 +769,7 @@ test('home countdown follows the personal deadline, refreshes on resume and expi
const View=f.load('assets/Script/CloudRiseView.ts',{
'./module/Config/CloudRiseService':{default:f.bridge},
'./module/Config/ActivityScheduleService':{default:{
active:()=>hasSchedule&&f.service.now()<period.endsAt?period:null,hasSchedule:()=>hasSchedule,now:()=>f.service.now()}},
active:()=>hasSchedule&&f.service.now()<period.endsAt?period:null,hasSchedule:()=>hasSchedule,now:()=>f.service.now(),cloudRiseReminder:()=>null}},
'./Sdk/MiniGameSdk':{MiniGameSdk:{}},'../home_popup_queue/HomePopupQueue':{default:{}}
});
const view=new View();view.node={};view.entry={};view.entryCountdown={string:''};view.displayedCoin=0;

View File

@ -37,6 +37,13 @@ test('every visibility combination fits without overlap, including both columns
for(let mask=0;mask<4096;mask++){
[...left,...right].forEach((name,i)=>f.top.getChildByName(name).active=!!(mask&(1<<i)));
f.layout.refresh(f.top);f.check();
const cloud=f.top.getChildByName('cloudRise');
if(cloud.active)assert.equal(cloud.x,f.top.getChildByName('rank').x,'CloudRise always stays on the right');
const benefit=f.top.getChildByName('chengxu');
if(benefit.active){
assert.equal(benefit.x,f.top.getChildByName('rank').x,'benefits always stays on the right');
if(cloud.active)assert(benefit.y<cloud.y,'benefits follows CloudRise');
}
}
}
});
@ -46,12 +53,14 @@ test('benefits and cloudRise occupy distinct slots and async hide/show immediate
const benefit=f.top.getChildByName('chengxu'),cloud=f.top.getChildByName('cloudRise');
assert.notDeepEqual([benefit.x,benefit.y],[cloud.x,cloud.y]);
assert.equal(cloud.x,f.top.getChildByName('rank').x,'overflow uses the free right slot');
const old={x:benefit.x,y:benefit.y};benefit.active=false;f.layout.refresh(f.top);f.check();
// The countdown makes CloudRise taller than benefits; only reclaim a slot if the whole entry fits.
const old={x:cloud.x,y:cloud.y};benefit.active=false;f.layout.refresh(f.top);f.check();
f.top.getChildByName('xinshou').active=false;f.layout.refresh(f.top);f.check();
assert.equal(cloud.x,old.x);assert(cloud.y>old.y);
assert.equal(cloud.x,old.x);assert.equal(cloud.y,old.y,'left column changes do not move CloudRise');
const writes=f.writes;for(let i=0;i<20;i++)f.layout.refresh(f.top);assert.equal(f.writes,writes,'unchanged frames do not reposition buttons');
benefit.active=true;f.layout.refresh(f.top);f.check();
const benefitY=benefit.y;cloud.active=false;f.layout.refresh(f.top);f.check();
assert(benefit.y>benefitY,'benefits moves up when CloudRise is hidden');
cloud.active=true;f.layout.refresh(f.top);f.check();assert.equal(benefit.y,benefitY);
});
test('home lays out late in the frame after asynchronous entry visibility changes',()=>{