import { MiniGameSdk } from '../Sdk/MiniGameSdk'; import HomePopupQueue from '../../home_popup_queue/HomePopupQueue'; import GoldMinerService from './GoldMinerService'; import Utils from '../module/Pay/Utils'; const { ccclass } = cc._decorator; const BUNDLE = 'coin_madness'; /** Resident bridge only: never import the bundle's UI class or preload its assets. */ @ccclass export default class CoinMadnessHost extends cc.Component { private static occupied = false; private bundle: cc.AssetManager.Bundle = null; private panel: cc.Node = null; private help: cc.Node = null; private entry: cc.Node = null; private entryOwned = false; private loading = false; private releasing = false; private requestId = 0; private finishQueue: () => void = null; private alive = true; private switchingToSettlement = false; private service: GoldMinerService = null; private syncing = false; private noticeOpened = false; private syncRetries = 0; private boundary = ''; private pendingCoinAnimations = 0; isCoinDisplayHeld() { return this.pendingCoinAnimations > 0; } isPopupActive() { return this.loading || this.switchingToSettlement || !!this.panel; } private rewardArrived() { this.pendingCoinAnimations = Math.max(0, this.pendingCoinAnimations - 1); this.refreshCoinDisplay(); } private recoveryRetry = false; private entryLoading = false; private setEntryLoading(visible: boolean) { if (this.entryLoading === visible) return; this.entryLoading = visible; const home = this.node && this.node.getComponent('JiaZai'); if (home) { if (visible) home.openLoad(); else home.closeLoad(); } } private retrySync = () => { this.sync(this.recoveryRetry); }; private resumeSync = () => { this.syncRetries = 0; this.unschedule(this.retrySync); this.sync(); }; start() { this.service = GoldMinerService.current(); this.updateEntry(); this.schedule(this.refreshEntryBadge, .5); this.resumeSync(); cc.game.on(cc.game.EVENT_SHOW, this.resumeSync, this); cc.game.on(cc.game.EVENT_HIDE, this.pausePayments, this); } private pausePayments() { if (this.service) this.service.stopPolling(); } private async sync(recoveryOnly = false) { if (this.syncing || !this.alive) return; this.unschedule(this.retrySync); this.syncing = true; try { const service = GoldMinerService.current(); if (this.service && this.service !== service) { this.close(); this.noticeOpened = false; } this.service = service; if (!this.service) { this.updateEntry(); return; } const first = recoveryOnly ? Promise.resolve() : this.service.readForDisplay(); first.then(() => { if (this.alive && this.service === service) this.updateEntry(); }, () => undefined); const data = await (recoveryOnly ? this.service.retryRecovery() : this.service.refresh()); if (!this.alive) return; this.updateEntry(); const panel = this.panel && this.panel.getComponent('CoinMadnessPanel'); if (panel) panel.render(data); this.showSettlementNotice(); if (this.service.refreshNeedsRetry) this.retryLater(true); } catch (error) { this.retryLater(); cc.warn('活动同步失败', error.message); if (this.alive && this.panel) this.toast(error.message); } finally { this.refreshCoinDisplay(); this.syncing = false; } } private retryLater(recoveryOnly = false) { this.recoveryRetry = recoveryOnly; const delays = [2, 3, 5, 7, 10, 15, 25, 30, 50]; if (!this.alive || this.syncRetries >= delays.length) return; this.unschedule(this.retrySync); this.scheduleOnce(this.retrySync, delays[this.syncRetries++]); } private showSettlementNotice() { if (!this.alive || !this.service) return; if (this.service.settlementNotice <= 0) { this.noticeOpened = false; return; } if (this.panel && this.panel.getComponent('CoinMadnessPanel')) { this.openSettlement({ state: { settlementCoins: this.service.settlementNotice } }); } else if (!this.panel && !this.noticeOpened && !this.loading && !this.releasing) { HomePopupQueue.get(this.node).enqueue('coinMadnessSettlement', 55, done => { if (!this.alive || !this.service || this.service.settlementNotice <= 0 || this.isPopupActive() || this.releasing || this.noticeOpened) { done(); return; } this.finishQueue = done; this.load(); }); } } private refreshCoinDisplay() { if (!this.alive || !cc.isValid(this.node) || this.isCoinDisplayHeld()) return; const home = this.node.getComponent('JiaZai'); if (home) home.updateCoin(); } private async requestActivity(service: GoldMinerService, action: string, body: any) { if (action === 'read') { const panel = this.panel, requestId = this.requestId; const current = () => this.alive && this.service === service && this.panel === panel && this.requestId === requestId && GoldMinerService.current() === service; const ready = service.readForDisplay(); service.refresh().then(data => { if (!current()) return; const component = panel && panel.getComponent('CoinMadnessPanel'); if (component) component.render(data); this.updateEntry(); this.refreshCoinDisplay(); this.showSettlementNotice(); if (service.refreshNeedsRetry) this.retryLater(true); }, error => { if (current()) { this.retryLater(); cc.warn('活动恢复失败', error.message); } }); let data; try { data = await ready; } finally { if (current()) this.setEntryLoading(false); } return current() ? { code: 1, data, msg: '' } : { code: 0, msg: '活动已关闭或账号已切换' }; } const panel = this.panel, requestId = this.requestId; let animated = false; if (action === 'claim') this.pendingCoinAnimations++; try { const result = await service.request(action, body, () => { if (action !== 'claim' || animated || !this.alive || this.panel !== panel || this.requestId !== requestId) return; const component = panel && panel.getComponent('CoinMadnessPanel'); if (!component) return; animated = true; component.rewardSaved(body.stage); }); if (action === 'claim' && !animated && this.requestId === requestId) this.rewardArrived(); return result; } catch (error) { if (action === 'claim' && !animated && this.requestId === requestId) this.rewardArrived(); throw error; } finally { this.refreshCoinDisplay(); this.showSettlementNotice(); if (service.refreshNeedsRetry) this.retryLater(true); } } private updateEntry() { const info = this.service && this.service.info; const currentPeriod = info && this.service.serverNow < info.endsAt; const visible = !!(this.service && (this.service.settlementNotice > 0 || currentPeriod && (this.service.hasBadge || cc.fx.GameConfig.GM_INFO.canIos && ['purchasable', 'purchase_disabled', 'unlocked'].indexOf(info.status) >= 0))); const sceneEntry = cc.find('Load/Top/CoinMadnessEntry', this.node); if (!visible) { if (sceneEntry) sceneEntry.active = false; if (this.entry) this.entry.active = false; this.layoutEntry(); return; } if (!this.entry) { this.entry = sceneEntry; if (!this.entry) { this.entryOwned = true; this.entry = new cc.Node('CoinMadnessEntry'); cc.find('Load/Top', this.node).addChild(this.entry); this.entry.setContentSize(184, 191); const sprite = this.entry.addComponent(cc.Sprite); sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; cc.resources.load('coin_madness_logo', cc.SpriteFrame, (error, frame: cc.SpriteFrame) => { if (!this.alive || !cc.isValid(sprite)) return; if (error) { cc.warn('Coin Madness entry logo unavailable', error); return; } sprite.spriteFrame = frame; }); const button = this.entry.addComponent(cc.Button); button.transition = cc.Button.Transition.SCALE; button.duration = .1; button.zoomScale = 1.1; const red = new cc.Node('red'); this.entry.addChild(red); red.setPosition(59, 61); const badge = red.addComponent(cc.Graphics); badge.fillColor = cc.Color.RED; badge.circle(0, 0, 13); badge.fill(); red.active = false; } this.entry.on('click', this.open, this); } this.entry.active = true; this.layoutEntry(); this.refreshEntryBadge(); } private layoutEntry() { const home = this.node.getComponent('JiaZai'); if (home) home.refreshHomeActivityEntryLayout(); } private refreshEntryBadge() { if (!this.syncing) { const service = GoldMinerService.current(); if (service !== this.service) { this.boundary = ''; this.resumeSync(); } else if (service && service.info) { const info = service.info, now = service.serverNow; const phase = now >= info.endsAt ? 'ended' : now >= info.startsAt ? 'started' : 'waiting'; const boundary = info.periodId + ':' + phase; if (this.boundary && this.boundary !== boundary) { this.boundary = boundary; this.resumeSync(); } else this.boundary = boundary; } } if (!this.entry || !cc.isValid(this.entry)) return; const timeNode = this.entry.getChildByName('time'); const timeBg = this.entry.getChildByName('timeBg'); const info = this.service && this.service.info; const remaining = info ? Math.max(0, Math.ceil((info.endsAt - this.service.serverNow) / 1000)) : 0; if (timeBg) timeBg.active = remaining > 0; if (timeNode) { const label = timeNode.getComponent(cc.Label); timeNode.active = remaining > 0; const 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); } const button = this.entry.getComponent(cc.Button); if (button) button.interactable = !this.loading && !this.releasing && !this.panel; const red = this.entry.getChildByName('red'); if (!red) return; red.active = !!(this.service && this.service.hasBadge); } open() { if (!this.alive || !this.service || this.loading || this.releasing || this.switchingToSettlement || this.panel) return; this.load(); } private load() { if (CoinMadnessHost.occupied) { this.close(); this.toast('活动资源释放中,请稍后重试'); return; } CoinMadnessHost.occupied = true; this.loading = true; this.setEntryLoading(true); const requestId = ++this.requestId; const service = this.service; // Fetch the first usable snapshot alongside assets, before mounting the panel. const ready = service ? service.readForDisplay().then(data => ({ data, error: null }), error => ({ data: null, error })) : Promise.resolve({ data: null, error: null }); const recovery = service ? service.refresh().then(data => ({ data, error: null }), error => ({ data: null, error })) : null; cc.assetManager.loadBundle(BUNDLE, (error, bundle) => { if (error) { CoinMadnessHost.occupied = false; this.loading = false; this.close(); this.toast('活动加载失败,请重试'); return; } if (!this.alive || requestId !== this.requestId) { this.release(bundle); return; } this.bundle = bundle; if (this.service && this.service.settlementNotice > 0) { this.loading = false; this.openSettlement({ state: { settlementCoins: this.service.settlementNotice } }); return; } bundle.load('prefab/CoinMadness', cc.Prefab, async (loadError, prefab: cc.Prefab) => { if (!this.alive || requestId !== this.requestId) { this.release(bundle); return; } if (loadError) { this.loading = false; this.close(); this.toast('活动资源加载失败,请重试'); return; } const initial = await ready; if (!this.alive || requestId !== this.requestId) { this.release(bundle); return; } this.loading = false; if (initial.error) { this.close(); this.toast(initial.error.message || '活动数据加载失败,请重试'); return; } // A background recovery may complete while the main prefab is loading. if (this.service && this.service.settlementNotice > 0) { this.openSettlement({ state: { settlementCoins: this.service.settlementNotice } }); return; } this.setEntryLoading(false); this.panel = cc.instantiate(prefab); this.node.addChild(this.panel); this.panel.zIndex = cc.macro.MAX_ZINDEX - 1; this.panel.getComponent('CoinMadnessPanel').initialize(initial.data, { initialReady: true, progressScope: Utils.serverUrl() + ':' + String(cc.fx.GameConfig.GM_INFO.uid), request: service ? (action, body) => this.requestActivity(service, action, body) : null, coinTarget: () => cc.find('Load/Top/Coin', this.node), rewardArrived: () => this.rewardArrived(), close: () => this.close(), expired: () => { if (this.service && this.service.settlementNotice > 0) { this.showSettlementNotice(); return; } this.close(); this.resumeSync(); }, openHelp: () => this.openHelp(), openSettlement: data => this.openSettlement(data), toast: (message: string) => this.toast(message), }); if (recovery) recovery.then(result => { if (!this.alive || requestId !== this.requestId || this.service !== service) return; if (result.error) { this.retryLater(); return; } const component = this.panel && this.panel.getComponent('CoinMadnessPanel'); if (component) component.render(result.data); this.updateEntry(); this.refreshCoinDisplay(); this.showSettlementNotice(); if (service.refreshNeedsRetry) this.retryLater(true); }); }); }); } private openSettlement(data: any) { if (!this.bundle || this.switchingToSettlement) return; this.switchingToSettlement = true; const bundle = this.bundle, requestId = this.requestId; bundle.load('prefab/CoinMadnessSettlement', cc.Prefab, (error, prefab: cc.Prefab) => { if (!this.alive || requestId !== this.requestId) return; this.switchingToSettlement = false; this.setEntryLoading(false); if (error) { if (!this.panel) this.close(); this.toast('结算面板加载失败,请重新打开活动'); return; } this.noticeOpened = true; this.pendingCoinAnimations = 0; this.refreshCoinDisplay(); this.clearHelp(); if (this.panel) { this.panel.removeFromParent(); this.panel.destroy(); } this.panel = cc.instantiate(prefab); this.node.addChild(this.panel); this.panel.zIndex = cc.macro.MAX_ZINDEX - 1; this.panel.getComponent('CoinMadnessSettlement').initialize(data, { real: !!this.service, close: () => { if (this.service) this.service.dismissNotice(); this.close(); this.updateEntry(); }, openHelp: () => this.openHelp(), }); }); } private openHelp() { if (!this.bundle || !this.panel || this.loading || this.help) return; const bundle = this.bundle, requestId = this.requestId; this.loading = true; bundle.load('prefab/CoinMadnessHelp', cc.Prefab, (error, prefab: cc.Prefab) => { this.loading = false; if (!this.alive || requestId !== this.requestId) { this.release(bundle); return; } if (error) { this.toast('说明加载失败,请重试'); return; } this.help = cc.instantiate(prefab); this.node.addChild(this.help); this.help.setContentSize(cc.view.getVisibleSize()); const content = this.help.getChildByName('Content'); content.scale = Math.min(this.help.width / content.width, this.help.height / content.height); this.help.zIndex = cc.macro.MAX_ZINDEX; // Match the pass description's staggered backOut entrance, preserving authored scales. this.helpAnimationParts().forEach((part, i) => { const scaleX = part.scaleX, scaleY = part.scaleY; part.setScale(0); cc.tween(part).delay((i + 1) * .3) .to(.3, { scaleX, scaleY }, { easing: 'backOut' }).start(); }); this.help.on(cc.Node.EventType.TOUCH_END, (event: cc.Event.EventTouch) => { event.stopPropagation(); this.clearHelp(); }); }); } private clearHelp() { if (!this.help) return; this.helpAnimationParts().forEach(part => cc.Tween.stopAllByTarget(part)); this.help.destroy(); this.help = null; } private helpAnimationParts(): cc.Node[] { const content = this.help && this.help.getChildByName('Content'); if (!content) return []; return content.children.reduce((parts, node) => parts.concat( node.name === 'Instructions' && node.children.length ? node.children : [node]), [] as cc.Node[]); } close() { this.setEntryLoading(false); this.pendingCoinAnimations = 0; this.refreshCoinDisplay(); this.pausePayments(); ++this.requestId; this.switchingToSettlement = false; const wasLoading = this.loading; this.loading = false; this.clearHelp(); if (this.panel) { this.panel.destroy(); this.panel = null; } const bundle = this.bundle; this.bundle = null; if (bundle && !wasLoading) this.release(bundle); const done = this.finishQueue; this.finishQueue = null; if (done) done(); } private release(bundle: cc.AssetManager.Bundle) { this.releasing = true; cc.director.once(cc.Director.EVENT_AFTER_DRAW, () => { bundle.releaseAll(); if (cc.assetManager.getBundle(BUNDLE) === bundle) cc.assetManager.removeBundle(bundle); this.releasing = false; CoinMadnessHost.occupied = false; }); } private toast(message: string) { if (this.alive) MiniGameSdk.API.showToast(message); } onDestroy() { cc.game.off(cc.game.EVENT_SHOW, this.resumeSync, this); cc.game.off(cc.game.EVENT_HIDE, this.pausePayments, this); this.alive = false; this.unscheduleAllCallbacks(); this.close(); if (this.entry && cc.isValid(this.entry)) { this.entry.off('click', this.open, this); if (this.entryOwned) this.entry.destroy(); } this.entry = null; } }