332 lines
20 KiB
TypeScript
332 lines
20 KiB
TypeScript
import Service from "./module/Config/PyramidService";
|
|
import ActivityScheduleService, { PyramidDailyReminder } from "./module/Config/ActivityScheduleService";
|
|
import HomePopupQueue from "../home_popup_queue/HomePopupQueue";
|
|
import LoadingCatAnimation from "./LoadingCatAnimation";
|
|
const { ccclass } = cc._decorator;
|
|
|
|
@ccclass
|
|
export default class PyramidView extends cc.Component {
|
|
private entry: cc.Node;
|
|
private overlay: cc.Node = null;
|
|
private unsubscribe: () => void;
|
|
private unsubscribeSchedule: () => void;
|
|
private needsCheck = true;
|
|
private checking = false;
|
|
private opening = false;
|
|
private deliveryQueued = false;
|
|
private stopPaymentLoading: () => void = null;
|
|
private dailyReminderQueued = false;
|
|
private attemptedDailyReminder = "";
|
|
private finishReminder: () => void = null;
|
|
private entryClock = 0;
|
|
private releaseResources: () => void = null;
|
|
private settlementQueued = false;
|
|
private settlementRoot: cc.Node = null;
|
|
private finishSettlement: () => void = null;
|
|
private settlementRetries = 0;
|
|
private settlementRetryPending = false;
|
|
private boundary = "";
|
|
private account = "";
|
|
static mount(host: cc.Node): void { if (!host.getComponent(PyramidView)) host.addComponent(PyramidView); }
|
|
onLoad(): void {
|
|
const load = this.node.getChildByName("Load"), top = load && load.getChildByName("Top");
|
|
this.entry = new cc.Node("pyramid"); (top || this.node).addChild(this.entry); this.entry.active = false;
|
|
const goldMinerEntry = top && top.getChildByName("CoinMadnessEntry");
|
|
for (const name of ["timeBg", "time", "red"]) {
|
|
const template = goldMinerEntry && goldMinerEntry.getChildByName(name);
|
|
if (template) { const child = cc.instantiate(template); this.entry.addChild(child); child.active = false; }
|
|
}
|
|
const button = this.entry.addComponent(cc.Button); button.transition = cc.Button.Transition.SCALE;
|
|
this.entry.on("click", () => { void this.open(); });
|
|
this.unsubscribe = Service.subscribe(() => { this.checkBoundary(); this.render(); this.syncPaymentLoading(); });
|
|
this.unsubscribeSchedule = ActivityScheduleService.subscribe(() => this.render());
|
|
cc.game.on(cc.game.EVENT_SHOW, this.wake, this); this.checkBoundary(); this.wake();
|
|
}
|
|
onEnable(): void { this.wake(); }
|
|
private wake = (): void => {
|
|
this.needsCheck = true; this.attemptedDailyReminder = "";
|
|
this.settlementRetries = 0; this.settlementRetryPending = false; this.unschedule(this.retrySettlement);
|
|
};
|
|
private retrySettlement = (): void => { this.settlementRetryPending = true; };
|
|
private retrySettlementLater(): void {
|
|
const delays = [2, 3, 5, 7, 10, 15, 25, 30, 50];
|
|
if (!cc.isValid(this.node, true) || this.settlementRetries >= delays.length) return;
|
|
this.unschedule(this.retrySettlement);
|
|
this.scheduleOnce(this.retrySettlement, delays[this.settlementRetries++]);
|
|
}
|
|
private checkBoundary(): void {
|
|
const gm = cc.fx.GameConfig.GM_INFO, account = String(gm.uid || "") + ":" + String(gm.token || "");
|
|
if (this.account !== account) { this.account = account; this.boundary = ""; this.wake(); }
|
|
const state = Service.data();
|
|
if (!state) return;
|
|
const now = Service.now(), phase = now >= state.endAt ? "ended" : now >= state.startAt ? "started" : "waiting";
|
|
const boundary = state.periodId + ":" + phase;
|
|
if (this.boundary && this.boundary !== boundary) this.wake();
|
|
this.boundary = boundary;
|
|
}
|
|
private closeOverlay = (): void => {
|
|
if (this.overlay && cc.isValid(this.overlay, true)) this.overlay.destroy();
|
|
this.overlay = null;
|
|
if (this.releaseResources) { this.releaseResources(); this.releaseResources = null; }
|
|
if (this.finishReminder) { const done = this.finishReminder; this.finishReminder = null; done(); }
|
|
};
|
|
private render(): void {
|
|
const period = ActivityScheduleService.active("pyramid");
|
|
const s = Service.data(), ended = (ActivityScheduleService.hasSchedule() && !period && !s)
|
|
|| !!s && (s.status !== "active" || Service.now() >= s.endAt);
|
|
if (this.settlementRoot && !Service.settlements().length && this.finishSettlement) this.finishSettlement();
|
|
this.entry.active = !!period && Number(cc.fx.GameConfig.GM_INFO.level) >= period.unlockLevel
|
|
&& !(s && s.periodId === period.periodId && s.completed);
|
|
if (ended) this.closeOverlay();
|
|
const home: any = this.node.getComponent("JiaZai");
|
|
if (this.entry.active && !this.entry.getComponent(cc.Sprite) && home && home.startUI2) {
|
|
const frame = home.startUI2.getSpriteFrame("pyramid.png") || home.startUI2.getSpriteFrame("pyramid");
|
|
if (frame) {
|
|
const sprite = this.entry.addComponent(cc.Sprite); sprite.spriteFrame = frame;
|
|
sprite.sizeMode = cc.Sprite.SizeMode.RAW;
|
|
}
|
|
}
|
|
if (this.entry.active) void Service.prefab().catch(error => cc.warn("[Pyramid] 首页预加载失败,打开时重试", error));
|
|
const badge = this.entry.getChildByName("red");
|
|
if (badge) badge.active = this.entry.active && !!s && !!period && s.periodId === period.periodId
|
|
&& ((s.pendingGrants || []).length > 0 || (s.levels || []).some(level =>
|
|
(level.rewards || []).some(reward => reward.deliveryStatus === "unclaimed" || reward.deliveryStatus === "pending")));
|
|
this.refreshCountdown();
|
|
if (home && home.updateCoin) home.updateCoin();
|
|
}
|
|
private refreshCountdown(): void {
|
|
const period = ActivityScheduleService.active("pyramid");
|
|
const remaining = period ? Math.max(0, Math.ceil((period.endsAt - ActivityScheduleService.now()) / 1000)) : 0;
|
|
const time = this.entry.getChildByName("time"), background = this.entry.getChildByName("timeBg");
|
|
if (background) background.active = remaining > 0;
|
|
if (!time) return;
|
|
time.active = remaining > 0;
|
|
const label = time.getComponent(cc.Label), pad = (n: number) => n < 10 ? "0" + n : String(n);
|
|
if (label) label.string = !remaining ? "" : remaining >= 86400
|
|
? Math.floor(remaining / 86400) + "天" + Math.floor(remaining % 86400 / 3600) + "时"
|
|
: pad(Math.floor(remaining / 3600)) + ":" + pad(Math.floor(remaining % 3600 / 60)) + ":" + pad(remaining % 60);
|
|
}
|
|
private reminderCurrent(reminder: PyramidDailyReminder): boolean {
|
|
const current = ActivityScheduleService.pyramidReminder(), state = Service.data();
|
|
return !!(cc.isValid(this.node, true) && this.entry.active && !Service.deliveryBusy
|
|
&& !(state && state.pendingGrants && state.pendingGrants.length)
|
|
&& current && current.key === reminder.key && current.day === reminder.day && current.periodId === reminder.periodId);
|
|
}
|
|
private queueDailyReminder(): void {
|
|
if (!this.entry.active || this.overlay || this.opening || Service.busy || this.deliveryQueued
|
|
|| this.dailyReminderQueued || this.settlementQueued || Service.settlements().length) return;
|
|
const reminder = ActivityScheduleService.pyramidReminder();
|
|
if (!reminder || !this.reminderCurrent(reminder)) return;
|
|
const attemptKey = JSON.stringify([reminder.key, reminder.day, reminder.periodId]);
|
|
if (this.attemptedDailyReminder === attemptKey) return;
|
|
this.dailyReminderQueued = true;
|
|
HomePopupQueue.get(this.node).enqueue("pyramidDailyReminder", 11, done => {
|
|
this.dailyReminderQueued = false;
|
|
if (!this.reminderCurrent(reminder) || this.overlay || this.opening || Service.busy || this.deliveryQueued) { done(); return; }
|
|
// A failed automatic popup retries only on the next home/foreground visit.
|
|
this.attemptedDailyReminder = attemptKey;
|
|
this.finishReminder = done;
|
|
void this.open(reminder);
|
|
});
|
|
}
|
|
private async open(reminder?: PyramidDailyReminder): Promise<void> {
|
|
const alive = () => cc.isValid(this, true) && cc.isValid(this.node, true);
|
|
if (!alive() || this.overlay || this.opening || this.settlementQueued) return; this.opening = true;
|
|
// Prepare automatic reminders silently; only an explicit click shows Loading.
|
|
const stopLoading = reminder ? () => { } : this.startLoading();
|
|
try {
|
|
const release = await Service.acquireResources();
|
|
if (!alive()) { release(); return; }
|
|
this.releaseResources = release;
|
|
const [synced, prefab] = await Promise.all([Service.sync(), Service.prefab()]);
|
|
if (!alive()) return;
|
|
const state = Service.data();
|
|
if (!synced && !reminder && typeof window !== "undefined" && (window as any).wx)
|
|
(window as any).wx.showToast({ title: Service.error || "活动信息暂时无法获取", icon: "none" });
|
|
if (!synced || !cc.isValid(this.node, true) || !this.entry.active || !state || state.status !== "active" || Service.now() >= state.endAt || state.completed) return;
|
|
if (reminder && !this.reminderCurrent(reminder)) return;
|
|
this.overlay = cc.instantiate(prefab); this.overlay.name = "PyramidPanel"; this.overlay.active = false;
|
|
this.node.addChild(this.overlay); this.overlay.zIndex = 2000;
|
|
const panel: any = this.overlay.getComponent("PyramidPanel");
|
|
await panel.initialize(this.closeOverlay);
|
|
if (!alive()) return;
|
|
if (!cc.isValid(panel, true) || !cc.isValid(this.overlay, true)) { this.closeOverlay(); return; }
|
|
if (reminder && !this.reminderCurrent(reminder)) { this.closeOverlay(); return; }
|
|
if (this.overlay && cc.isValid(this.overlay, true)) {
|
|
this.overlay.active = true;
|
|
if (reminder) ActivityScheduleService.markPyramidReminderShown(reminder);
|
|
}
|
|
} catch (error) {
|
|
console.error("[Pyramid] 界面加载失败", error);
|
|
if (alive()) {
|
|
cc.warn("[Pyramid] 界面加载失败", error);
|
|
this.closeOverlay();
|
|
}
|
|
} finally {
|
|
stopLoading();
|
|
if (alive()) {
|
|
this.opening = false; this.syncPaymentLoading();
|
|
if (!this.overlay && this.releaseResources) { this.releaseResources(); this.releaseResources = null; }
|
|
if (!this.overlay && this.finishReminder) this.closeOverlay();
|
|
}
|
|
}
|
|
}
|
|
private startLoading(): () => void {
|
|
const template = this.node.getChildByName("Loading");
|
|
if (!template || !template.parent) return () => { };
|
|
const parent = template.parent, size = parent.getContentSize();
|
|
const factor = Math.min(1, size.width / 1080, size.height / 1920);
|
|
const viewport = new cc.Node("PyramidLoading");
|
|
viewport.setContentSize(size.width / factor, size.height / factor);
|
|
viewport.scale = factor; parent.addChild(viewport); viewport.zIndex = cc.macro.MAX_ZINDEX;
|
|
viewport.addComponent(cc.BlockInputEvents);
|
|
const loading = cc.instantiate(template); viewport.addChild(loading);
|
|
LoadingCatAnimation.play(loading);
|
|
return () => {
|
|
if (!cc.isValid(viewport, true)) return;
|
|
LoadingCatAnimation.stop(loading); viewport.destroy();
|
|
};
|
|
}
|
|
private syncPaymentLoading(): void {
|
|
if (Service.paymentConfirming && !this.stopPaymentLoading && !this.opening) this.stopPaymentLoading = this.startLoading();
|
|
else if (!Service.paymentConfirming && this.stopPaymentLoading) {
|
|
this.stopPaymentLoading(); this.stopPaymentLoading = null;
|
|
}
|
|
}
|
|
private async check(): Promise<void> {
|
|
this.checking = true;
|
|
try {
|
|
this.queueSettlement();
|
|
await Service.sync();
|
|
if (this.overlay && cc.isValid(this.overlay, true)) {
|
|
const panel: any = this.overlay.getComponent("PyramidPanel");
|
|
if (panel) await panel.recoverDelivery();
|
|
}
|
|
if (!this.overlay && !this.deliveryQueued && await Service.pendingDelivery() && cc.isValid(this.node, true)) {
|
|
this.deliveryQueued = true;
|
|
HomePopupQueue.get(this.node).enqueue("pyramidDelivery", 5, done => {
|
|
if (!cc.isValid(this.node, true)) { done(); return; }
|
|
// A short-lived input shield keeps unrelated gameplay inventory writes out of this save window.
|
|
const shield = new cc.Node("PyramidDelivery"); this.node.addChild(shield); shield.zIndex = 2500;
|
|
shield.setContentSize(cc.view.getVisibleSize()); shield.addComponent(cc.BlockInputEvents);
|
|
const finish = () => {
|
|
if (cc.isValid(shield, true)) shield.destroy(); this.deliveryQueued = false; done(); this.render();
|
|
};
|
|
void Service.deliver().then(finish, finish);
|
|
});
|
|
}
|
|
} catch (_) { /* Network errors retry on the next bounded refresh, never prevent login. */ }
|
|
finally { this.checking = false; }
|
|
}
|
|
private queueSettlement(): void {
|
|
if (this.settlementQueued || !cc.isValid(this.node, true)) return;
|
|
this.settlementQueued = true;
|
|
HomePopupQueue.get(this.node).enqueue("pyramidSettlement", 4, done => {
|
|
void this.showSettlements(done);
|
|
});
|
|
}
|
|
private async showSettlements(done: () => void): Promise<void> {
|
|
if (!cc.isValid(this.node, true)) { this.settlementQueued = false; done(); return; }
|
|
let release: () => void = null, finished = false;
|
|
const shield = new cc.Node("PyramidSettlementLoading"); this.node.addChild(shield); shield.zIndex = 2199;
|
|
shield.setContentSize(cc.view.getVisibleSize()); shield.addComponent(cc.BlockInputEvents);
|
|
const finish = () => {
|
|
if (finished) return; finished = true;
|
|
if (this.settlementRoot && cc.isValid(this.settlementRoot, true)) this.settlementRoot.destroy();
|
|
if (cc.isValid(shield, true)) shield.destroy();
|
|
this.settlementRoot = null; this.finishSettlement = null;
|
|
if (release) { release(); release = null; }
|
|
this.settlementQueued = false; done();
|
|
};
|
|
this.finishSettlement = finish;
|
|
try {
|
|
const settled = await Service.readSettlements();
|
|
if (!settled) this.retrySettlementLater();
|
|
if (finished || !cc.isValid(this.node, true) || !Service.settlements().length) { finish(); return; }
|
|
const lease = await Service.acquireResources();
|
|
if (finished || !cc.isValid(this.node, true)) { lease(); finish(); return; }
|
|
release = lease;
|
|
const bundle = await Service.bundle();
|
|
const prefab: cc.Prefab = await new Promise((resolve, reject) =>
|
|
bundle.load("prefab/PyramidSettlement", cc.Prefab, (error, asset: cc.Prefab) => error ? reject(error) : resolve(asset)));
|
|
const publicBundle = cc.assetManager.getBundle("publicUI") || await new Promise<cc.AssetManager.Bundle>((resolve, reject) =>
|
|
cc.assetManager.loadBundle("publicUI", (error, asset) => error ? reject(error) : resolve(asset)));
|
|
const rewardPrefab: cc.Prefab = await new Promise((resolve, reject) =>
|
|
publicBundle.load("prefab/RewardWindow", cc.Prefab, (error, asset: cc.Prefab) => error ? reject(error) : resolve(asset)));
|
|
const next = () => {
|
|
if (finished || !cc.isValid(this.node, true)) { finish(); return; }
|
|
const notice = Service.settlements()[0];
|
|
if (!notice) { finish(); return; }
|
|
if (this.settlementRoot && cc.isValid(this.settlementRoot, true)) this.settlementRoot.destroy();
|
|
this.settlementRoot = cc.instantiate(prefab); this.node.addChild(this.settlementRoot); this.settlementRoot.zIndex = 2200;
|
|
const panel: any = this.settlementRoot.getComponent("PyramidSettlement");
|
|
let dismissed = false;
|
|
panel.show(notice, async claim => {
|
|
if (dismissed || finished || !cc.isValid(this.node, true)) return;
|
|
if (!claim) {
|
|
dismissed = true;
|
|
this.settlementRoot.active = false; this.settlementRoot.destroy(); this.settlementRoot = null;
|
|
Service.dismissSettlement(notice.noticeId); next(); return;
|
|
}
|
|
let claimed: any;
|
|
try { claimed = await Service.claimSettlement(notice); }
|
|
catch (error) {
|
|
if (!finished && cc.isValid(this.node, true)) {
|
|
cc.warn("[Pyramid] 补发奖励领取失败", error);
|
|
if (typeof window !== "undefined" && (window as any).wx)
|
|
(window as any).wx.showToast({ title: Service.error || "领取失败,请重试", icon: "none" });
|
|
}
|
|
throw error;
|
|
}
|
|
if (finished || !cc.isValid(this.node, true)) return;
|
|
dismissed = true;
|
|
this.settlementRoot.active = false;
|
|
this.settlementRoot.destroy(); this.settlementRoot = null;
|
|
let animationDone = false;
|
|
const complete = () => {
|
|
if (animationDone || finished) return;
|
|
animationDone = true;
|
|
if (this.settlementRoot && cc.isValid(this.settlementRoot, true)) {
|
|
this.settlementRoot.active = false; this.settlementRoot.destroy();
|
|
}
|
|
this.settlementRoot = null;
|
|
Service.dismissSettlement(notice.noticeId); next();
|
|
};
|
|
// The explicit claim credited these goods; Reward only presents them.
|
|
const types = { 1001: "coin", 2001: "freeze", 2002: "hammer", 2003: "magic", 2005: "infinite_health" };
|
|
const rewards = claimed.goods.map(good => ({ type: types[good.id], count: good.count }));
|
|
if (!rewards.length) { complete(); return; }
|
|
this.settlementRoot = cc.instantiate(rewardPrefab); this.node.addChild(this.settlementRoot);
|
|
this.settlementRoot.setPosition(0, 0); this.settlementRoot.zIndex = 2200;
|
|
const widget = this.settlementRoot.getComponent(cc.Widget); if (widget) widget.updateAlignment();
|
|
this.settlementRoot.getComponent("Reward").init(rewards, complete, true);
|
|
});
|
|
};
|
|
next();
|
|
} catch (error) { cc.warn("[Pyramid] 补发清单弹窗加载失败", error); finish(); }
|
|
}
|
|
update(dt = 0): void {
|
|
this.syncPaymentLoading();
|
|
this.entryClock += dt;
|
|
if (this.entryClock >= .5) { this.entryClock = 0; this.checkBoundary(); this.refreshCountdown(); this.queueDailyReminder(); }
|
|
const state = Service.data();
|
|
if ((this.entry.active || this.overlay) && (!state || state.status !== "active" || Service.now() >= state.endAt)) this.render();
|
|
if (!this.checking && !Service.busy && !this.settlementQueued && this.settlementRetryPending) {
|
|
this.settlementRetryPending = false;
|
|
if (!this.needsCheck) this.queueSettlement();
|
|
}
|
|
if (this.checking || Service.busy || !this.needsCheck) return;
|
|
this.needsCheck = false;
|
|
void this.check();
|
|
}
|
|
onDestroy(): void {
|
|
this.unschedule(this.retrySettlement);
|
|
if (this.finishSettlement) this.finishSettlement();
|
|
this.closeOverlay();
|
|
if (this.unsubscribe) this.unsubscribe(); cc.game.off(cc.game.EVENT_SHOW, this.wake, this);
|
|
if (this.unsubscribeSchedule) this.unsubscribeSchedule();
|
|
if (this.stopPaymentLoading) this.stopPaymentLoading();
|
|
if (this.entry && cc.isValid(this.entry, true)) this.entry.destroy();
|
|
}
|
|
}
|