import Utils from './module/Pay/Utils'; import Art from './BattlePassArt'; import NumberToImage from './NumberToImage'; import { settlementRewards, settlementSteps } from './module/Config/BattlePassSettlementModel'; import { MiniGameSdk } from './Sdk/MiniGameSdk'; import BattlePassSettlementCheck from './module/Config/BattlePassSettlementCheck'; const { ccclass } = cc._decorator; @ccclass export default class BattlePassSettlement extends cc.Component { private root: cc.Node; private panel: cc.Node; private confirm: cc.Node; private rows: any[] = []; private row: any; private latest = 0; private serverTime = 0; private mode = ''; private busy = false; private offerToken = ''; private finished: () => void; private iosOrder = ''; private recoveredRewards: any[] = []; private check: BattlePassSettlementCheck; private readVersion = 0; private checkedThrough = 0; private retryDelay = 5; private back = (e: cc.Event.EventKeyboard) => { if (e.keyCode === cc.macro.KEY.escape || e.keyCode === 6) this.close(); }; private resume = () => { if (this.iosOrder) this.checkIos(); }; async run(done: () => void) { this.finished = done; try { // Resolve paid entitlements before selecting frozen rewards. Other login // orders remain in the home queue until this entire sequence is closed. const gm = cc.fx.GameConfig.GM_INFO; this.check = BattlePassSettlementCheck.forPlayer(gm); // Usually observed by the existing homepage read. Also cover failed // or locked entry reads so the login cache never lacks a deadline. if (!this.check.hasSchedule()) { const period: any = await new Promise((resolve, reject) => Utils.POSTJSON('passCheck', { uid: gm.uid || Utils.uid, action: 'read' }, res => res.code === 1 ? resolve(res) : reject(res))); this.check.observeStart(Number(period.data.time)); if (!this.check.hasSchedule()) throw { msg: '战令周期时间不可用' }; } if (!this.isCurrentRun()) return; for (const order of (gm.allOutTradeNo || []).slice()) { if (!/^battlepass(?:_30|_12)?$/.test(order.itemid)) continue; if (this.check.reviewOrders.has(order.outTradeNo)) continue; try { const paid = await Utils.completePassOrder(order.outTradeNo); if (!this.isCurrentRun()) return; if (!paid.data.alreadyGranted) { if (paid.data.postSeason) this.recoveredRewards.push(...(paid.data.rewards || [])); else if (paid.data.bonus) this.recoveredRewards.push({ type: 'coin', count: 3000 }, { type: 'pass_key', count: 20 }); } gm.allOutTradeNo = gm.allOutTradeNo.filter(o => o.outTradeNo !== order.outTradeNo); } catch (e) { if (e.code !== 410) throw e; // Paid duplicates/obsolete orders remain available for manual review. this.check.reviewOrders.add(order.outTradeNo); cc.warn('战令订单需要人工处理', order.outTradeNo, e.msg); } } this.readVersion = this.check.version; const readStarted = Date.now(); const result = await Utils.settlementRequest('read'); if (!this.isCurrentRun()) return; this.serverTime = result.data.serverTime; this.check.syncClock(this.serverTime); // A request spanning the cutoff may contain a pre-cutoff snapshot. this.checkedThrough = this.serverTime - Math.max(0, Date.now() - readStarted); this.latest = Math.max(0, ...result.data.rows.map(r => r.end)); this.rows = settlementSteps(result.data.rows, this.latest, this.serverTime); if (!this.rows.length && !this.recoveredRewards.length) { this.finish(); return; } await Art.load(); if (!this.isCurrentRun()) return; this.root = cc.instantiate(Art.settlementPrefab); this.root.active = false; this.root.parent = this.node; this.root.zIndex = 1000; this.panel = this.root.getChildByName('panel'); const fit = Math.min(this.node.width / this.root.width, this.node.height / this.root.height); this.panel.scaleX *= fit; this.panel.scaleY *= fit; for (const mode of ['offer', 'empty', 'claim']) this.bindButton(mode + '/closeButton', () => this.close()); this.bindButton('claim/claimButton', () => this.claim()); this.bindButton('offer/buyButton', () => this.purchase()); this.bindButton('confirmation/thinkAgainButton', () => this.hideConfirmation()); this.bindButton('confirmation/abandonButton', () => this.decline()); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.back, this); // @ts-ignore if (typeof wx !== 'undefined') wx.onShow(this.resume); this.next(); } catch (e) { if (!this.isCurrentRun()) return; this.error(e); if (this.root && cc.isValid(this.root)) this.root.destroy(); cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.back, this); // @ts-ignore if (typeof wx !== 'undefined') wx.offShow(this.resume); // Keep the queue reserved until the settlement check can complete. this.scheduleOnce(() => this.run(done), this.retryDelay); this.retryDelay = Math.min(this.retryDelay * 2, 60); } } private isCurrentRun(): boolean { return cc.isValid(this, true) && cc.isValid(this.node, true) && this.check === BattlePassSettlementCheck.forPlayer(cc.fx.GameConfig.GM_INFO); } private finish() { // Cache only after every popup and reward animation has completed. A // destroyed scene or failed request must leave the login check pending. if (!this.isCurrentRun()) return; this.check.complete(this.checkedThrough, this.readVersion); if (this.root && cc.isValid(this.root)) this.root.destroy(); this.destroy(); this.finished(); } private error(e: any) { cc.log(e.msg || e.message || '网络异常,请重试'); // MiniGameSdk.API.showToast(e.msg || e.message || '网络异常,请重试'); } private next() { this.row = this.rows.shift(); if (!this.row) { if (this.recoveredRewards.length) { const rewards = this.recoveredRewards.splice(0); this.playRewards(rewards, () => this.next()); return; } this.finish(); return; } if (!this.row.claimed && !this.row.emptySeen) { const rewards = settlementRewards(this.row, cc.fx.GameConfig.PASS_CHECK); if (rewards.length) this.show('claim', rewards); else { this.show('empty'); this.busy = true; // Mark only after the successfully constructed view has reached a rendered frame. this.scheduleOnce(() => this.markEmpty(), 0); } } else this.offerOrNext(); } private async markEmpty() { try { const r = await Utils.settlementRequest('empty_seen', { end: this.row.end }); this.row = r.data.row; this.busy = false; } catch (e) { this.busy = false; this.error(e); } } private offerOrNext() { if (this.row.end !== this.latest || this.row.end <= this.serverTime - 30 * 86400000 || this.row.stage.activate || this.row.stage.tier || this.row.offer !== 'unused' || this.row.purchase) { this.next(); return; } this.offerToken = Date.now().toString(36) + '_' + Math.random().toString(36).slice(2) + '_' + Math.random().toString(36).slice(2); // A stale snapshot may pass the local check but be rejected by the server. // Keep the prefab (including its default offer and shade) hidden until accepted. this.root.active = false; this.busy = true; this.markOffer(); } private async markOffer() { try { const r = await Utils.settlementRequest('offer_shown', { end: this.row.end, offerToken: this.offerToken }); if (!cc.isValid(this.node) || !cc.isValid(this.root)) return; this.row = r.data.row; this.show('offer', settlementRewards(this.row, cc.fx.GameConfig.PASS_CHECK, true)); this.busy = false; } catch (e) { if (!cc.isValid(this.node) || !cc.isValid(this.root)) return; this.busy = false; if (e.code === 410) { this.next(); return; } this.error(e); // No button is visible yet. Retry with the same token so a lost response // does not consume the opportunity twice or leave the home queue stuck. this.busy = true; this.scheduleOnce(() => this.markOffer(), 5); } } private bindButton(path: string, click: () => void) { cc.find(path, this.panel).on('click', () => { if (!this.busy) { cc.fx.AudioManager._instance.playEffect("anniu_little", null); click(); } }, this); } private show(mode: string, rewards: any[] = []) { this.mode = mode; this.hideConfirmation(); for (const name of ['offer', 'empty', 'claim']) this.panel.getChildByName(name).active = name === mode; if (mode !== 'empty') this.preview(rewards); this.root.active = true; } private preview(rewards: any[]) { const group = cc.find(this.mode + '/rewards', this.panel); const slots = group.getChildByName('topRow').children.concat(group.getChildByName('bottomRow').children); slots.forEach((cell, i) => { const r = rewards[i]; cell.active = !!r; if (!r) return; const item = cell.getChildByName('item'); const icon = item.getChildByName('icon'); for (const child of icon.children) child.active = child.name === r.type; const label = item.getChildByName('amount'); if (r.type === 'infinite_health') NumberToImage.durationToImageNodes(r.count, label); else NumberToImage.numberToImageNodesNumber(r.count, 45, 5, 'big_', label, true); }); } private hideConfirmation() { const confirmation = this.confirm || this.panel.getChildByName('confirmation'); if (confirmation) confirmation.active = false; this.confirm = null; } private close() { if (this.busy) return; if (this.confirm) { this.hideConfirmation(); return; } if (this.mode === 'claim') { this.claim(); return; } if (this.mode === 'empty') { if (!this.row.emptySeen) { this.busy = true; this.markEmpty(); } else this.offerOrNext(); return; } if (this.mode !== 'offer') return; this.confirm = this.panel.getChildByName('confirmation'); this.confirm.active = true; } private async decline() { this.busy = true; try { await Utils.settlementRequest('decline', { end: this.row.end }); this.busy = false; this.next(); } catch (e) { this.busy = false; this.error(e); } } private async claim() { if (this.busy) return; this.busy = true; try { const r = await Utils.commitPassSettlement('claim', this.row); if (!cc.isValid(this.root)) return; this.row = r.data.row; this.playRewards(r.data.rewards, () => { this.busy = false; this.offerOrNext(); }); } catch (e) { this.busy = false; this.error(e); } } private playRewards(rewards: any[], done: () => void) { this.root.active = true; this.mode = 'animation'; this.panel.active = false; const home = this.node.getComponent('JiaZai'); home.updateCoin(); home.updatePower(); if (rewards?.length) home.openRewardWindow(rewards, undefined, () => { this.panel.active = true; done(); }); else { this.panel.active = true; done(); } } private async purchase() { if (this.busy) return; if (this.row.offer !== 'shown') { this.busy = true; await this.markOffer(); return; } this.busy = true; let system = 'Android'; // @ts-ignore if (typeof wx !== 'undefined' && wx.getSystemInfoSync().platform === 'ios') system = 'ios'; if (system === 'ios' && !cc.fx.GameConfig.GM_INFO.iosCanPay) { this.oldIos(); return; } Utils.buyProp('battlepass', 1, 1800, system, 'battlepass', res => { if (!res || res.err) { if (res?.errCode === 16) { this.oldIos(); return; } this.busy = false; this.error(res || {}); return; } Utils.getPayInfo(data => { if (data?.data?.pay_state === 2) this.fulfill(res.outTradeNo); else { this.busy = false; this.error({ msg: '支付未完成,请重试' }); } }, res.outTradeNo); }, String(this.row.end), undefined, this.offerToken); } private oldIos() { Utils.GoKEFu({ payment_name: 'battlepass', payment_count: 1, price: 1800, passEnd: String(this.row.end), passOfferToken: this.offerToken }, () => { this.busy = false; }, order => { this.iosOrder = order; }); } private checkIos() { this.busy = true; Utils.getIosPayInfo(this.iosOrder, res => { if (res.code === 1) this.fulfill(this.iosOrder); else this.busy = false; }); } private async fulfill(order: string) { try { const result = await Utils.completePassOrder(order); this.iosOrder = ''; cc.fx.GameConfig.GM_INFO.iosPassCheckOrder = null; if (!cc.isValid(this.root)) return; this.playRewards(result.data.rewards, () => { this.busy = false; this.next(); }); } catch (e) { this.busy = false; this.error(e); } } onDestroy() { cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.back, this); // @ts-ignore if (typeof wx !== 'undefined') wx.offShow(this.resume); if (this.root && cc.isValid(this.root)) this.root.destroy(); } }