import Utils from "../Pay/Utils"; /** Main-package bridge; the lazy bundle registers the activity implementation. */ export default class CloudRiseService { private static runtime: any = null; private static bundleTask: Promise = null; private static prefabTask: Promise = null; private static checkTask: Promise = null; private static snapshot: any = null; private static uid = ""; private static offset = 0; private static message = ""; private static autoDisplay = false; private static listeners: Array<() => void> = []; static install(runtime: any): void { this.runtime = runtime; runtime.subscribe(() => this.emit()); } private static ensureUser(): void { // Login stores users.onlyId in userId; GM_INFO.uid is the legacy Mongo identifier. const uid = String(cc.fx.GameConfig.GM_INFO.userId || ""); if (uid === this.uid) return; this.uid = uid; this.snapshot = null; this.checkTask = null; this.message = ""; this.autoDisplay = false; } static get error(): string { return this.runtime ? this.runtime.error : this.message; } static data(): any { this.ensureUser(); return this.runtime ? this.runtime.data() : this.snapshot; } static run(): any { return this.currentRun(this.data()); } /** Also handles responses from older servers that return a settled run alongside the new period. */ static currentRun(data: any): any { const run = data && data.run, period = data && data.period; if (run && period && period.periodId !== run.periodId && ["completed", "failed", "expired"].includes(run.status) && (run.stages || []).every(stage => stage.status !== "won" || stage.rewardSaved)) return null; return run || null; } static stage(): any { const run = this.run(); return run && run.stages[run.stages.length - 1]; } static now(): number { return this.runtime ? this.runtime.now() : Date.now() + this.offset; } static activityOpen(data: any = this.data(), now: number = this.now()): boolean { const period = data && data.period; return !!(period && period.startsAt <= now && now < period.endsAt); } static registrationOpen(data: any = this.data(), now: number = this.now()): boolean { return this.activityOpen(data, now); } /** GM_INFO.level is the number of completed main levels, not the next level number. */ static lockedUntilLevel(data: any = this.data()): number { if (this.currentRun(data)) return 0; const unlockLevel = Number(data && data.period && data.period.unlockLevel) || 0; return (Number(cc.fx.GameConfig.GM_INFO.level) || 0) < unlockLevel ? unlockLevel : 0; } static challengeActive(run: any = this.run()): boolean { return !!(run && ["playing", "waiting"].includes(run.status) && this.now() < run.expiresAt); } static challengeExpired(run: any = this.run()): boolean { return !!(run && (run.status === "expired" || ["playing", "waiting"].includes(run.status) && this.now() >= run.expiresAt)); } static visible(): boolean { const data = this.data(), run = this.currentRun(data); // A terminal personal run consumes this period's entry, even while signup is still open. // Pending results/reward retries are handled by their presentation queue, not by the home entry. return run ? this.challengeActive(run) : this.activityOpen(data); } static resultKey(): string { return this.runtime ? this.runtime.resultKey() : ""; } static unseenResult(): boolean { return !!this.runtime && this.runtime.unseenResult(); } static allowsAutomaticResults(): boolean { this.ensureUser(); return this.autoDisplay; } static acknowledgeResult(): void { if (this.runtime) this.runtime.acknowledgeResult(); } static elimination(): any { return this.runtime ? this.runtime.elimination() : null; } static acknowledgeElimination(key: string): void { if (this.runtime) this.runtime.acknowledgeElimination(key); } static subscribe(fn: () => void): () => void { this.listeners.push(fn); return () => { this.listeners = this.listeners.filter(item => item !== fn); }; } private static emit(): void { this.listeners.slice().forEach(fn => fn()); } /** Automatic loading requires fresh server eligibility, never just a cached run. */ static shouldPreload(data: any): boolean { if (!data) return false; const now = Math.max(Number(data.serverNow) || 0, this.now()); const run = data.run; return !!(run && (run.stages || []).some(stage => stage.status === "won" && !stage.rewardSaved)) || !!(run && ["playing", "waiting"].includes(run.status) && run.expiresAt > now) || !!(data.available && this.registrationOpen(data, now) && !this.lockedUntilLevel(data)); } private static bundle(): Promise { if (!this.bundleTask) this.bundleTask = new Promise((resolve, reject) => { cc.assetManager.loadBundle("cloud_rise", (error, bundle) => error ? reject(error) : resolve(bundle)); }).catch(error => { this.bundleTask = null; throw error; }); return this.bundleTask; } static pagePrefab(): Promise { if (!this.prefabTask) this.prefabTask = this.bundle().then(bundle => new Promise((resolve, reject) => { bundle.load("prefab/CloudRise", cc.Prefab, (error, prefab: cc.Prefab) => { if (error) reject(error); else { prefab.addRef(); resolve(prefab); } }); })).catch(error => { this.prefabTask = null; throw error; }); return this.prefabTask; } static afterLogin(): void { void this.sync(); } static async sync(): Promise { this.ensureUser(); if (!this.uid) return false; if (this.runtime) { const uid = this.uid; const ok = await this.runtime.sync(); if (ok && String(cc.fx.GameConfig.GM_INFO.userId || "") === uid && this.shouldPreload(this.runtime.data())) { this.autoDisplay = true; void this.pagePrefab().catch(error => cc.warn("百人赛预加载失败,打开时可重试", error)); } return ok; } if (this.checkTask) return this.checkTask; const uid = this.uid; const task = (async () => { try { const res: any = await new Promise(resolve => { try { Utils.POST("cloudRise/cloudRise", { uid, action: "status" }, resolve); } catch (_) { resolve(null); } }); if (String(cc.fx.GameConfig.GM_INFO.userId || "") !== uid) return false; if (!res || res.code !== 1) { if (res && res.msg === "活动未开启") { this.snapshot = null; this.autoDisplay = false; } this.message = res && res.msg || "活动暂时无法连接"; this.emit(); return false; } this.snapshot = res.data; this.offset = res.data.serverNow - Date.now(); this.message = ""; this.emit(); if (!this.shouldPreload(res.data)) return true; this.autoDisplay = true; await this.bundle(); if (String(cc.fx.GameConfig.GM_INFO.userId || "") !== uid) return false; if (!this.runtime) throw new Error("百人赛分包未注册业务模块"); // Prepare referenced textures and prefab without showing a page or delaying login. void this.pagePrefab().catch(error => cc.warn("百人赛预加载失败,打开时可重试", error)); return await this.runtime.sync(); } catch (error) { this.message = "活动加载失败,请重试"; cc.warn(error); this.emit(); return false; } })(); this.checkTask = task; try { return await task; } finally { if (this.checkTask === task) this.checkTask = null; } } static async startStage(): Promise { if (!await this.sync()) return false; return !!this.runtime && this.runtime.startStage(); } static prepareRound(resume: () => void): boolean { this.ensureUser(); if (this.runtime) return this.runtime.prepareRound(resume); if (cc.fx.GameConfig.GM_INFO.otherLevel !== 0) return true; if (this.snapshot && !this.shouldPreload(this.snapshot)) return true; void this.sync().then(() => { if (this.runtime) { if (this.runtime.prepareRound(resume)) resume(); return; } // A cold launch may still have an enrolled journal while the status request is offline. const saved = cc.fx.StorageMessage.getStorage("cloudRise:" + this.uid); const run = this.snapshot && this.snapshot.run || saved && saved.snapshot && saved.snapshot.run; if ((!run || run.status !== "playing") && !(saved && saved.queue && saved.queue.length)) resume(); }); return false; } static finish(won: boolean): void { if (this.runtime) this.runtime.finish(won); } }