import CloudRiseService from "./module/Config/CloudRiseService"; import ActivityScheduleService from "./module/Config/ActivityScheduleService"; import { MiniGameSdk } from "./Sdk/MiniGameSdk"; import HomePopupQueue from "../home_popup_queue/HomePopupQueue"; const { ccclass } = cc._decorator; @ccclass export default class CloudRiseView extends cc.Component { private host: cc.Node; private play: () => void; private map: any = null; private entry: cc.Node; private entryCountdown: cc.Label = null; private overlay: cc.Node = null; private unsubscribe: () => void; private unsubscribeSchedule: () => void; private panel: any = null; private busy = false; private priorPause = false; private queuedResult = ""; private popupDone: () => void = null; private displayedCoin = -1; private nextSyncAt = 0; private timeState = ""; private queuedElimination = ""; private eliminationDone: () => void = null; private expiryPending = false; /** All win continuations present pending results; only the activity overview starts another stage. */ static async continueFromNextLevel(host: cc.Node): Promise { const current = CloudRiseService.run(); if (!current || !["playing", "waiting"].includes(current.status) && !CloudRiseService.elimination() && !CloudRiseService.unseenResult()) return true; try { if (!await CloudRiseService.sync()) return true; // The durable queue retries in the background. if (CloudRiseService.challengeExpired()) { CloudRiseService.dismissExpiryPresentation(); return true; } // Includes the NewMode confirmation path and delayed finish responses. // Keep the level transition waiting while the result and overview are displayed. await CloudRiseView.playElimination(host); if (CloudRiseService.elimination() || CloudRiseService.unseenResult()) { MiniGameSdk.API.showToast("活动结算尚未展示完成,请重试"); return false; } } catch (error) { cc.warn(error); return true; } return true; } /** Network delays never hold the main level settlement indefinitely. */ static async playElimination(host: cc.Node): Promise { const entry = host && host.getChildByName("CloudRiseEntry"); const view = entry && entry.getComponent(CloudRiseView); if (!view || !CloudRiseService.run() || !CloudRiseService.elimination() && !CloudRiseService.unseenResult() && CloudRiseService.run().status !== "playing") return; let timer: any; try { await Promise.race([CloudRiseService.sync(), new Promise(resolve => { timer = setTimeout(resolve, 2500); })]); if (CloudRiseService.challengeExpired()) { CloudRiseService.dismissExpiryPresentation(); return; } while (cc.isValid(view.node, true) && (CloudRiseService.elimination() || CloudRiseService.unseenResult())) { const item = CloudRiseService.elimination(), resultKey = CloudRiseService.resultKey(); await view.showElimination(); if (item ? CloudRiseService.elimination() && CloudRiseService.elimination().key === item.key : CloudRiseService.unseenResult() && CloudRiseService.resultKey() === resultKey) break; } } catch (error) { cc.warn(error); } finally { clearTimeout(timer); } } static mount(host: cc.Node, play: () => void, map?: any): CloudRiseView { const existing = host.getChildByName("CloudRiseEntry"); if (existing) return existing.getComponent(CloudRiseView); const node = new cc.Node("CloudRiseEntry"); host.addChild(node); const view = node.addComponent(CloudRiseView); view.host = host; view.play = play; view.map = map || null; view.initialize(); return view; } private initialize(): void { this.node.zIndex = 0; const home: any = !this.map && this.host.getComponent("JiaZai"); const load = !this.map && this.host.getChildByName("Load"), top = load && load.getChildByName("Top"); this.entry = new cc.Node("cloudRise"); (top || this.node).addChild(this.entry); this.entry.active = false; if (home && home.startUI2) { const sprite = this.entry.addComponent(cc.Sprite); sprite.spriteFrame = home.startUI2.getSpriteFrame("cloudRise.png") || home.startUI2.getSpriteFrame("cloudRise"); sprite.sizeMode = cc.Sprite.SizeMode.RAW; const button = this.entry.addComponent(cc.Button); button.transition = cc.Button.Transition.SCALE; button.zoomScale = 1.1; } const gift = top && top.getChildByName("xinshou"); if (gift) { // Reuse the lucky-gift clock artwork and typography, aligned to the icon's bottom edge. ["New Sprite", "time"].forEach(name => { const source = gift.getChildByName(name); if (!source) return; const node = cc.instantiate(source); this.entry.addChild(node); node.active = true; node.y += (gift.height - this.entry.height) / 2; if (name === "time") { this.entryCountdown = node.getComponent(cc.Label); this.entryCountdown.string = "00:00:00"; } }); } this.entry.on("click", () => { void this.open(); }); this.unsubscribe = CloudRiseService.subscribe(() => this.refresh()); if (!this.map) this.unsubscribeSchedule = ActivityScheduleService.subscribe(() => this.tick()); this.schedule(this.tick, 1); cc.game.on(cc.game.EVENT_SHOW, this.onShow, this); this.onShow(); } private onShow = (): void => { this.nextSyncAt = 0; this.tick(); }; private tick = (): void => { if (this.expiryPending) { if (!this.busy && Date.now() >= this.nextSyncAt) void this.showExpiry(); return; } this.refresh(); const period = !this.map && ActivityScheduleService.active("cloudRise"); if (period && this.entry.active) CloudRiseService.preloadPublicEntry(period); // Opening a page explicitly syncs its details. Idle home only polls unfinished personal work. if (!this.overlay && !CloudRiseService.needsPersonalSync()) { this.timeState = ""; this.nextSyncAt = 0; return; } const run = CloudRiseService.run(); const state = (run ? run.id + ":" + run.status : "pending") + ":" + CloudRiseService.challengeExpired(); if (state !== this.timeState || Date.now() >= this.nextSyncAt) { this.timeState = state; this.nextSyncAt = Date.now() + 30000; void CloudRiseService.sync(); } }; private refresh(): void { if (!this.node || !cc.isValid(this.node)) return; if (!this.map || !this.overlay) CloudRiseService.dismissExpiryPresentation(); this.entry.active = !this.map && CloudRiseService.homeEntryVisible(ActivityScheduleService.active("cloudRise"), ActivityScheduleService.hasSchedule(), ActivityScheduleService.now()); this.updateEntryCountdown(); 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"); if (home && home.updateCoin) home.updateCoin(); } const elimination = CloudRiseService.elimination(); const pendingLoss = CloudRiseService.hasPendingLoss(); const eliminationKey = elimination ? elimination.key : pendingLoss ? "pendingLoss" : ""; if (!this.map && eliminationKey && !this.overlay && !this.queuedElimination) { this.queuedElimination = eliminationKey; HomePopupQueue.get(this.host).enqueue("cloudRiseElimination", 0, done => { // Reserve the first popup while the abandon response is still in flight. void (async () => { try { if (CloudRiseService.hasPendingLoss()) await CloudRiseService.sync(); if (cc.isValid(this.node, true)) await this.showElimination(); } finally { this.queuedElimination = ""; done(); } })().catch(error => cc.warn(error)); }); } const key = CloudRiseService.resultKey(); if (!this.map && !elimination && !pendingLoss && !this.overlay && CloudRiseService.allowsAutomaticResults() && CloudRiseService.unseenResult() && key !== this.queuedResult) { this.queuedResult = key; HomePopupQueue.get(this.host).enqueue("cloudRiseResult", 1, done => { this.popupDone = done; void this.open("result").then(() => { if (!this.overlay) this.finishPopup(); }, () => this.finishPopup()); }); } } private async showElimination(reuseCurrent = false): Promise { if (this.busy || this.overlay && !reuseCurrent) return; // Recheck when a queued home popup runs: timeout may have arrived after enqueue. if (!this.map) CloudRiseService.dismissExpiryPresentation(); if (!reuseCurrent && CloudRiseService.challengeExpired()) { CloudRiseService.dismissExpiryPresentation(); return; } const item = CloudRiseService.elimination(); if (!item && !CloudRiseService.unseenResult()) return; this.busy = true; let loadTimer: any; try { const reuse = reuseCurrent && this.overlay && this.panel; if (!reuse) { const resources = Promise.all([CloudRiseService.pagePrefab(), CloudRiseService.pageFragment("stage", item ? item.after.stage : CloudRiseService.stage().stage)]).then(([prefab]) => prefab); const prefab = await Promise.race([resources, new Promise((_, reject) => { loadTimer = setTimeout(() => reject(new Error("百人赛动画资源待加载")), 2500); })]); clearTimeout(loadTimer); if (!cc.isValid(this.node, true)) return; if (CloudRiseService.challengeExpired()) { CloudRiseService.dismissExpiryPresentation(); return; } if (!this.map) { CloudRiseService.dismissExpiryPresentation(); if (!item && !CloudRiseService.unseenResult()) return; } this.overlay = cc.instantiate(prefab); // Settings and other dynamic popups use MAX_ZINDEX - 2; settlement must cover them. this.host.addChild(this.overlay); this.overlay.zIndex = cc.macro.MAX_ZINDEX - 1; this.panel = this.overlay.getComponent("CloudRisePanel"); } await this.panel.preparePage(item ? "elimination" : "result", item && item.after.stage); if (!cc.isValid(this.node, true)) return; const close = () => { this.panel.acknowledge(); if (this.eliminationDone) this.eliminationDone(); }; if (reuse) this.panel.beginSettlement(item ? "elimination" : "result", close); else this.panel.initialize(item ? "elimination" : "result", !!this.map, close, () => {}); await new Promise(resolve => { this.eliminationDone = resolve; // The cloud motion may already be acknowledged while its stage result is still unseen. if (!item) return; this.panel.playElimination(item).then(completed => { if (completed && cc.isValid(this.node, true)) CloudRiseService.acknowledgeElimination(item.key); const stage = CloudRiseService.stage(); if (completed && cc.isValid(this.node, true) && ["won", "lost", "expired"].includes(item.after.status) && stage && stage.stage === item.after.stage && (stage.status === item.after.status || item.after.status === "expired" && CloudRiseService.challengeExpired()) && CloudRiseService.unseenResult()) { this.panel.openResult(); } else if (completed && cc.isValid(this.node, true)) { this.panel.waitForProgressContinue().then(() => resolve()); } else resolve(); }).catch(error => { cc.warn(error); resolve(); }); }); } catch (error) { cc.warn(error); } finally { clearTimeout(loadTimer); this.eliminationDone = null; if (this.overlay && cc.isValid(this.overlay)) this.overlay.destroy(); this.overlay = null; this.panel = null; this.busy = false; this.queuedElimination = ""; } } async open(mode?: string): Promise { if (this.busy || this.overlay) return; this.busy = true; try { const synced = await CloudRiseService.sync(); if (!cc.isValid(this.node, true)) return; if (!this.map) { CloudRiseService.dismissExpiryPresentation(); if (mode === "result" && !CloudRiseService.unseenResult()) return; } if (!synced && mode !== "result") { MiniGameSdk.API.showToast(CloudRiseService.error || "活动信息待同步,请重试"); return; } const canOpen = () => CloudRiseService.visible() || mode === "result" && CloudRiseService.unseenResult(); if (!canOpen()) { MiniGameSdk.API.showToast("活动未开启"); return; } const locked = () => { const level = CloudRiseService.lockedUntilLevel(); if (level) MiniGameSdk.API.showToast(`月光宝盒活动通过第${level}关后解锁`); return !!level; }; if (locked()) return; const prefab = await CloudRiseService.pagePrefab(); const pageMode = mode || (CloudRiseService.unseenResult() ? "result" : CloudRiseService.run() && CloudRiseService.run().status === "playing" ? "progress" : "overview"); if (pageMode !== "overview") await CloudRiseService.pageFragment("stage", CloudRiseService.stage().stage); if (!this.map) CloudRiseService.dismissExpiryPresentation(); if (!cc.isValid(this.node, true) || !canOpen()) return; if (locked()) return; this.overlay = cc.instantiate(prefab); this.host.addChild(this.overlay); this.overlay.zIndex = cc.macro.MAX_ZINDEX - 1; this.panel = this.overlay.getComponent("CloudRisePanel"); await this.panel.preparePage(pageMode); if (!cc.isValid(this.node, true)) return; if (this.map) { this.priorPause = this.map.pause; this.map.pause = true; } this.panel.initialize(pageMode, !!this.map, () => this.close(), () => { this.close(); if (!this.map) this.play(); }, () => { this.expiryPending = true; void this.showExpiry(); }); } catch (error) { if (this.overlay && cc.isValid(this.overlay)) this.overlay.destroy(); this.overlay = null; this.panel = null; MiniGameSdk.API.showToast("活动资源加载失败,请重试"); cc.warn(error); } finally { this.busy = false; } } /** Home closes quietly; an open in-game stage keeps its existing timeout presentation. */ private async showExpiry(): Promise { if (this.busy) return; if (!this.map) { this.expiryPending = false; this.close(); this.refresh(); void CloudRiseService.sync(); return; } this.busy = true; this.nextSyncAt = Date.now() + 30000; this.queuedElimination = "expiry"; try { if (!await CloudRiseService.sync() || !cc.isValid(this.node, true)) return; this.busy = false; await this.showElimination(true); this.expiryPending = !!(CloudRiseService.elimination() || CloudRiseService.unseenResult() || CloudRiseService.run() && CloudRiseService.run().status !== "expired" && CloudRiseService.challengeExpired()); } catch (error) { cc.warn(error); } finally { this.busy = false; this.queuedElimination = ""; if (!this.overlay) { if (this.map && cc.isValid(this.map.node) && !this.map.gameOver && !this.map.gameWin) this.map.pause = this.priorPause; this.finishPopup(); } } } private close(): void { if (this.busy) return; if (this.panel) this.panel.acknowledge(); if (this.overlay) this.overlay.destroy(); this.overlay = null; this.panel = null; this.finishPopup(); if (this.map && cc.isValid(this.map.node) && !this.map.gameOver && !this.map.gameWin) this.map.pause = this.priorPause; } private updateEntryCountdown(): void { if (!this.entryCountdown) return; const period = ActivityScheduleService.active("cloudRise"), run = CloudRiseService.run(); const now = ActivityScheduleService.hasSchedule() ? ActivityScheduleService.now() : CloudRiseService.now(); const activeRun = run && ["playing", "waiting"].includes(run.status) && now < run.expiresAt; const end = activeRun ? run.expiresAt : period ? period.endsAt : 0; const seconds = Math.max(0, Math.ceil((end - now) / 1000)); const pad = (value: number) => value < 10 ? "0" + value : String(value); this.entryCountdown.string = [Math.floor(seconds / 3600), Math.floor(seconds / 60) % 60, seconds % 60] .map(pad).join(":"); } onDestroy(): void { cc.game.off(cc.game.EVENT_SHOW, this.onShow, this); this.unschedule(this.tick); if (this.unsubscribe) this.unsubscribe(); if (this.unsubscribeSchedule) this.unsubscribeSchedule(); if (this.entry && cc.isValid(this.entry)) this.entry.destroy(); if (this.overlay && cc.isValid(this.overlay)) this.overlay.destroy(); if (this.eliminationDone) this.eliminationDone(); this.finishPopup(); } private finishPopup(): void { const done = this.popupDone; this.popupDone = null; if (done) done(); } }