304 lines
12 KiB
TypeScript
304 lines
12 KiB
TypeScript
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;
|
||
|
||
// 监听回到前台事件,并在正式环境查询活动。
|
||
onLoad() {
|
||
if (this.node.parent) this.node.setContentSize(this.node.parent.getContentSize());
|
||
cc.game.on(cc.game.EVENT_SHOW, this.onGameShow, this);
|
||
const gameTool = cc.fx && cc.fx.GameTool;
|
||
if (CC_PREVIEW || (gameTool && gameTool.getWechatGameVersion() === "开发版")) this.loadTestPanel();
|
||
|
||
if (CC_PREVIEW) return;
|
||
this.queryActivity(true);
|
||
}
|
||
|
||
// 移除监听,同时清理弹窗和分包资源。
|
||
onDestroy() {
|
||
if (this.testPanelNode && cc.isValid(this.testPanelNode)) this.testPanelNode.destroy();
|
||
cc.game.off(cc.game.EVENT_SHOW, this.onGameShow, this);
|
||
this.releaseGiftAssets(true);
|
||
}
|
||
|
||
// 开发环境按需加载接口测试面板。
|
||
private loadTestPanel() {
|
||
// 主包不静态引用测试脚本;发布构建可以直接排除 testSeven。
|
||
cc.assetManager.loadBundle("testSeven", error => {
|
||
if (!cc.isValid(this.node, true)) return;
|
||
if (error) {
|
||
cc.warn("[SevenDayGiftHost] 测试面板未加载(需要包含 testSeven Bundle)", error);
|
||
return;
|
||
}
|
||
const canvas = cc.find("Canvas");
|
||
if (!canvas) return;
|
||
const node = new cc.Node("SevenApiTestPanel");
|
||
canvas.addChild(node);
|
||
node.setPosition(0, 0);
|
||
node.zIndex = 3000;
|
||
this.testPanelNode = node;
|
||
const panel: any = node.addComponent("SevenApiPanel");
|
||
panel.testHome = (info: SevenDayActivityInfo) => {
|
||
if (this.giftLoading || (this.giftNode && this.giftNode.active)) {
|
||
cc.warn("[SevenDayGiftHost] 请先关闭当前七日活动,再测试自动弹窗");
|
||
return;
|
||
}
|
||
this.info = info;
|
||
if (this.shouldAutoOpen()) this.openGift(true);
|
||
};
|
||
});
|
||
}
|
||
|
||
// 等待登录 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) {
|
||
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;
|
||
}
|
||
}
|