MatchMaster/assets/Script/coin_madness/CoinMadnessHost.ts

378 lines
20 KiB
TypeScript

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;
}
}