378 lines
20 KiB
TypeScript
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;
|
|
}
|
|
}
|