334 lines
18 KiB
TypeScript
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();
|
|
}
|
|
}
|