MatchMaster/assets/Script/seven_day_gift/SevenDayGiftHost.ts
2026-09-18 11:49:52 +08:00

309 lines
13 KiB
TypeScript
Raw Permalink 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 SevenDayGiftBootstrapApi, { SevenDayActivityInfo } from "./SevenDayGiftBootstrapApi";
import HomePopupQueue from "../../home_popup_queue/HomePopupQueue";
const { ccclass } = cc._decorator;
const AUTO_OPEN_KEY = "seven_day_gift_auto_open_v1";
const BUNDLE_NAME = "seven_day_gift";
const GIFT_PREFAB_PATH = "prefab/sevenDayGift";
// 主包通过接口调用分包,避免静态引入活动脚本和资源。
interface SevenDayGiftView {
initializeRuntime(info: SevenDayActivityInfo, closeHandler: () => void): boolean;
updateRuntimeInfo(info: SevenDayActivityInfo): void;
show(autoClaim?: boolean): boolean;
}
/** 主页入口:查询活动、安排自动展示、管理分包生命周期。 */
@ccclass
export default class SevenDayGiftHost extends cc.Component {
private giftNode: cc.Node = null;
private giftView: SevenDayGiftView = null;
private giftBundle: cc.AssetManager.Bundle = null;
private giftLoading = false;
private giftLoadId = 0; // 加载批次号,用于忽略过期回调。
private giftLoadCallbacks: Array<(error?: Error) => void> = [];
private info: SevenDayActivityInfo = null;
private querying = false;
private retryCount = 0;
private testPanelNode: cc.Node = null;
private testBundle: cc.AssetManager.Bundle = null;
// 监听回到前台事件,并在正式环境查询活动。
onLoad() {
if (this.node.parent) this.node.setContentSize(this.node.parent.getContentSize());
cc.game.on(cc.game.EVENT_SHOW, this.onGameShow, this);
// 暂停加载测试面板;恢复测试时重新启用,并打开预制体 entry 节点。
// this.loadTestEntry();
if (CC_PREVIEW) return;
this.queryActivity(true);
}
// 测试 UI 留在活动分包内,仅独立测试服加载;主包不导入测试脚本。
private loadTestEntry() {
if (!SevenDayGiftBootstrapApi.getTestEnvironment().allowed) return;
const canvas = cc.find("Canvas");
const home: any = canvas && canvas.getComponent("JiaZai");
if (!home || typeof home.loadHomeBundleWithDependencies !== "function") return;
home.loadHomeBundleWithDependencies(BUNDLE_NAME, 5, (error: Error, bundle: cc.AssetManager.Bundle) => {
if (error || !bundle || !cc.isValid(this.node, true)) return;
bundle.load("test/SevenDayTest", cc.Prefab, (loadError: Error, prefab: cc.Prefab) => {
if (loadError || !prefab || !cc.isValid(this.node, true) || !SevenDayGiftBootstrapApi.getTestEnvironment().allowed) return;
// 测试 Prefab 仅保存内置 UI 组件,避免失效脚本引用反序列化为 null。
const TestPanel = cc.js.getClassByName("SevenDayTest");
if (!TestPanel) {
cc.error("[SevenDayGiftHost] SevenDayTest 脚本未注册,请重新构建七日活动分包");
return;
}
const testNode = cc.instantiate(prefab);
testNode.active = false;
const panel: any = testNode.addComponent(TestPanel);
panel.initialize(() => this.giftLoading || this.querying || !!(this.giftNode && cc.isValid(this.giftNode) && this.giftNode.active), () => this.queryActivity(true));
this.testPanelNode = testNode;
this.testBundle = bundle;
canvas.addChild(testNode, 3000);
testNode.active = true;
});
});
}
// 移除监听,同时清理弹窗和分包资源。
onDestroy() {
if (this.testPanelNode && cc.isValid(this.testPanelNode)) this.testPanelNode.destroy();
this.testPanelNode = null;
cc.game.off(cc.game.EVENT_SHOW, this.onGameShow, this);
this.releaseGiftAssets(true);
if (this.testBundle) this.releaseBundle(this.testBundle);
this.testBundle = null;
}
// 等待登录 UID 就绪后查询,避免并发重复请求。
private queryActivity(allowAutoOpen: boolean) {
if (this.querying) return;
if (!SevenDayGiftBootstrapApi.getUid()) {
if (this.retryCount < 10) {
this.retryCount++;
this.scheduleOnce(() => this.queryActivity(allowAutoOpen), 1);
} else {
cc.warn("[SevenDayGiftHost] 10 秒内没有取得 uid");
}
return;
}
this.querying = true;
SevenDayGiftBootstrapApi.getInfo(response => {
this.querying = false;
if (!response || response.code !== 1 || !response.data) return;
this.retryCount = 0;
this.info = response.data;
if (this.giftView) this.giftView.updateRuntimeInfo(this.info);
if (allowAutoOpen && this.shouldAutoOpen()) this.openGift(true);
});
}
// 把七日活动加入主页弹窗队列。
private openGift(autoClaim: boolean) {
// 保留旧参数名;现在表示自动展示来源,不会自动领取。
const canvas = cc.find("Canvas");
if (!canvas) return;
HomePopupQueue.get(canvas).enqueue("sevenDayGift", 10, done => this.openQueuedGift(autoClaim, done));
}
// 轮到本活动时加载并显示,成功显示后才记录当天已弹。
private openQueuedGift(autoClaim: boolean, done: () => void) {
this.ensureGiftLoaded((error?: Error) => {
done();
if (error) {
cc.error("[SevenDayGiftHost] 七日活动分包加载失败", error);
return;
}
if (!this.info || !this.giftView) return;
if (autoClaim && !this.shouldAutoOpen()) return;
if (!this.initializeGiftView() || !this.giftView.show(autoClaim)) {
cc.error("[SevenDayGiftHost] 弹窗初始化或显示失败,未记录当天已弹");
this.releaseGiftAssets();
return;
}
if (autoClaim) this.markAutoOpened();
console.log("[SevenDayGiftHost] 弹窗已打开", { autoClaim });
});
}
// 把活动快照和关闭后的资源清理回调交给分包。
private initializeGiftView(): boolean {
if (!this.giftView) return false;
return this.giftView.initializeRuntime(this.info, () => this.releaseGiftAssets());
}
// 复用已加载视图;多个加载请求共用一次分包加载。
private ensureGiftLoaded(callback: (error?: Error) => void) {
if (this.giftView && this.giftNode && cc.isValid(this.giftNode)) {
callback();
return;
}
this.giftLoadCallbacks.push(callback);
if (this.giftLoading) return;
this.giftLoading = true;
const loadId = ++this.giftLoadId;
const loadPrefab = (bundle: cc.AssetManager.Bundle) => {
bundle.load(GIFT_PREFAB_PATH, cc.Prefab, (error: Error, prefab: cc.Prefab) => {
if (loadId !== this.giftLoadId || !cc.isValid(this.node, true)) {
this.releaseBundle(bundle);
return;
}
if (error || !prefab) {
this.releaseBundle(bundle);
this.finishGiftLoad(error || new Error("sevenDayGift prefab is missing"));
return;
}
let giftNode: cc.Node = null;
try {
giftNode = cc.instantiate(prefab);
giftNode.active = false;
giftNode.setPosition(0, 0);
this.node.addChild(giftNode);
} catch (instantiateError) {
if (giftNode && cc.isValid(giftNode)) giftNode.destroy();
this.releaseBundle(bundle);
this.finishGiftLoad(instantiateError as Error);
return;
}
const giftView = giftNode.getComponent("SevenDayGift") as any as SevenDayGiftView;
if (!giftView) {
giftNode.destroy();
this.releaseBundle(bundle);
this.finishGiftLoad(new Error("SevenDayGift component is missing"));
return;
}
this.giftBundle = bundle;
this.giftNode = giftNode;
this.giftView = giftView;
this.finishGiftLoad();
});
};
// 先加载活动分包的完整依赖,再解析 Prefab;已缓存的分包也需检查依赖。
const canvas = cc.find("Canvas");
const home: any = canvas && canvas.getComponent("JiaZai");
if (!home || typeof home.loadHomeBundleWithDependencies !== "function") {
this.finishGiftLoad(new Error("主页分包加载器不可用"));
return;
}
home.loadHomeBundleWithDependencies(BUNDLE_NAME, 5, (error: Error, bundle: cc.AssetManager.Bundle) => {
if (loadId !== this.giftLoadId || !cc.isValid(this.node, true)) {
if (bundle) this.releaseBundle(bundle);
return;
}
if (error || !bundle) {
this.finishGiftLoad(error || new Error("seven_day_gift bundle is missing"));
return;
}
loadPrefab(bundle);
});
}
// 统一通知本次等待加载的调用方。
private finishGiftLoad(error?: Error) {
this.giftLoading = false;
const callbacks = this.giftLoadCallbacks.splice(0);
callbacks.forEach(callback => callback(error));
}
// 使旧加载回调失效,并在节点销毁后释放资源。
private releaseGiftAssets(immediate: boolean = false) {
this.giftLoadId++;
this.giftLoadCallbacks.length = 0;
if (this.giftNode && cc.isValid(this.giftNode)) {
this.giftNode.active = false;
this.giftNode.removeFromParent();
this.giftNode.destroy();
}
this.giftNode = null;
this.giftView = null;
this.info = null;
const bundle = this.giftBundle;
this.giftBundle = null;
if (!bundle) return;
const release = () => {
if (this.giftBundle === bundle) return;
this.releaseBundle(bundle);
};
if (immediate) release();
else this.scheduleOnce(release, 0);
}
// 仅释放当前仍登记在资源管理器中的活动分包。
private releaseBundle(bundle: cc.AssetManager.Bundle) {
// 测试入口仍持有本包资源时保留 Bundle,随主页销毁释放。
if (this.testPanelNode && cc.isValid(this.testPanelNode, true)) return;
if (!bundle || cc.assetManager.getBundle(BUNDLE_NAME) !== bundle) return;
bundle.releaseAll();
if (cc.assetManager.getBundle(BUNDLE_NAME) === bundle) {
cc.assetManager.removeBundle(bundle);
}
}
// 根据触发条件、活动状态和服务端时间判断是否可展示。
private isActivityVisible(): boolean {
if (!this.info || !this.info.activityId || !this.info.triggerReached) return false;
if (this.info.status === "expired" || this.info.status === "completed") return false;
const serverNow = this.toTime(this.info.serverNow);
const endAt = this.toTime(this.info.endAt);
return !serverNow || !endAt || serverNow < endAt;
}
// 今天可领且当前账号当天未弹出过,才自动展示。
private shouldAutoOpen(): boolean {
if (!this.isActivityVisible() || !this.info.canClaim || this.info.todayClaimed) {
console.log("[SevenDayGiftHost] 跳过弹窗:活动不可见或今天不可领取");
return false;
}
const key = this.getAutoOpenStorageKey();
const recordedDay = cc.fx.StorageMessage.getStorage(key);
const serverDay = this.getServerDayKey();
const shouldOpen = recordedDay !== serverDay;
console.log("[SevenDayGiftHost] 自动弹窗判断", JSON.stringify({ key, recordedDay, serverDay, shouldOpen }));
return shouldOpen;
}
// 只记录已展示,不代表奖励已经领取。
private markAutoOpened() {
cc.fx.StorageMessage.setStorage(this.getAutoOpenStorageKey(), this.getServerDayKey());
}
// 按账号和活动隔离自动弹窗记录。
private getAutoOpenStorageKey(): string {
return AUTO_OPEN_KEY + ":" + SevenDayGiftBootstrapApi.getUid() + ":" + this.info.activityId;
}
// 按北京时间划分服务端日期,避免设备时区影响。
private getServerDayKey(): string {
const time = this.toTime(this.info.serverNow) || Date.now();
return Math.floor((time + 8 * 60 * 60 * 1000) / (24 * 60 * 60 * 1000)).toString();
}
// 回到前台时刷新活动;正在展示时继续使用原快照。
private onGameShow() {
// 弹窗正在使用本次查询结果,切前后台不重新查询或重复展示。
if (this.giftLoading || (this.giftNode && cc.isValid(this.giftNode) && this.giftNode.active)) return;
if (!CC_PREVIEW) this.queryActivity(true);
}
// 兼容秒、毫秒时间戳和日期字符串。
private toTime(value: string | number): number {
if (typeof value === "number") return value < 100000000000 ? value * 1000 : value;
if (!value) return 0;
const parsed = new Date(value).getTime();
return isNaN(parsed) ? 0 : parsed;
}
}