MatchMaster/assets/Script/CloudRiseView.ts
2026-09-24 19:33:11 +08:00

334 lines
18 KiB
TypeScript

import CloudRiseService from "./module/Config/CloudRiseService";
import ActivityScheduleService from "./module/Config/ActivityScheduleService";
import { MiniGameSdk } from "./Sdk/MiniGameSdk";
import HomePopupQueue from "../home_popup_queue/HomePopupQueue";
const { ccclass } = cc._decorator;
@ccclass
export default class CloudRiseView extends cc.Component {
private host: cc.Node;
private play: () => void;
private map: any = null;
private entry: cc.Node;
private entryCountdown: cc.Label = null;
private overlay: cc.Node = null;
private unsubscribe: () => void;
private unsubscribeSchedule: () => void;
private panel: any = null;
private busy = false;
private priorPause = false;
private queuedResult = "";
private popupDone: () => void = null;
private displayedCoin = -1;
private nextSyncAt = 0;
private timeState = "";
private queuedElimination = "";
private eliminationDone: () => void = null;
private expiryPending = false;
/** All win continuations present pending results; only the activity overview starts another stage. */
static async continueFromNextLevel(host: cc.Node): Promise<boolean> {
const current = CloudRiseService.run();
if (!current || !["playing", "waiting"].includes(current.status)
&& !CloudRiseService.elimination() && !CloudRiseService.unseenResult()) return true;
try {
if (!await CloudRiseService.sync()) return true; // The durable queue retries in the background.
if (CloudRiseService.challengeExpired()) { CloudRiseService.dismissExpiryPresentation(); return true; }
// Includes the NewMode confirmation path and delayed finish responses.
// Keep the level transition waiting while the result and overview are displayed.
await CloudRiseView.playElimination(host);
if (CloudRiseService.elimination() || CloudRiseService.unseenResult()) {
MiniGameSdk.API.showToast("活动结算尚未展示完成,请重试"); return false;
}
} catch (error) { cc.warn(error); return true; }
return true;
}
/** Network delays never hold the main level settlement indefinitely. */
static async playElimination(host: cc.Node): Promise<void> {
const entry = host && host.getChildByName("CloudRiseEntry");
const view = entry && entry.getComponent(CloudRiseView);
if (!view || !CloudRiseService.run() || !CloudRiseService.elimination() && !CloudRiseService.unseenResult()
&& CloudRiseService.run().status !== "playing") return;
let timer: any;
try {
await Promise.race([CloudRiseService.sync(), new Promise(resolve => { timer = setTimeout(resolve, 2500); })]);
if (CloudRiseService.challengeExpired()) { CloudRiseService.dismissExpiryPresentation(); return; }
while (cc.isValid(view.node, true) && (CloudRiseService.elimination() || CloudRiseService.unseenResult())) {
const item = CloudRiseService.elimination(), resultKey = CloudRiseService.resultKey();
await view.showElimination();
if (item ? CloudRiseService.elimination() && CloudRiseService.elimination().key === item.key
: CloudRiseService.unseenResult() && CloudRiseService.resultKey() === resultKey) break;
}
} catch (error) { cc.warn(error); }
finally { clearTimeout(timer); }
}
static mount(host: cc.Node, play: () => void, map?: any): CloudRiseView {
const existing = host.getChildByName("CloudRiseEntry");
if (existing) return existing.getComponent(CloudRiseView);
const node = new cc.Node("CloudRiseEntry"); host.addChild(node);
const view = node.addComponent(CloudRiseView);
view.host = host; view.play = play; view.map = map || null;
view.initialize(); return view;
}
private initialize(): void {
this.node.zIndex = 0;
const home: any = !this.map && this.host.getComponent("JiaZai");
const load = !this.map && this.host.getChildByName("Load"), top = load && load.getChildByName("Top");
this.entry = new cc.Node("cloudRise");
(top || this.node).addChild(this.entry); this.entry.active = false;
if (home && home.startUI2) {
const sprite = this.entry.addComponent(cc.Sprite);
sprite.spriteFrame = home.startUI2.getSpriteFrame("cloudRise.png") || home.startUI2.getSpriteFrame("cloudRise");
sprite.sizeMode = cc.Sprite.SizeMode.RAW;
const button = this.entry.addComponent(cc.Button);
button.transition = cc.Button.Transition.SCALE; button.zoomScale = 1.1;
}
const gift = top && top.getChildByName("xinshou");
if (gift) {
// Reuse the lucky-gift clock artwork and typography, aligned to the icon's bottom edge.
["New Sprite", "time"].forEach(name => {
const source = gift.getChildByName(name);
if (!source) return;
const node = cc.instantiate(source);
this.entry.addChild(node); node.active = true;
node.y += (gift.height - this.entry.height) / 2;
if (name === "time") {
this.entryCountdown = node.getComponent(cc.Label);
this.entryCountdown.string = "00:00:00";
}
});
}
this.entry.on("click", () => { void this.open(); });
this.unsubscribe = CloudRiseService.subscribe(() => this.refresh());
if (!this.map) this.unsubscribeSchedule = ActivityScheduleService.subscribe(() => this.tick());
this.schedule(this.tick, 1);
cc.game.on(cc.game.EVENT_SHOW, this.onShow, this);
this.onShow();
}
private onShow = (): void => { this.nextSyncAt = 0; this.tick(); };
private tick = (): void => {
if (this.expiryPending) {
if (!this.busy && Date.now() >= this.nextSyncAt) void this.showExpiry();
return;
}
this.refresh();
const period = !this.map && ActivityScheduleService.active("cloudRise");
if (period && this.entry.active) CloudRiseService.preloadPublicEntry(period);
// Opening a page explicitly syncs its details. Idle home only polls unfinished personal work.
if (!this.overlay && !CloudRiseService.needsPersonalSync()) {
this.timeState = ""; this.nextSyncAt = 0; return;
}
const run = CloudRiseService.run();
const state = (run ? run.id + ":" + run.status : "pending") + ":" + CloudRiseService.challengeExpired();
if (state !== this.timeState || Date.now() >= this.nextSyncAt) {
this.timeState = state; this.nextSyncAt = Date.now() + 30000;
void CloudRiseService.sync();
}
};
private refresh(): void {
if (!this.node || !cc.isValid(this.node)) return;
if (!this.map || !this.overlay) CloudRiseService.dismissExpiryPresentation();
this.entry.active = !this.map && CloudRiseService.homeEntryVisible(ActivityScheduleService.active("cloudRise"),
ActivityScheduleService.hasSchedule(), ActivityScheduleService.now());
this.updateEntryCountdown();
if (!this.map && this.displayedCoin !== cc.fx.GameConfig.GM_INFO.coin) {
this.displayedCoin = cc.fx.GameConfig.GM_INFO.coin;
const home: any = this.host.getComponent("JiaZai");
if (home && home.updateCoin) home.updateCoin();
}
const elimination = CloudRiseService.elimination();
const pendingLoss = CloudRiseService.hasPendingLoss();
const eliminationKey = elimination ? elimination.key : pendingLoss ? "pendingLoss" : "";
if (!this.map && eliminationKey && !this.overlay && !this.queuedElimination) {
this.queuedElimination = eliminationKey;
HomePopupQueue.get(this.host).enqueue("cloudRiseElimination", 0, done => {
// Reserve the first popup while the abandon response is still in flight.
void (async () => {
try {
if (CloudRiseService.hasPendingLoss()) await CloudRiseService.sync();
if (cc.isValid(this.node, true)) await this.showElimination();
} finally { this.queuedElimination = ""; done(); }
})().catch(error => cc.warn(error));
});
}
const key = CloudRiseService.resultKey();
if (!this.map && !elimination && !pendingLoss && !this.overlay && CloudRiseService.allowsAutomaticResults() && CloudRiseService.unseenResult() && key !== this.queuedResult) {
this.queuedResult = key;
HomePopupQueue.get(this.host).enqueue("cloudRiseResult", 1, done => {
this.popupDone = done;
void this.open("result").then(() => { if (!this.overlay) this.finishPopup(); }, () => this.finishPopup());
});
}
}
private async showElimination(reuseCurrent = false): Promise<void> {
if (this.busy || this.overlay && !reuseCurrent) return;
// Recheck when a queued home popup runs: timeout may have arrived after enqueue.
if (!this.map) CloudRiseService.dismissExpiryPresentation();
if (!reuseCurrent && CloudRiseService.challengeExpired()) { CloudRiseService.dismissExpiryPresentation(); return; }
const item = CloudRiseService.elimination();
if (!item && !CloudRiseService.unseenResult()) return;
this.busy = true;
let loadTimer: any;
try {
const reuse = reuseCurrent && this.overlay && this.panel;
if (!reuse) {
const resources = Promise.all([CloudRiseService.pagePrefab(),
CloudRiseService.pageFragment("stage", item ? item.after.stage : CloudRiseService.stage().stage)]).then(([prefab]) => prefab);
const prefab = await Promise.race([resources, new Promise<cc.Prefab>((_, reject) => {
loadTimer = setTimeout(() => reject(new Error("百人赛动画资源待加载")), 2500);
})]);
clearTimeout(loadTimer);
if (!cc.isValid(this.node, true)) return;
if (CloudRiseService.challengeExpired()) { CloudRiseService.dismissExpiryPresentation(); return; }
if (!this.map) {
CloudRiseService.dismissExpiryPresentation();
if (!item && !CloudRiseService.unseenResult()) return;
}
this.overlay = cc.instantiate(prefab);
// Settings and other dynamic popups use MAX_ZINDEX - 2; settlement must cover them.
this.host.addChild(this.overlay); this.overlay.zIndex = cc.macro.MAX_ZINDEX - 1;
this.panel = this.overlay.getComponent("CloudRisePanel");
}
await this.panel.preparePage(item ? "elimination" : "result", item && item.after.stage);
if (!cc.isValid(this.node, true)) return;
const close = () => {
this.panel.acknowledge();
if (this.eliminationDone) this.eliminationDone();
};
if (reuse) this.panel.beginSettlement(item ? "elimination" : "result", close);
else this.panel.initialize(item ? "elimination" : "result", !!this.map, close, () => {});
await new Promise<void>(resolve => {
this.eliminationDone = resolve;
// The cloud motion may already be acknowledged while its stage result is still unseen.
if (!item) return;
this.panel.playElimination(item).then(completed => {
if (completed && cc.isValid(this.node, true)) CloudRiseService.acknowledgeElimination(item.key);
const stage = CloudRiseService.stage();
if (completed && cc.isValid(this.node, true) && ["won", "lost", "expired"].includes(item.after.status) && stage
&& stage.stage === item.after.stage && (stage.status === item.after.status
|| item.after.status === "expired" && CloudRiseService.challengeExpired()) && CloudRiseService.unseenResult()) {
this.panel.openResult();
} else if (completed && cc.isValid(this.node, true)) {
this.panel.waitForProgressContinue().then(() => resolve());
} else resolve();
}).catch(error => { cc.warn(error); resolve(); });
});
} catch (error) { cc.warn(error); }
finally {
clearTimeout(loadTimer); this.eliminationDone = null;
if (this.overlay && cc.isValid(this.overlay)) this.overlay.destroy();
this.overlay = null; this.panel = null; this.busy = false;
this.queuedElimination = "";
}
}
async open(mode?: string): Promise<void> {
if (this.busy || this.overlay) return;
this.busy = true;
try {
const synced = await CloudRiseService.sync();
if (!cc.isValid(this.node, true)) return;
if (!this.map) {
CloudRiseService.dismissExpiryPresentation();
if (mode === "result" && !CloudRiseService.unseenResult()) return;
}
if (!synced && mode !== "result") { MiniGameSdk.API.showToast(CloudRiseService.error || "活动信息待同步,请重试"); return; }
const canOpen = () => CloudRiseService.visible() || mode === "result" && CloudRiseService.unseenResult();
if (!canOpen()) { MiniGameSdk.API.showToast("活动未开启"); return; }
const locked = () => {
const level = CloudRiseService.lockedUntilLevel();
if (level) MiniGameSdk.API.showToast(`月光宝盒活动通过第${level}关后解锁`);
return !!level;
};
if (locked()) return;
const prefab = await CloudRiseService.pagePrefab();
const pageMode = mode || (CloudRiseService.unseenResult() ? "result"
: CloudRiseService.run() && CloudRiseService.run().status === "playing" ? "progress" : "overview");
if (pageMode !== "overview") await CloudRiseService.pageFragment("stage", CloudRiseService.stage().stage);
if (!this.map) CloudRiseService.dismissExpiryPresentation();
if (!cc.isValid(this.node, true) || !canOpen()) return;
if (locked()) return;
this.overlay = cc.instantiate(prefab);
this.host.addChild(this.overlay); this.overlay.zIndex = cc.macro.MAX_ZINDEX - 1;
this.panel = this.overlay.getComponent("CloudRisePanel");
await this.panel.preparePage(pageMode);
if (!cc.isValid(this.node, true)) return;
if (this.map) { this.priorPause = this.map.pause; this.map.pause = true; }
this.panel.initialize(pageMode, !!this.map, () => this.close(), () => { this.close(); if (!this.map) this.play(); },
() => { this.expiryPending = true; void this.showExpiry(); });
} catch (error) {
if (this.overlay && cc.isValid(this.overlay)) this.overlay.destroy();
this.overlay = null; this.panel = null;
MiniGameSdk.API.showToast("活动资源加载失败,请重试"); cc.warn(error);
}
finally { this.busy = false; }
}
/** Home closes quietly; an open in-game stage keeps its existing timeout presentation. */
private async showExpiry(): Promise<void> {
if (this.busy) return;
if (!this.map) {
this.expiryPending = false;
this.close();
this.refresh();
void CloudRiseService.sync();
return;
}
this.busy = true; this.nextSyncAt = Date.now() + 30000;
this.queuedElimination = "expiry";
try {
if (!await CloudRiseService.sync() || !cc.isValid(this.node, true)) return;
this.busy = false;
await this.showElimination(true);
this.expiryPending = !!(CloudRiseService.elimination() || CloudRiseService.unseenResult()
|| CloudRiseService.run() && CloudRiseService.run().status !== "expired" && CloudRiseService.challengeExpired());
} catch (error) {
cc.warn(error);
} finally {
this.busy = false; this.queuedElimination = "";
if (!this.overlay) {
if (this.map && cc.isValid(this.map.node) && !this.map.gameOver && !this.map.gameWin) this.map.pause = this.priorPause;
this.finishPopup();
}
}
}
private close(): void {
if (this.busy) return;
if (this.panel) this.panel.acknowledge();
if (this.overlay) this.overlay.destroy();
this.overlay = null; this.panel = null; this.finishPopup();
if (this.map && cc.isValid(this.map.node) && !this.map.gameOver && !this.map.gameWin) this.map.pause = this.priorPause;
}
private updateEntryCountdown(): void {
if (!this.entryCountdown) return;
const period = ActivityScheduleService.active("cloudRise"), run = CloudRiseService.run();
const now = ActivityScheduleService.hasSchedule() ? ActivityScheduleService.now() : CloudRiseService.now();
const activeRun = run && ["playing", "waiting"].includes(run.status) && now < run.expiresAt;
const end = activeRun ? run.expiresAt : period ? period.endsAt : 0;
const seconds = Math.max(0, Math.ceil((end - now) / 1000));
const pad = (value: number) => value < 10 ? "0" + value : String(value);
this.entryCountdown.string = [Math.floor(seconds / 3600), Math.floor(seconds / 60) % 60, seconds % 60]
.map(pad).join(":");
}
onDestroy(): void {
cc.game.off(cc.game.EVENT_SHOW, this.onShow, this);
this.unschedule(this.tick);
if (this.unsubscribe) this.unsubscribe();
if (this.unsubscribeSchedule) this.unsubscribeSchedule();
if (this.entry && cc.isValid(this.entry)) this.entry.destroy();
if (this.overlay && cc.isValid(this.overlay)) this.overlay.destroy();
if (this.eliminationDone) this.eliminationDone();
this.finishPopup();
}
private finishPopup(): void {
const done = this.popupDone; this.popupDone = null;
if (done) done();
}
}