import PopupAnimator, { ActivityPopupAnimationType as PopupType } from "./ActivityPopupAnimator"; import Service from "./module/Config/PyramidService"; import ActivityScheduleService, { PyramidDailyReminder } from "./module/Config/ActivityScheduleService"; import HomePopupQueue from "../home_popup_queue/HomePopupQueue"; import LoadingCatAnimation from "./LoadingCatAnimation"; const { ccclass } = cc._decorator; @ccclass export default class PyramidView extends cc.Component { private entry: cc.Node; private overlay: cc.Node = null; private unsubscribe: () => void; private unsubscribeSchedule: () => void; private needsCheck = true; private checking = false; private opening = false; private deliveryQueued = false; private stopPaymentLoading: () => void = null; private dailyReminderQueued = false; private attemptedDailyReminder = ""; private finishReminder: () => void = null; private entryClock = 0; private releaseResources: () => void = null; private settlementQueued = false; private settlementRoot: cc.Node = null; private finishSettlement: () => void = null; private settlementRetries = 0; private settlementRetryPending = false; private boundary = ""; private account = ""; static mount(host: cc.Node): void { if (!host.getComponent(PyramidView)) host.addComponent(PyramidView); } onLoad(): void { const load = this.node.getChildByName("Load"), top = load && load.getChildByName("Top"); this.entry = new cc.Node("pyramid"); (top || this.node).addChild(this.entry); this.entry.active = false; const goldMinerEntry = top && top.getChildByName("CoinMadnessEntry"); for (const name of ["timeBg", "time", "red"]) { const template = goldMinerEntry && goldMinerEntry.getChildByName(name); if (template) { const child = cc.instantiate(template); this.entry.addChild(child); child.active = false; } } const button = this.entry.addComponent(cc.Button); button.transition = cc.Button.Transition.SCALE; this.entry.on("click", () => { void this.open(); }); this.unsubscribe = Service.subscribe(() => { this.checkBoundary(); this.render(); this.syncPaymentLoading(); }); this.unsubscribeSchedule = ActivityScheduleService.subscribe(() => this.render()); cc.game.on(cc.game.EVENT_SHOW, this.wake, this); this.checkBoundary(); this.wake(); } onEnable(): void { this.wake(); } private wake = (): void => { this.needsCheck = true; this.attemptedDailyReminder = ""; this.settlementRetries = 0; this.settlementRetryPending = false; this.unschedule(this.retrySettlement); }; private retrySettlement = (): void => { this.settlementRetryPending = true; }; private retrySettlementLater(): void { const delays = [2, 3, 5, 7, 10, 15, 25, 30, 50]; if (!cc.isValid(this.node, true) || this.settlementRetries >= delays.length) return; this.unschedule(this.retrySettlement); this.scheduleOnce(this.retrySettlement, delays[this.settlementRetries++]); } private checkBoundary(): void { const gm = cc.fx.GameConfig.GM_INFO, account = String(gm.uid || "") + ":" + String(gm.token || ""); if (this.account !== account) { this.account = account; this.boundary = ""; this.wake(); } const state = Service.data(); if (!state) return; const now = Service.now(), phase = now >= state.endAt ? "ended" : now >= state.startAt ? "started" : "waiting"; const boundary = state.periodId + ":" + phase; if (this.boundary && this.boundary !== boundary) this.wake(); this.boundary = boundary; } private closeOverlay = (): void => { if (this.overlay && cc.isValid(this.overlay, true)) this.overlay.destroy(); this.overlay = null; if (this.releaseResources) { this.releaseResources(); this.releaseResources = null; } if (this.finishReminder) { const done = this.finishReminder; this.finishReminder = null; done(); } }; private render(): void { const period = ActivityScheduleService.active("pyramid"); const s = Service.data(), ended = (ActivityScheduleService.hasSchedule() && !period && !s) || !!s && (s.status !== "active" || Service.now() >= s.endAt); if (this.settlementRoot && !Service.settlements().length && this.finishSettlement) this.finishSettlement(); this.entry.active = !!period && Number(cc.fx.GameConfig.GM_INFO.level) >= period.unlockLevel && !(s && s.periodId === period.periodId && s.completed); if (ended) this.closeOverlay(); const home: any = this.node.getComponent("JiaZai"); if (this.entry.active && !this.entry.getComponent(cc.Sprite) && home && home.startUI2) { const frame = home.startUI2.getSpriteFrame("pyramid.png") || home.startUI2.getSpriteFrame("pyramid"); if (frame) { const sprite = this.entry.addComponent(cc.Sprite); sprite.spriteFrame = frame; sprite.sizeMode = cc.Sprite.SizeMode.RAW; } } const badge = this.entry.getChildByName("red"); if (badge) badge.active = this.entry.active && !!s && !!period && s.periodId === period.periodId && ((s.pendingGrants || []).length > 0 || (s.levels || []).some(level => (level.rewards || []).some(reward => reward.deliveryStatus === "unclaimed" || reward.deliveryStatus === "pending"))); this.refreshCountdown(); if (home && home.updateCoin) home.updateCoin(); } private refreshCountdown(): void { const period = ActivityScheduleService.active("pyramid"); const remaining = period ? Math.max(0, Math.ceil((period.endsAt - ActivityScheduleService.now()) / 1000)) : 0; const time = this.entry.getChildByName("time"), background = this.entry.getChildByName("timeBg"); if (background) background.active = remaining > 0; if (!time) return; time.active = remaining > 0; const label = time.getComponent(cc.Label), pad = (n: number) => n < 10 ? "0" + n : String(n); if (label) label.string = !remaining ? "" : remaining >= 86400 ? Math.floor(remaining / 86400) + "天" + Math.floor(remaining % 86400 / 3600) + "时" : pad(Math.floor(remaining / 3600)) + ":" + pad(Math.floor(remaining % 3600 / 60)) + ":" + pad(remaining % 60); } private reminderCurrent(reminder: PyramidDailyReminder): boolean { const current = ActivityScheduleService.pyramidReminder(), state = Service.data(); return !!(cc.isValid(this.node, true) && this.entry.active && !Service.deliveryBusy && !(state && state.pendingGrants && state.pendingGrants.length) && current && current.key === reminder.key && current.day === reminder.day && current.periodId === reminder.periodId); } private queueDailyReminder(): void { if (!HomePopupQueue.get(this.node).canCollect('pyramidDailyReminder')) return; if (!this.entry.active || this.overlay || this.opening || this.dailyReminderQueued) return; const reminder = ActivityScheduleService.pyramidReminder(); if (!reminder) return; const attemptKey = JSON.stringify([reminder.key, reminder.day, reminder.periodId]); if (this.attemptedDailyReminder === attemptKey) return; this.dailyReminderQueued = true; HomePopupQueue.get(this.node).enqueue("pyramidDailyReminder", 11, done => { this.dailyReminderQueued = false; if (!this.reminderCurrent(reminder) || this.overlay || this.opening || Service.busy || this.deliveryQueued) { done(); return; } // Automatic failures remain unconsumed and retry on the next login. this.attemptedDailyReminder = attemptKey; this.finishReminder = done; void this.open(reminder); }); } private async open(reminder?: PyramidDailyReminder): Promise { const alive = () => cc.isValid(this, true) && cc.isValid(this.node, true); if (!alive() || this.overlay || this.opening || this.settlementQueued) return; this.opening = true; // Prepare automatic reminders silently; only an explicit click shows Loading. const stopLoading = reminder ? () => { } : this.startLoading(); let release: () => void = null; const current = () => alive() && this.releaseResources === release; try { const unretain = await Service.acquireResources(); let released = false; release = () => { if (!released) { released = true; unretain(); } }; if (!alive()) { release(); return; } this.releaseResources = release; const [synced, prefab] = await Promise.all([Service.sync(), Service.prefab()]); if (!current()) return; const state = Service.data(); if (!synced && !reminder && typeof window !== "undefined" && (window as any).wx) (window as any).wx.showToast({ title: Service.error || "活动信息暂时无法获取", icon: "none" }); if (!synced || !cc.isValid(this.node, true) || !this.entry.active || !state || state.status !== "active" || Service.now() >= state.endAt || state.completed) return; if (reminder && (!this.reminderCurrent(reminder) || !HomePopupQueue.get(this.node).canPresent('pyramidDailyReminder'))) return; this.overlay = cc.instantiate(prefab); this.overlay.name = "PyramidPanel"; this.overlay.active = false; this.node.addChild(this.overlay); this.overlay.zIndex = 2000; const panel: any = this.overlay.getComponent("PyramidPanel"); await panel.initialize(this.closeOverlay); if (!current()) return; if (!cc.isValid(panel, true) || !cc.isValid(this.overlay, true)) { this.closeOverlay(); return; } if (reminder && (!this.reminderCurrent(reminder) || !HomePopupQueue.get(this.node).canPresent('pyramidDailyReminder'))) { this.closeOverlay(); return; } if (this.overlay && cc.isValid(this.overlay, true)) { this.overlay.active = true; if (reminder) HomePopupQueue.get(this.node).markShown('pyramidDailyReminder'); } } catch (error) { console.error("[Pyramid] 界面加载失败", error); if (alive()) { cc.warn("[Pyramid] 界面加载失败", error); this.closeOverlay(); } } finally { stopLoading(); if (release && (!alive() || !this.overlay)) release(); if (alive()) { this.opening = false; this.syncPaymentLoading(); if (!this.overlay && this.releaseResources) { this.releaseResources(); this.releaseResources = null; } if (!this.overlay && this.finishReminder) this.closeOverlay(); } } } private startLoading(): () => void { const template = this.node.getChildByName("Loading"); if (!template || !template.parent) return () => { }; const parent = template.parent, size = parent.getContentSize(); const factor = Math.min(1, size.width / 1080, size.height / 1920); const viewport = new cc.Node("PyramidLoading"); viewport.setContentSize(size.width / factor, size.height / factor); viewport.scale = factor; parent.addChild(viewport); viewport.zIndex = cc.macro.MAX_ZINDEX; viewport.addComponent(cc.BlockInputEvents); const loading = cc.instantiate(template); viewport.addChild(loading); LoadingCatAnimation.play(loading); return () => { if (!cc.isValid(viewport, true)) return; LoadingCatAnimation.stop(loading); viewport.destroy(); }; } private syncPaymentLoading(): void { if (Service.paymentConfirming && !this.stopPaymentLoading && !this.opening) this.stopPaymentLoading = this.startLoading(); else if (!Service.paymentConfirming && this.stopPaymentLoading) { this.stopPaymentLoading(); this.stopPaymentLoading = null; } } private async check(): Promise { this.checking = true; try { this.queueSettlement(); await Service.sync(); if (this.overlay && cc.isValid(this.overlay, true)) { const panel: any = this.overlay.getComponent("PyramidPanel"); if (panel) await panel.recoverDelivery(); } if (!this.overlay && !this.deliveryQueued && await Service.pendingDelivery() && cc.isValid(this.node, true)) { this.deliveryQueued = true; HomePopupQueue.get(this.node).enqueue("pyramidDelivery", 5, done => { if (!cc.isValid(this.node, true)) { done(); return; } // A short-lived input shield keeps unrelated gameplay inventory writes out of this save window. const shield = new cc.Node("PyramidDelivery"); this.node.addChild(shield); shield.zIndex = 2500; shield.setContentSize(cc.view.getVisibleSize()); shield.addComponent(cc.BlockInputEvents); const finish = () => { if (cc.isValid(shield, true)) shield.destroy(); this.deliveryQueued = false; done(); this.render(); }; void Service.deliver().then(finish, finish); }); } } catch (_) { /* Network errors retry on the next bounded refresh, never prevent login. */ } finally { this.checking = false; if (cc.isValid(this.node, true)) { this.queueDailyReminder(); HomePopupQueue.get(this.node).completePreparation('pyramid'); } } } private queueSettlement(): void { if (!HomePopupQueue.get(this.node).canCollect('pyramidSettlement')) return; if (this.settlementQueued || !cc.isValid(this.node, true)) return; this.settlementQueued = true; HomePopupQueue.get(this.node).enqueue("pyramidSettlement", 4, done => { void this.showSettlements(done); return () => { if (this.finishSettlement) this.finishSettlement(); }; }); } private async showSettlements(done: () => void): Promise { if (!cc.isValid(this.node, true)) { this.settlementQueued = false; done(); return; } let release: () => void = null, finished = false; const shield = new cc.Node("PyramidSettlementLoading"); this.node.addChild(shield); shield.zIndex = 2199; shield.setContentSize(cc.view.getVisibleSize()); shield.addComponent(cc.BlockInputEvents); const finish = () => { if (finished) return; finished = true; if (this.settlementRoot && cc.isValid(this.settlementRoot, true)) this.settlementRoot.destroy(); if (cc.isValid(shield, true)) shield.destroy(); this.settlementRoot = null; this.finishSettlement = null; if (release) { release(); release = null; } this.settlementQueued = false; done(); }; this.finishSettlement = finish; try { const settled = await Service.readSettlements(); if (!settled) this.retrySettlementLater(); if (finished || !cc.isValid(this.node, true) || !HomePopupQueue.get(this.node).isRunning('pyramidSettlement') || !Service.settlements().length) { finish(); return; } const lease = await Service.acquireResources(); if (finished || !cc.isValid(this.node, true)) { lease(); finish(); return; } release = lease; const bundle = await Service.bundle(); const prefab: cc.Prefab = await new Promise((resolve, reject) => Service.loadAsset(bundle, "prefab/PyramidSettlement", cc.Prefab, (error, asset: cc.Prefab) => error ? reject(error) : resolve(asset))); const next = () => { if (finished || !cc.isValid(this.node, true) || !HomePopupQueue.get(this.node).isRunning('pyramidSettlement')) { finish(); return; } const notice = Service.settlements()[0]; if (!notice) { finish(); return; } if (this.settlementRoot && cc.isValid(this.settlementRoot, true)) this.settlementRoot.destroy(); this.settlementRoot = cc.instantiate(prefab); this.node.addChild(this.settlementRoot); this.settlementRoot.zIndex = 2200; const panel: any = this.settlementRoot.getComponent("PyramidSettlement"); let dismissed = false; panel.show(notice, async () => { if (dismissed || finished || !cc.isValid(this.node, true)) return; dismissed = true; await new Promise(resolve => PopupAnimator.close(this.settlementRoot.getChildByName("panel"), PopupType.Bounce, resolve)); if (finished || !cc.isValid(this.node, true)) return; this.settlementRoot.active = false; this.settlementRoot.destroy(); this.settlementRoot = null; Service.dismissSettlement(notice.noticeId); next(); }); HomePopupQueue.get(this.node).markShown('pyramidSettlement'); }; next(); } catch (error) { cc.warn("[Pyramid] 补发清单弹窗加载失败", error); finish(); } } update(dt = 0): void { this.syncPaymentLoading(); this.entryClock += dt; if (this.entryClock >= .5) { this.entryClock = 0; this.checkBoundary(); this.refreshCountdown(); this.queueDailyReminder(); } const state = Service.data(); if ((this.entry.active || this.overlay) && (!state || state.status !== "active" || Service.now() >= state.endAt)) this.render(); if (!this.checking && !Service.busy && !this.settlementQueued && this.settlementRetryPending) { this.settlementRetryPending = false; if (!this.needsCheck) this.queueSettlement(); } if (this.checking || Service.busy || !this.needsCheck) return; this.needsCheck = false; void this.check(); } onDestroy(): void { this.unschedule(this.retrySettlement); if (this.finishSettlement) this.finishSettlement(); this.closeOverlay(); if (this.unsubscribe) this.unsubscribe(); cc.game.off(cc.game.EVENT_SHOW, this.wake, this); if (this.unsubscribeSchedule) this.unsubscribeSchedule(); if (this.stopPaymentLoading) this.stopPaymentLoading(); if (this.entry && cc.isValid(this.entry, true)) this.entry.destroy(); } }