feat: 百人赛

This commit is contained in:
guanchao 2026-09-24 19:33:11 +08:00
parent 8fb2ef1be8
commit 2fe6c9fb62
19 changed files with 150 additions and 165 deletions

View File

@ -1,13 +0,0 @@
{
"ver": "1.1.3",
"uuid": "34338d87-8ed5-4847-bdb5-cc0e0085901b",
"importer": "folder",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}

View File

@ -137,7 +137,6 @@ export default class CloudRiseView extends cc.Component {
this.entry.active = !this.map && CloudRiseService.homeEntryVisible(ActivityScheduleService.active("cloudRise"),
ActivityScheduleService.hasSchedule(), ActivityScheduleService.now());
this.updateEntryCountdown();
this.layoutEntry();
if (!this.map && this.displayedCoin !== cc.fx.GameConfig.GM_INFO.coin) {
this.displayedCoin = cc.fx.GameConfig.GM_INFO.coin;
const home: any = this.host.getComponent("JiaZai");
@ -317,14 +316,6 @@ export default class CloudRiseView extends cc.Component {
this.entryCountdown.string = [Math.floor(seconds / 3600), Math.floor(seconds / 60) % 60, seconds % 60]
.map(pad).join(":");
}
private layoutEntry(): void {
if (this.map || !this.entry.active || !this.entry.parent) return;
const top = this.entry.parent, first = top.getChildByName("shop");
if (!first) return;
const visible = ["shop", "yicon", "redeemBtn", "passBtn", "jungle", "xinshou"]
.map(name => top.getChildByName(name)).filter(node => node && node.active);
this.entry.setPosition(first.x, visible.length ? Math.min(...visible.map(node => node.y)) - 220 : first.y);
}
onDestroy(): void {
cc.game.off(cc.game.EVENT_SHOW, this.onShow, this);
this.unschedule(this.tick);

View File

@ -0,0 +1,52 @@
/** 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 rightNames = ["rank", "gacha", "day", "hammer"];
static refresh(top: cc.Node): void {
if (!top || !top.parent) return;
const first = top.getChildByName("shop"), right = top.getChildByName("rank");
const footer = top.parent.getChildByName("shezhiBtn");
if (!first || !right || !footer) return;
const left = this.leftNames.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]),
...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 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);
if (y + below >= footerTop) {
node.setPosition(first.x, y); leftY = y; leftBottom = y + below;
} else {
// Use the free lower right slots instead of covering the bottom-left Settings button.
const nextY = Math.min(rightY - 220, rightBottom - 16 - above);
node.setPosition(right.x, nextY); rightY = nextY; rightBottom = nextY + below;
}
}
}
/** Include clocks and labels below icons so the next entry cannot cover them. */
private static extent(node: cc.Node): { top: number; bottom: number } {
let top = node.height * (1 - node.anchorY), bottom = -node.height * node.anchorY;
for (const child of node.children) {
if (!child.active) continue;
const bounds = this.extent(child), scale = Math.abs(child.scaleY);
top = Math.max(top, child.y + bounds.top * scale);
bottom = Math.min(bottom, child.y + bounds.bottom * scale);
}
return { top, bottom };
}
}

View File

@ -0,0 +1,10 @@
{
"ver": "1.1.0",
"uuid": "79848e7c-3bbb-4cff-b60b-ec1748b22be4",
"importer": "typescript",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@ -1,4 +1,5 @@
import ActivityScheduleService from "./module/Config/ActivityScheduleService";
import HomeActivityEntryLayout from "./HomeActivityEntryLayout";
const { ccclass } = cc._decorator;
@ -6,6 +7,10 @@ const { ccclass } = cc._decorator;
@ccclass
export default class HomeActivitySchedule extends cc.Component {
private visibility = 0;
lateUpdate(): void {
const load = this.node.getChildByName("Load");
HomeActivityEntryLayout.refresh(load && load.getChildByName("Top"));
}
onEnable(): void {
cc.game.on(cc.game.EVENT_SHOW, this.onShow, this);
cc.game.on(cc.game.EVENT_HIDE, this.onHide, this);

View File

@ -86,10 +86,8 @@ export default class JiaZai extends cc.Component {
private jungleSettlementRequestId: number = 0;
private jungleSettlementRequestTimeout: any = null;
private pendingJungleSettlement: any = null;
private starterPackDefaultPosition: cc.Vec2 = null;
private starterPackOpening = false;
private starterPackChecking = false;
private benefitsEntryOffset: cc.Vec2 = null;
// 缓存 reward 预制体
private static cachedRewardPrefab: cc.Prefab | null = null;
@ -1397,6 +1395,9 @@ export default class JiaZai extends cc.Component {
"shop",
"yicon",
"passBtn",
"chengxu",
"cloudRise",
"redeemBtn",
"xinshou",
"jungle",
"avatar",
@ -3144,37 +3145,8 @@ export default class JiaZai extends cc.Component {
return top && top.getChildByName("xinshou");
}
/** 新手礼包补位时,福利入口保持场景中配置的相对间距。 */
/** Visibility is owned here; HomeActivitySchedule arranges all entries before rendering. */
private refreshHomeActivityEntryLayout(): void {
let starterPackButton = this.getStarterPackHomeButton();
let jungleButton = this.getJungleTreasureHomeButton();
if (!starterPackButton || !jungleButton) {
return;
}
if (!this.starterPackDefaultPosition) {
this.starterPackDefaultPosition = cc.v2(starterPackButton.x, starterPackButton.y);
}
const benefitsButton = starterPackButton.parent.getChildByName("chengxu");
if (benefitsButton && !this.benefitsEntryOffset) {
this.benefitsEntryOffset = cc.v2(
benefitsButton.x - this.starterPackDefaultPosition.x,
benefitsButton.y - this.starterPackDefaultPosition.y,
);
}
let targetPosition = starterPackButton.active && !jungleButton.active
? cc.v2(jungleButton.x, jungleButton.y)
: this.starterPackDefaultPosition;
starterPackButton.setPosition(targetPosition);
if (benefitsButton && this.benefitsEntryOffset) {
// Hidden activities do not reserve slots above the benefits entry.
const benefitsPosition = starterPackButton.active
? cc.v2(targetPosition.x + this.benefitsEntryOffset.x, targetPosition.y + this.benefitsEntryOffset.y)
: jungleButton.active ? this.starterPackDefaultPosition : cc.v2(jungleButton.x, jungleButton.y);
benefitsButton.setPosition(benefitsPosition);
}
MiniProgramBenefitsBridge.instance.refreshEntry();
}

View File

@ -24,7 +24,7 @@
<key>offset</key>
<string>{-1,-7}</string>
<key>rotated</key>
<false/>
<true/>
<key>sourceColorRect</key>
<string>{{3,19},{196,180}}</string>
<key>sourceSize</key>
@ -63,7 +63,7 @@
<key>offset</key>
<string>{-1,-7}</string>
<key>rotated</key>
<false/>
<true/>
<key>sourceColorRect</key>
<string>{{5,19},{192,180}}</string>
<key>sourceSize</key>
@ -103,7 +103,6 @@
<string>{0,-3}</string>
<key>rotated</key>
<false/>
<false/>
<key>sourceColorRect</key>
<string>{{11,11},{182,188}}</string>
<key>sourceSize</key>
@ -116,7 +115,7 @@
<key>offset</key>
<string>{0,-7}</string>
<key>rotated</key>
<true/>
<false/>
<key>sourceColorRect</key>
<string>{{4,19},{196,180}}</string>
<key>sourceSize</key>
@ -142,7 +141,7 @@
<key>offset</key>
<string>{0,0}</string>
<key>rotated</key>
<true/>
<false/>
<key>sourceColorRect</key>
<string>{{0,0},{188,190}}</string>
<key>sourceSize</key>
@ -156,7 +155,6 @@
<string>{0,0}</string>
<key>rotated</key>
<true/>
<true/>
<key>sourceColorRect</key>
<string>{{0,5},{204,194}}</string>
<key>sourceSize</key>
@ -182,7 +180,7 @@
<key>offset</key>
<string>{-5,-8}</string>
<key>rotated</key>
<false/>
<true/>
<key>sourceColorRect</key>
<string>{{2,21},{190,178}}</string>
<key>sourceSize</key>
@ -196,7 +194,6 @@
<string>{0,0}</string>
<key>rotated</key>
<false/>
<false/>
<key>sourceColorRect</key>
<string>{{0,0},{146,160}}</string>
<key>sourceSize</key>
@ -209,7 +206,7 @@
<key>offset</key>
<string>{0,-8}</string>
<key>rotated</key>
<false/>
<true/>
<key>sourceColorRect</key>
<string>{{11,21},{182,178}}</string>
<key>sourceSize</key>

View File

@ -39,7 +39,7 @@
"rawTextureUuid": "b45f4eaf-4250-449a-81ea-a7ca1e534a02",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": -1,
"offsetY": -7,
"trimX": 198,
@ -108,7 +108,7 @@
"rawTextureUuid": "b45f4eaf-4250-449a-81ea-a7ca1e534a02",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": -1,
"offsetY": -7,
"trimX": 380,
@ -178,7 +178,6 @@
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"rotated": false,
"offsetX": 0,
"offsetY": -3,
"trimX": 390,
@ -201,7 +200,7 @@
"rawTextureUuid": "b45f4eaf-4250-449a-81ea-a7ca1e534a02",
"trimType": "auto",
"trimThreshold": 1,
"rotated": true,
"rotated": false,
"offsetX": 0,
"offsetY": -7,
"trimX": 192,
@ -247,7 +246,7 @@
"rawTextureUuid": "b45f4eaf-4250-449a-81ea-a7ca1e534a02",
"trimType": "auto",
"trimThreshold": 1,
"rotated": true,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 2,
@ -271,7 +270,6 @@
"trimType": "auto",
"trimThreshold": 1,
"rotated": true,
"rotated": true,
"offsetX": 0,
"offsetY": 0,
"trimX": 2,
@ -317,7 +315,7 @@
"rawTextureUuid": "b45f4eaf-4250-449a-81ea-a7ca1e534a02",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": -5,
"offsetY": -8,
"trimX": 562,
@ -341,7 +339,6 @@
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 342,
@ -364,7 +361,7 @@
"rawTextureUuid": "b45f4eaf-4250-449a-81ea-a7ca1e534a02",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": -8,
"trimX": 825,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 158 KiB

View File

@ -1,13 +0,0 @@
{
"ver": "1.1.3",
"uuid": "4970b759-7a17-4559-84a3-9bbe73489173",
"importer": "folder",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}

View File

@ -1,13 +0,0 @@
{
"ver": "1.1.3",
"uuid": "e159bf60-8e51-4dca-85dd-8bec167c9036",
"importer": "folder",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}

View File

@ -1,13 +0,0 @@
{
"ver": "1.1.3",
"uuid": "dbde4bfd-0b20-45aa-8c9c-139722aabc64",
"importer": "folder",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}

View File

@ -1,13 +0,0 @@
{
"ver": "1.1.3",
"uuid": "7a2e5485-be73-4ed1-af4a-6a00ab845cce",
"importer": "folder",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}

View File

@ -1,13 +0,0 @@
{
"ver": "1.1.3",
"uuid": "a78cb4f0-a528-4a3e-9470-01100e4c24d1",
"importer": "folder",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}

View File

@ -1,13 +0,0 @@
{
"ver": "1.1.3",
"uuid": "d0d45606-9a70-4a8b-a213-540cc0100e9d",
"importer": "folder",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}

View File

@ -1,13 +0,0 @@
{
"ver": "1.1.3",
"uuid": "4572f582-ceb9-419d-b453-44d48b3ac622",
"importer": "folder",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}

View File

@ -57,4 +57,4 @@
"cloud-function"
],
"assets-sort-type": "name"
}
}

View File

@ -42,7 +42,7 @@ function fixture(storage = new Map()) {
return {service, cc, storage, requests, events, setNow: n => now = n, setEnv: e => environment = e,
failStorage: () => storageFails = true,
loadHome: () => load('assets/Script/HomeActivitySchedule.ts', cc,
{'./module/Config/ActivityScheduleService': {default: service}}, () => now),
{'./module/Config/ActivityScheduleService': {default: service}, './HomeActivityEntryLayout': {default: {refresh(){}}}}, () => now),
emit: name => {const event = events.get(name); event.fn.call(event.target);}};
}
const flush = async () => {for (let i = 0; i < 12; i++) await Promise.resolve();};

View File

@ -0,0 +1,65 @@
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'),ts=require('typescript');
const left=['shop','yicon','redeemBtn','passBtn','jungle','xinshou','chengxu','cloudRise'];
const right=['rank','gacha','day','hammer'];
function fixture(){
const exports={};
vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/Script/HomeActivityEntryLayout.ts','utf8'),{
compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017}}).outputText,{exports});
const a=JSON.parse(fs.readFileSync('assets/Scene/HomeScene.fire')),nodes=new Map();let writes=0;
for(let i=0;i<a.length;i++)if(a[i].__type__==='cc.Node'){
const n=a[i];nodes.set(i,{name:n._name,active:n._active,x:n._trs.array[0],y:n._trs.array[1],
width:n._contentSize.width,height:n._contentSize.height,anchorY:n._anchorPoint.y,scaleY:n._trs.array[8],children:[],
getChildByName(name){return this.children.find(n=>n.name===name);},setPosition(x,y){this.x=x;this.y=y;writes++;}});
}
for(const [i,n]of nodes){n.children=a[i]._children.map(r=>nodes.get(r.__id__));n.parent=nodes.get(a[i]._parent?.__id__);}
const top=[...nodes.values()].find(n=>n.name==='Top'&&n.parent.name==='Load');
const gift=top.getChildByName('xinshou');
const frame=JSON.parse(fs.readFileSync('assets/UI/UI/start/startUI2.plist.meta')).subMetas['cloudRise.png'];
const cloud={...gift,name:'cloudRise',width:frame.rawWidth,height:frame.rawHeight,children:gift.children.filter(n=>['New Sprite','time'].includes(n.name))};
top.children.push(cloud);
const layout=exports.default;
function box(n){const b=layout.extent(n),scale=top.parent.scaleY;return {top:n.y+b.top*scale,bottom:n.y+b.bottom*scale};}
function check(){
const all=[...left,...right].map(n=>top.getChildByName(n)).filter(n=>n.active);
for(let i=0;i<all.length;i++)for(let j=i+1;j<all.length;j++){
const p=all[i],q=all[j];if(Math.abs(p.x-q.x)>300)continue;
const a=box(p),b=box(q);assert(a.bottom>=b.top+15.99||b.bottom>=a.top+15.99,`${p.name} overlaps ${q.name}`);
}
const footer=top.parent.getChildByName('shezhiBtn'),bound=footer.y+footer.height*(1-footer.anchorY)*top.parent.scaleY-top.y;
for(const n of all)assert(box(n).bottom>=bound+15.99,`${n.name} covers footer: ${box(n).bottom} < ${bound+16}`);
}
return {layout,top,check,get writes(){return writes;}};
}
test('every visibility combination fits without overlap, including both columns and bottom controls',()=>{
const f=fixture();
for(const scale of [1,0.85]){
f.top.parent.scaleY=scale;
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();
}
}
});
test('benefits and cloudRise occupy distinct slots and async hide/show immediately compacts the layout',()=>{
const f=fixture();[...left,...right].forEach(name=>f.top.getChildByName(name).active=name!=='redeemBtn');
f.layout.refresh(f.top);f.check();
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.
f.top.getChildByName('xinshou').active=false;f.layout.refresh(f.top);f.check();
assert.equal(cloud.x,old.x);assert(cloud.y>old.y);
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();
});
test('home lays out late in the frame after asynchronous entry visibility changes',()=>{
const source=fs.readFileSync('assets/Script/HomeActivitySchedule.ts','utf8');let arranged;
const exports={},cc={Component:class{},_decorator:{ccclass:c=>c}};
vm.runInNewContext(ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017,experimentalDecorators:true}}).outputText,
{exports,cc,require:name=>({default:name.endsWith('HomeActivityEntryLayout')?{refresh:top=>arranged=top}:{}})});
const f=fixture(),home=new exports.default();home.node={getChildByName:()=>f.top.parent};home.lateUpdate();assert.equal(arranged,f.top);
assert(!fs.readFileSync('assets/Script/CloudRiseView.ts','utf8').includes('private layoutEntry()'),'CloudRise no longer writes competing positions');
assert(!fs.readFileSync('assets/Script/JiaZai.ts','utf8').includes('benefitsEntryOffset'),'benefits no longer follows a separate layout');
});