import JungleAnimator from "./JungleAnimator"; import JungleProgress from "./JungleProgress"; import JungleRewardService from "./JungleRewardService"; import { JungleActivityConfig, JungleRenderState, JungleTierConfig, JungleTierState, JungleViewAssets, } from "./JungleTypes"; import JungleView from "./JungleView"; import Utils from "../../Script/module/Pay/Utils"; import { MiniGameSdk } from "../../Script/Sdk/MiniGameSdk"; const { ccclass, property } = cc._decorator; /** * Jungle Treasure 流程控制器。 * 只管理活动状态和模块协作;UI、动画、配置、存档与发奖均由独立模块负责。 */ @ccclass export default class JungleManager extends cc.Component { @property(cc.SpriteFrame) cardFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) trackFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) titleFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) buttonFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) freeFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) claimFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) yuanFrame: cc.SpriteFrame = null; @property(cc.BitmapFont) priceFont: cc.BitmapFont = null; @property(cc.BitmapFont) countFont: cc.BitmapFont = null; @property(cc.SpriteFrame) lockFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) unlockFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) timerFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) closeFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) coinFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) freezeFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) hammerFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) magicWandFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) infiniteHealthFrame: cc.SpriteFrame = null; @property({ tooltip: "服务器支付尚未接入时,点击付费档位自动模拟支付成功" }) useMockPayment: boolean = true; @property({ tooltip: "只预览领取和轨道动画,不发道具、不支付、不保存进度" }) previewAnimationOnly: boolean = true; @property(cc.AudioClip) anniuMusic: cc.AudioClip = null; @property(cc.AudioClip) unLockMusic: cc.AudioClip = null; @property(cc.AudioClip) rewardMusic: cc.AudioClip = null; private activityConfig: JungleActivityConfig = null; private currentTierIndex: number = 0; private cards: cc.Node[] = []; private slotPositions: cc.Vec2[] = []; private isMoving: boolean = false; private isPurchasing: boolean = false; private isClaiming: boolean = false; private isUnlockAnimating: boolean = false; private isAttentionAnimating: boolean = false; private hasExpired: boolean = false; private claimTimeout: any = null; private paymentTimeout: any = null; private onShowListener: () => void = null; private pendingPaymentTierId: number = 0; private pendingPaymentProductId: string = ""; private pendingPaymentPrice: number = 0; private checkingIosOrder: boolean = false; private entrySource: string = "jump_window"; private unlockPollDelays: number[] = [3000, 3000, 3000, 3000, 6000, 9000, 9000]; private unlockPollMaxAttempts: number = 8; private unlockPollAttempt: number = 0; private unlockPollTierId: number = 0; private unlockPollSession: number = 0; private unlockPollTimer: any = null; private unlockRequestTimeout: any = null; private unlockRequestTimeoutMs: number = 5500; private isUnlockPolling: boolean = false; private directPayPollDelays: number[] = [3000, 3000, 3000, 3000, 6000, 9000, 9000]; private directPayPollMaxAttempts: number = 8; private directPayPollAttempt: number = 0; private directPayPollSession: number = 0; private directPayPollTimer: any = null; private directPayRequestTimeout: any = null; private directPayRequestTimeoutMs: number = 5500; private directPayOrderNo: string = ""; private directPayTier: JungleTierConfig = null; private isDirectPayPolling: boolean = false; private paymentRetryStage: string = ""; private view: JungleView = null; private animator: JungleAnimator = null; private progress: JungleProgress = null; private rewardService: JungleRewardService = null; protected onLoad(): void { this.progress = new JungleProgress(); this.rewardService = new JungleRewardService(); this.activityConfig = null; this.currentTierIndex = 0; this.view = new JungleView( this.node, this.getViewAssets(), this, this.closeActivity, this.onCardClicked, ); const refs = this.view.build(); this.cards = refs.cards; this.slotPositions = refs.slotPositions; this.loadClaimFrame(); this.animator = new JungleAnimator( this, refs.contentNode, this.cardFrame, this.unlockFrame, ); this.cards.forEach((card) => card.active = false); this.schedule(this.updateTimer, 1); this.schedule(this.playFirstCardAttention, 2); this.registerWechatOnShow(); } /** * 领取图是后加入的分包资源,运行时再按路径加载一次。 * 这样即使旧 prefab 缓存没有反序列化 claimFrame,也不会出现付费按钮空白。 */ private loadClaimFrame(): void { let bundle = cc.assetManager.getBundle("jungle_treasure"); if (!bundle) { cc.warn("[JungleTreasure] 领取图片所在分包未加载"); return; } bundle.load("img/lingqu", cc.SpriteFrame, (error: Error, frame: cc.SpriteFrame) => { if (error || !frame || !cc.isValid(this.node) || !this.view) { if (error) { cc.warn("[JungleTreasure] 领取图片加载失败", error); } return; } this.claimFrame = frame; this.view.setClaimFrame(frame); this.renderAllCards(); }); } protected onDestroy(): void { this.unschedule(this.updateTimer); this.unschedule(this.playFirstCardAttention); this.clearClaimTimeout(); this.clearPaymentTimeout(); this.stopDirectPayResultPolling(true); this.stopUnlockStatePolling(true); this.unregisterWechatOnShow(); } /** 使用后端配置和 0/1/2 状态数组初始化;状态 2 的档位不进入展示队列。 */ public setServerData( config: JungleActivityConfig, tierStates: JungleTierState[], source?: string, ): boolean { if (!config || !Array.isArray(config.tiers) || !Array.isArray(tierStates) || tierStates.length < config.tiers.length) { cc.warn("[JungleTreasure] server config or tierStates is invalid"); return false; } if (source !== undefined) { this.entrySource = source === "front_page" || source === "shop_page" ? source : "jump_window"; } const visibleTiers: JungleTierConfig[] = []; for (let index = 0; index < config.tiers.length; index++) { const sourceTier = config.tiers[index]; const rawState = Number(tierStates[index]); if (rawState !== 0 && rawState !== 1 && rawState !== 2) { cc.warn("[JungleTreasure] invalid tier state:", index + 1, tierStates[index]); return false; } if (rawState === 2) { continue; } visibleTiers.push(Object.assign({}, sourceTier, { state: rawState as JungleTierState, })); } this.activityConfig = Object.assign({}, config, { tiers: visibleTiers, }); this.currentTierIndex = 0; this.hasExpired = false; this.renderAllCards(); this.updateTimer(); return true; } /** 正式支付层完成后回调。支付失败或取消不会移动轨道。 */ public completePurchase(success: boolean): void { if (!this.isPurchasing) { return; } this.isPurchasing = false; if (success) { const tier = this.activityConfig && this.activityConfig.tiers[this.currentTierIndex]; if (tier) { tier.state = 1; } this.renderAllCards(); if (tier) { this.playTierUnlockAnimation(tier.id); } } else { this.renderAllCards(); } } public openActivity(): void { this.node.active = true; this.registerWechatOnShow(); this.renderAllCards(); this.updateTimer(); } public closeActivity(): void { if (this.isMoving || this.isPurchasing || this.isClaiming || this.isUnlockAnimating) { return; } this.stopFirstCardAttention(); this.unregisterWechatOnShow(); this.node.active = false; this.node.emit("jungle-close"); } /** 仅用于当前模拟配置的联调。 */ public resetMockProgress(): void { if (!this.activityConfig) { return; } this.progress.reset(this.activityConfig); this.currentTierIndex = 0; this.renderAllCards(); } public getCurrentTierIndex(): number { return this.currentTierIndex; } private onCardClicked(buttonComponent: cc.Button): void { const clickedButton = buttonComponent && buttonComponent.node; const clickedCard = clickedButton && clickedButton.parent; const clickedTierIndex = clickedCard ? this.cards.indexOf(clickedCard) : -1; const clickedTier = this.activityConfig && clickedTierIndex >= 0 ? this.activityConfig.tiers[this.currentTierIndex + clickedTierIndex] : null; // 状态 1 对应“免费/领取”按钮;无论当前是否轮到该档位,点击都会播放按钮音效。 if (clickedTier && clickedTier.state === 1) { this.playJungleEffect(this.anniuMusic); } if (this.isMoving || this.isPurchasing || this.isClaiming || this.isUnlockAnimating || !this.activityConfig) { return; } const button = buttonComponent && buttonComponent.node; if (!button) { return; } const card = button.parent; if (card !== this.cards[0]) { this.animator.playLockedFeedback(card); return; } this.stopFirstCardAttention(); const tier = this.activityConfig.tiers[this.currentTierIndex]; if (!tier) { return; } const isPurchaseBlocked = tier.price > 0 && tier.state === 0 && !!this.activityConfig.endAt && Date.now() >= this.activityConfig.endAt; if (isPurchaseBlocked) { return; } if (this.previewAnimationOnly) { this.completeCurrentTier(); } else if (tier.state === 0) { this.requestPurchase(tier); } else { this.requestClaim(tier); } } private trackJungleAction(eventName: string, tier: JungleTierConfig): void { let currentBlock = Number(tier && tier.id); if (!isFinite(currentBlock) || currentBlock < 1 || currentBlock > 51) { cc.warn("[JungleTreasure] 埋点格子编号无效:", currentBlock); return; } cc.fx.GameTool.shushu_Track(eventName, { current_block: currentBlock, source: this.entrySource, }); } private requestPurchase(tier: JungleTierConfig): void { this.trackJungleAction("pay_jungle", tier); if (!tier.productId) { this.showToast("礼包商品配置错误"); return; } this.isPurchasing = true; this.pendingPaymentTierId = tier.id; this.pendingPaymentProductId = tier.productId; this.pendingPaymentPrice = Math.round(Number(tier.price) * 100); this.renderAllCards(); this.openLoad(); if (this.useMockPayment) { this.scheduleOnce(() => { this.closeLoad(); this.completePurchase(true); }, 0.45); return; } const systemType = this.getSystemType(); if (systemType === "ios" && !cc.fx.GameConfig.GM_INFO.iosCanPay) { this.startOldIosPayment(tier); return; } this.startDirectPayment(tier, systemType); } /** 状态 1 的礼包必须先由后端改成 2;成功后才发奖励并播放轨道动画。 */ private requestClaim(tier: JungleTierConfig): void { this.trackJungleAction("collect_jungle", tier); this.isClaiming = true; this.renderAllCards(); this.openLoad(); let completed = false; this.clearClaimTimeout(); this.claimTimeout = setTimeout(() => { if (completed) { return; } completed = true; this.claimTimeout = null; this.finishClaimFailure("领取请求超时,请稍后重试"); }, 8000); Utils.claimJungleTreasure(tier.id, (response) => { if (completed || !cc.isValid(this.node)) { return; } completed = true; this.clearClaimTimeout(); const data = response && response.code === 1 ? response.data : null; const states = data && data.tierStates; const claimedTierId = Number(data && data.claimedTierId); if (!Array.isArray(states) || claimedTierId !== tier.id || Number(states[tier.id - 1]) !== 2) { this.finishClaimFailure((response && (response.msg || response.message)) || "礼包领取失败"); return; } this.closeLoad(); this.isClaiming = false; tier.state = 2; this.publishServerState(data); // 后端已确认领取后,才执行客户端道具发放与礼包移动动画。 this.completeCurrentTier(); }); } private finishClaimFailure(message: string): void { this.closeLoad(); this.isClaiming = false; this.renderAllCards(); this.showToast(message); } private startDirectPayment(tier: JungleTierConfig, systemType: string): void { let callbackReceived = false; this.clearPaymentTimeout(); this.paymentTimeout = setTimeout(() => { if (callbackReceived) { return; } callbackReceived = true; this.paymentTimeout = null; this.finishPaymentFailure("支付请求超时,请稍后重试"); // 包含玩家在微信支付面板中的操作时间,不能按普通接口的 8 秒计算。 }, 5 * 60 * 1000); Utils.buyProp( tier.productId, 1, this.pendingPaymentPrice, systemType, tier.productId, (result) => { if (!cc.isValid(this.node)) { return; } callbackReceived = true; this.clearPaymentTimeout(); if (!result) { this.finishPaymentFailure("支付拉起失败"); return; } if (result.err) { if (Number(result.errCode) === 16) { this.startOldIosPayment(tier); return; } this.finishPaymentFailure(Number(result.errCode) === -2 ? "已取消支付" : "支付拉起失败"); return; } this.pollDirectPaymentResult(tier); }, ); } private pollDirectPaymentResult(tier: JungleTierConfig): void { this.directPayOrderNo = String(Utils.outTradeNo || ""); this.directPayTier = tier; if (!this.directPayOrderNo) { this.finishPaymentFailure("支付订单号获取失败"); return; } this.startDirectPayResultPolling(false); } private startDirectPayResultPolling(isManualRetry: boolean): void { this.stopDirectPayResultPolling(false); if (!this.directPayOrderNo || !this.directPayTier) { this.finishPaymentFailure("支付订单信息已失效"); return; } this.directPayPollAttempt = 0; this.isDirectPayPolling = true; this.isPurchasing = true; this.paymentRetryStage = ""; this.hideJungleRetryConfirm(); this.requestDirectPayResult(this.directPayPollSession); } private requestDirectPayResult(session: number): void { if (!this.isDirectPayPolling || session !== this.directPayPollSession || !cc.isValid(this.node)) { return; } this.directPayPollAttempt++; const currentAttempt = this.directPayPollAttempt; const orderNo = this.directPayOrderNo; let requestFinished = false; this.clearDirectPayRequestTimeout(); this.directPayRequestTimeout = setTimeout(() => { if (requestFinished || session !== this.directPayPollSession) { return; } requestFinished = true; this.directPayRequestTimeout = null; this.scheduleNextDirectPayPoll(session); }, this.directPayRequestTimeoutMs); try { Utils.POST("wx/getPayInfo", { uid: cc.fx.GameConfig.GM_INFO.uid || Utils.uid, outTradeNo: orderNo, }, (response) => { if (requestFinished || session !== this.directPayPollSession || !cc.isValid(this.node)) { return; } requestFinished = true; this.clearDirectPayRequestTimeout(); const payState = Number(response && response.data && response.data.pay_state); if (response && response.code === 1 && payState === 2) { this.completeDirectPayResultPolling(); return; } if (response && response.code === 1 && payState === 1) { this.stopDirectPayResultPolling(true); this.finishPaymentFailure("已取消支付"); return; } this.scheduleNextDirectPayPoll(session); }); } catch (error) { if (requestFinished || session !== this.directPayPollSession) { return; } requestFinished = true; this.clearDirectPayRequestTimeout(); this.scheduleNextDirectPayPoll(session); } } private scheduleNextDirectPayPoll(session: number): void { if (!this.isDirectPayPolling || session !== this.directPayPollSession) { return; } if (this.directPayPollAttempt >= this.directPayPollMaxAttempts) { this.handleDirectPayPollExhausted(); return; } const delayIndex = Math.min(this.directPayPollAttempt - 1, this.directPayPollDelays.length - 1); const delay = this.directPayPollDelays[delayIndex]; this.clearDirectPayPollTimer(); this.directPayPollTimer = setTimeout(() => { this.directPayPollTimer = null; this.requestDirectPayResult(session); }, delay); } private completeDirectPayResultPolling(): void { const tier = this.directPayTier; const orderNo = this.directPayOrderNo; const attempts = this.directPayPollAttempt; this.stopDirectPayResultPolling(true); this.paymentRetryStage = ""; if (!tier) { this.finishPaymentFailure("支付成功,但礼包信息已失效"); return; } const name = "购买丛林宝藏礼包:" + tier.productId; MiniGameSdk.API.yinli_Pay(this.pendingPaymentPrice, orderNo, name); this.refreshAfterPayment(tier.id); } private handleDirectPayPollExhausted(): void { const orderNo = this.directPayOrderNo; const attempts = this.directPayPollAttempt; this.stopDirectPayResultPolling(false); this.paymentRetryStage = "pay_result"; this.isPurchasing = false; this.closeLoad(); this.renderAllCards(); this.showJungleRetryConfirm(); } private stopDirectPayResultPolling(clearContext: boolean): void { this.directPayPollSession++; this.isDirectPayPolling = false; this.clearDirectPayPollTimer(); this.clearDirectPayRequestTimeout(); this.directPayPollAttempt = 0; if (clearContext) { this.directPayOrderNo = ""; this.directPayTier = null; } } private clearDirectPayPollTimer(): void { if (this.directPayPollTimer) { clearTimeout(this.directPayPollTimer); this.directPayPollTimer = null; } } private clearDirectPayRequestTimeout(): void { if (this.directPayRequestTimeout) { clearTimeout(this.directPayRequestTimeout); this.directPayRequestTimeout = null; } } /** iOS 旧支付:跳转客服,回到小游戏时由 wx.onShow 查询订单结果。 */ private startOldIosPayment(tier: JungleTierConfig): void { this.clearPaymentTimeout(); const payInfo = { price: this.pendingPaymentPrice, payment_name: tier.productId, payment_count: 1, }; Utils.GoKEFu(payInfo, (result) => { if (!cc.isValid(this.node)) { return; } if (result !== "success") { this.finishPaymentFailure("客服支付入口打开失败"); } }); } private registerWechatOnShow(): void { if (this.onShowListener) { return; } // @ts-ignore if (typeof wx === "undefined" || !wx.onShow) { return; } this.onShowListener = () => this.handleWechatOnShow(); // @ts-ignore wx.onShow(this.onShowListener); } private unregisterWechatOnShow(): void { // @ts-ignore if (!this.onShowListener || typeof wx === "undefined" || !wx.offShow) { return; } // @ts-ignore wx.offShow(this.onShowListener); this.onShowListener = null; } private handleWechatOnShow(): void { const gameInfo: any = cc.fx.GameConfig.GM_INFO; const orderNo = gameInfo.iosJungleOrder; if (!orderNo || this.checkingIosOrder) { return; } this.checkingIosOrder = true; this.isPurchasing = true; this.openLoad(); Utils.getIosPayInfo(orderNo, (response) => { if (!cc.isValid(this.node)) { return; } this.checkingIosOrder = false; const data = response && response.data; const productId = (data && data.payment_name) || this.pendingPaymentProductId; const price = Number(data && data.goodsPrice) || this.pendingPaymentPrice; if (response && response.code === 1) { gameInfo.iosJungleOrder = ""; const name = "购买丛林宝藏礼包:" + productId; MiniGameSdk.API.yinli_Pay(price, orderNo, name); this.refreshAfterPayment(this.resolvePendingTierId(productId)); return; } if (response && response.code === 2) { this.closeLoad(); this.isPurchasing = false; this.renderAllCards(); this.showToast("支付结果确认超时,稍后返回活动会继续查询"); return; } gameInfo.iosJungleOrder = ""; this.finishPaymentFailure("支付未成功"); }); } private refreshAfterPayment(tierId: number): void { this.startUnlockStatePolling(tierId, false); } /** ConfirmBox 重试按钮事件:按失败阶段继续查询支付结果或礼包解锁状态。 */ public retryJungleUnlock(): void { if (this.paymentRetryStage === "pay_result") { if (!this.directPayOrderNo || !this.directPayTier) { this.hideJungleRetryConfirm(); this.showToast("支付订单信息已失效"); return; } if (this.isDirectPayPolling) { return; } this.hideJungleRetryConfirm(); this.isPurchasing = true; this.renderAllCards(); this.openLoad(); this.startDirectPayResultPolling(true); return; } if (this.unlockPollTierId <= 0) { this.hideJungleRetryConfirm(); this.showToast("没有需要重试的礼包"); return; } if (this.isUnlockPolling) { return; } this.hideJungleRetryConfirm(); this.isPurchasing = true; this.renderAllCards(); this.openLoad(); this.startUnlockStatePolling(this.unlockPollTierId, true); } private startUnlockStatePolling(tierId: number, isManualRetry: boolean): void { this.clearPaymentTimeout(); this.stopUnlockStatePolling(false); if (tierId <= 0) { this.finishPaymentFailure("支付成功,但礼包档位信息无效"); return; } this.unlockPollTierId = tierId; this.unlockPollAttempt = 0; this.isUnlockPolling = true; this.isPurchasing = true; this.paymentRetryStage = ""; this.hideJungleRetryConfirm(); const totalAttempts = this.unlockPollMaxAttempts; this.requestUnlockState(this.unlockPollSession); } private requestUnlockState(session: number): void { if (!this.isUnlockPolling || session !== this.unlockPollSession || !cc.isValid(this.node)) { return; } this.unlockPollAttempt++; const currentAttempt = this.unlockPollAttempt; const totalAttempts = this.unlockPollMaxAttempts; const tierId = this.unlockPollTierId; let requestFinished = false; this.clearUnlockRequestTimeout(); this.unlockRequestTimeout = setTimeout(() => { if (requestFinished || session !== this.unlockPollSession) { return; } requestFinished = true; this.unlockRequestTimeout = null; this.scheduleNextUnlockPoll(session); }, this.unlockRequestTimeoutMs); try { Utils.getJungleTreasure((response) => { if (requestFinished || session !== this.unlockPollSession || !cc.isValid(this.node)) { return; } requestFinished = true; this.clearUnlockRequestTimeout(); const data = response && response.code === 1 ? response.data : null; const states = data && data.tierStates; const refreshedState = Array.isArray(states) && tierId > 0 ? Number(states[tierId - 1]) : -1; if (data && data.config && Array.isArray(states) && (refreshedState === 1 || refreshedState === 2)) { this.completeUnlockStatePolling(tierId, refreshedState, data, states); return; } this.scheduleNextUnlockPoll(session); }); } catch (error) { if (requestFinished || session !== this.unlockPollSession) { return; } requestFinished = true; this.clearUnlockRequestTimeout(); this.scheduleNextUnlockPoll(session); } } private scheduleNextUnlockPoll(session: number): void { if (!this.isUnlockPolling || session !== this.unlockPollSession) { return; } const totalAttempts = this.unlockPollMaxAttempts; if (this.unlockPollAttempt >= totalAttempts) { this.handleUnlockPollExhausted(); return; } const delayIndex = Math.min(this.unlockPollAttempt - 1, this.unlockPollDelays.length - 1); const delay = this.unlockPollDelays[delayIndex]; this.clearUnlockPollTimer(); this.unlockPollTimer = setTimeout(() => { this.unlockPollTimer = null; this.requestUnlockState(session); }, delay); } private completeUnlockStatePolling( tierId: number, refreshedState: number, data: any, states: JungleTierState[], ): void { this.stopUnlockStatePolling(true); this.paymentRetryStage = ""; this.isPurchasing = false; this.publishServerState(data); const refreshed = this.setServerData(data.config, states); if (refreshed && refreshedState === 1) { this.playTierUnlockAnimation(tierId); } if (!refreshed) { this.closeLoad(); this.renderAllCards(); this.showToast("支付成功,礼包状态刷新失败,请重新进入活动"); return; } this.hideJungleRetryConfirm(); // setServerData 已将付费档位从“几元”重新渲染为“领取”,完成后再关闭 Loading。 this.closeLoad(); this.showToast("支付成功,礼包已解锁"); } private handleUnlockPollExhausted(): void { const tierId = this.unlockPollTierId; const attempts = this.unlockPollAttempt; this.stopUnlockStatePolling(false); this.paymentRetryStage = "unlock_state"; this.isPurchasing = false; this.closeLoad(); this.renderAllCards(); this.showJungleRetryConfirm(); } private showJungleRetryConfirm(): void { const confirmBox = this.node.getChildByName("ConfirmBox"); if (!confirmBox) { this.showToast("网络异常,请检查网络后重新进入活动"); return; } confirmBox.zIndex = 100; if (confirmBox.parent) { confirmBox.setSiblingIndex(confirmBox.parent.childrenCount - 1); } confirmBox.active = true; } private hideJungleRetryConfirm(): void { const confirmBox = this.node.getChildByName("ConfirmBox"); if (confirmBox) { confirmBox.active = false; } } private stopUnlockStatePolling(clearTierId: boolean): void { this.unlockPollSession++; this.isUnlockPolling = false; this.clearUnlockPollTimer(); this.clearUnlockRequestTimeout(); this.unlockPollAttempt = 0; if (clearTierId) { this.unlockPollTierId = 0; } } private clearUnlockPollTimer(): void { if (this.unlockPollTimer) { clearTimeout(this.unlockPollTimer); this.unlockPollTimer = null; } } private clearUnlockRequestTimeout(): void { if (this.unlockRequestTimeout) { clearTimeout(this.unlockRequestTimeout); this.unlockRequestTimeout = null; } } private finishPaymentFailure(message: string): void { this.stopDirectPayResultPolling(true); this.stopUnlockStatePolling(true); this.paymentRetryStage = ""; this.hideJungleRetryConfirm(); this.clearPaymentTimeout(); this.closeLoad(); this.isPurchasing = false; this.checkingIosOrder = false; this.renderAllCards(); this.showToast(message); } private resolvePendingTierId(productId: string): number { if (this.pendingPaymentTierId > 0) { return this.pendingPaymentTierId; } const match = String(productId || "").match(/^jungle_treasure_(\d+)$/); if (!match) { return 0; } const paidIndex = Number(match[1]); return paidIndex === 1 ? 4 : 4 + (paidIndex - 1) * 6; } private publishServerState(data: any): void { this.node.emit("jungle-state-changed", data); } private getSystemType(): string { try { // @ts-ignore const info = typeof wx !== "undefined" && wx.getSystemInfoSync // @ts-ignore ? wx.getSystemInfoSync() : null; return info && info.platform === "ios" ? "ios" : "Android"; } catch (error) { cc.warn("[JungleTreasure] 获取系统类型失败", error); return "Android"; } } private getSceneController(): any { const canvas = cc.find("Canvas"); return canvas && (canvas.getComponent("JiaZai") || canvas.getComponent("SceneManager")); } private openLoad(): void { const controller = this.getSceneController(); if (controller && typeof controller.openLoad === "function") { controller.openLoad(); } } private closeLoad(): void { const controller = this.getSceneController(); if (controller && typeof controller.closeLoad === "function") { controller.closeLoad(); } } private showToast(message: string): void { console.log("显示提示:", message); if (MiniGameSdk && MiniGameSdk.API && MiniGameSdk.API.showToast) { MiniGameSdk.API.showToast(message); } } private clearClaimTimeout(): void { if (this.claimTimeout) { clearTimeout(this.claimTimeout); this.claimTimeout = null; } } private clearPaymentTimeout(): void { if (this.paymentTimeout) { clearTimeout(this.paymentTimeout); this.paymentTimeout = null; } } private completeCurrentTier(): void { console.log("点击礼包档位按钮完成当前档位"); const tier = this.activityConfig.tiers[this.currentTierIndex]; if (!tier || this.isMoving) { return; } this.isMoving = true; if (!this.previewAnimationOnly) { this.rewardService.grant(this.node, tier.rewards); } const leavingCard = this.cards[0]; // playClaimEffect 开始时,奖励图标会立即执行向上飞并渐隐动画。 this.playJungleEffect(this.rewardMusic); this.animator.playClaimEffect( leavingCard, () => this.animator.playCardExit(leavingCard, () => this.shiftCards()), ); } private shiftCards(): void { const oldCards = this.cards.slice(); this.currentTierIndex++; if (!this.previewAnimationOnly) { this.progress.save(this.activityConfig, this.currentTierIndex); } const recycled = oldCards[0]; recycled.stopAllActions(); recycled.active = false; recycled.scale = 1; recycled.opacity = 0; recycled.setAnchorPoint(0.5, 0.5); this.animator.moveCardsWithOverlap(oldCards, this.slotPositions, 1, () => { this.cards.splice( 0, this.cards.length, oldCards[1], oldCards[2], oldCards[3], oldCards[4], oldCards[5], recycled, ); this.showNewSixthCard(recycled); }); } private showNewSixthCard(card: cc.Node): void { const sixthTierIndex = this.currentTierIndex + 5; const targetCenter = this.slotPositions[5]; this.view.renderCard(card, this.activityConfig, this.getRenderState(), sixthTierIndex, 5); if (sixthTierIndex >= this.activityConfig.tiers.length) { card.setAnchorPoint(0.5, 0.5); card.setPosition(targetCenter); this.finishShift(); return; } this.animator.showNewCard(card, targetCenter, () => this.finishShift()); } private finishShift(): void { this.isMoving = false; this.renderAllCards(); if (this.currentTierIndex >= this.activityConfig.tiers.length) { this.node.emit("jungle-complete"); return; } let currentTier = this.activityConfig.tiers[this.currentTierIndex]; if (currentTier && currentTier.state !== 0) { this.playTierUnlockAnimation(currentTier.id); } } private renderAllCards(): void { if (this.view) { this.view.renderAllCards(this.activityConfig, this.getRenderState()); } } /** 无论当前礼包是否解锁,每两秒让最前方礼包整体 duang 一次。 */ private playFirstCardAttention(): void { if (!this.node.activeInHierarchy || !this.activityConfig || !this.animator || this.isAttentionAnimating || this.isMoving || this.isPurchasing || this.isClaiming || this.isUnlockAnimating) { return; } let firstCard = this.cards[0]; if (!firstCard || !firstCard.activeInHierarchy) { return; } this.isAttentionAnimating = true; this.animator.playCardAttention(firstCard, () => { if (cc.isValid(this.node)) { this.isAttentionAnimating = false; } }); } private stopFirstCardAttention(): void { if (!this.isAttentionAnimating || !this.animator) { return; } this.animator.stopCardAttention(this.cards[0]); this.isAttentionAnimating = false; } private playTierUnlockAnimation(tierId: number): void { let targetCard: cc.Node = null; for (let index = 0; index < this.cards.length; index++) { let card = this.cards[index]; if (card && card.active && card.name === "reward_card_" + tierId) { targetCard = card; break; } } if (!targetCard || !this.animator) { return; } this.isUnlockAnimating = true; // playUnlockEffect 起始阶段会立即将 lock 图片切换为 unlock。 this.playJungleEffect(this.unLockMusic); this.animator.playUnlockEffect(targetCard, () => { if (!cc.isValid(this.node)) { return; } this.isUnlockAnimating = false; this.renderAllCards(); }); } private playJungleEffect(clip: cc.AudioClip): void { if (!clip || (cc.fx.GameConfig && cc.fx.GameConfig.GM_INFO && cc.fx.GameConfig.GM_INFO.effectOpen === false)) { return; } const audioManager = cc.fx.AudioManager && cc.fx.AudioManager._instance; if (audioManager && audioManager.play) { audioManager.play(clip, false, null, false); return; } cc.audioEngine.playEffect(clip, false); } private updateTimer(): void { if (!this.view || !this.activityConfig) { return; } if (this.view.updateTimer(this.activityConfig) === 0 && !this.hasExpired) { this.hasExpired = true; this.renderAllCards(); this.node.emit("jungle-expired"); } } private getRenderState(): JungleRenderState { return { currentTierIndex: this.currentTierIndex, isMoving: this.isMoving, isPurchasing: this.isPurchasing || this.isClaiming || this.isUnlockAnimating, }; } private getViewAssets(): JungleViewAssets { return { cardFrame: this.cardFrame, trackFrame: this.trackFrame, titleFrame: this.titleFrame, buttonFrame: this.buttonFrame, freeFrame: this.freeFrame, claimFrame: this.claimFrame, yuanFrame: this.yuanFrame, priceFont: this.priceFont, countFont: this.countFont, lockFrame: this.lockFrame, timerFrame: this.timerFrame, closeFrame: this.closeFrame, coinFrame: this.coinFrame, freezeFrame: this.freezeFrame, hammerFrame: this.hammerFrame, magicWandFrame: this.magicWandFrame, infiniteHealthFrame: this.infiniteHealthFrame, }; } }