MatchMaster/assets/jungle_treasure/script/jungle_Manager.ts
2026-08-07 10:59:22 +08:00

849 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
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 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.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 {
console.log("点击礼包档位按钮最外层", buttonComponent);
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 {
console.log("点击礼包档位按钮准备请求", buttonComponent);
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);
console.log("点击礼包档位按钮内里面");
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);
console.log("点击礼包档位按钮开始请求");
Utils.claimJungleTreasure(tier.id, (response) => {
console.log("点击礼包档位按钮请求结果", 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);
console.log("准备开始调用 购买礼包接口", this.pendingPaymentPrice);
Utils.buyProp(
tier.productId,
1,
this.pendingPaymentPrice,
systemType,
tier.productId,
(result) => {
console.log("点击礼包档位按钮支付结果", result);
if (!cc.isValid(this.node)) {
console.log("11111点击礼包档位按钮支付结果回调已处理或节点已销毁");
return;
}
callbackReceived = true;
this.clearPaymentTimeout();
if (!result) {
this.finishPaymentFailure("支付拉起失败");
console.log("22222点击礼包档位按钮支付结果回调已处理或节点已销毁");
return;
}
if (result.err) {
if (Number(result.errCode) === 16) {
this.startOldIosPayment(tier);
console.log("33333点击礼包档位按钮支付结果回调已处理或节点已销毁");
return;
}
this.finishPaymentFailure(Number(result.errCode) === -2 ? "已取消支付" : "支付拉起失败");
console.log("44444点击礼包档位按钮支付结果回调已处理或节点已销毁");
return;
}
console.log("55555点击礼包档位按钮支付结果", tier);
this.pollDirectPaymentResult(tier);
},
);
}
private pollDirectPaymentResult(tier: JungleTierConfig): void {
console.log("拿到礼包档位按钮轮询支付结果之后调用档位是否解锁");
Utils.getPayInfo((response) => {
console.log("点击礼包档位按钮轮询支付结果", response);
if (!cc.isValid(this.node)) {
return;
}
const payState = Number(response && response.data && response.data.pay_state);
console.log("payState点击礼包档位按钮轮询支付结果", payState);
if (response && response.code === 1 && payState === 2) {
console.log("准备改变礼包状态为可领取", response);
const name = "购买丛林宝藏礼包:" + tier.productId;
MiniGameSdk.API.yinli_Pay(this.pendingPaymentPrice, Utils.outTradeNo, name);
this.refreshAfterPayment(tier.id);
return;
}
this.finishPaymentFailure(payState === 1 ? "已取消支付" : "支付结果获取失败");
});
}
/** 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 {
let completed = false;
this.clearPaymentTimeout();
this.paymentTimeout = setTimeout(() => {
if (completed) {
return;
}
completed = true;
this.paymentTimeout = null;
this.finishPaymentFailure("支付成功,礼包状态刷新超时,请重新进入活动");
}, 8000);
const requestUnlockedState = () => {
if (completed || !cc.isValid(this.node)) {
return;
}
Utils.getJungleTreasure((response) => {
if (completed || !cc.isValid(this.node)) {
return;
}
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;
// 微信支付回调会先把订单改为成功,再写玩家的活动解锁状态。
// 订单成功后第一次读取可能仍为 0因此在原有 8 秒超时内继续轮询。
if (!data || !data.config || !Array.isArray(states)
|| (tierId > 0 && refreshedState !== 1 && refreshedState !== 2)) {
this.scheduleOnce(requestUnlockedState, 0.4);
return;
}
completed = true;
this.clearPaymentTimeout();
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;
}
// setServerData 已将付费档位从“3元”重新渲染为“领取”完成后再关闭 Loading。
this.closeLoad();
this.showToast("支付成功,礼包已解锁");
});
};
requestUnlockedState();
}
private finishPaymentFailure(message: string): void {
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 {
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];
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;
this.animator.playUnlockEffect(targetCard, () => {
if (!cc.isValid(this.node)) {
return;
}
this.isUnlockAnimating = false;
this.renderAllCards();
});
}
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,
};
}
}