Files

213 lines
12 KiB
TypeScript

import HomePopupPolicy, { HOME_POPUP_RULES, PopupFrequency, PopupRecord, PopupSession } from './HomePopupPolicy';
import ActivityScheduleService from '../Script/module/Config/ActivityScheduleService';
import Utils from '../Script/module/Pay/Utils';
import SharedPopupBackdrop from '../Script/SharedPopupBackdrop';
const { ccclass } = cc._decorator;
type PopupType = PopupFrequency | (() => PopupFrequency);
type PopupRun = (done: () => void) => void | (() => void);
/** HomeScene 共用队列。显示中的活动及其奖励窗口关闭后才推进下一项。 */
@ccclass
export default class HomePopupQueue extends cc.Component {
private static loginPending = true;
private static sessions: { [key: string]: PopupSession } = {};
private tasks: Array<{ key: string; priority: number; run: PopupRun; type?: PopupType }> = [];
private running = "";
private policy: HomePopupPolicy;
private collecting = true;
private sources = new Set(['seven', 'monthlyReward', 'monthlyCard', 'starter', 'pass', 'jungle', 'daily', 'pyramid', 'gold', 'cloud']);
private preparedAt = Date.now();
private settledRun = false;
private recordKey = '';
private accountToken = '';
private session: PopupSession;
private backdrop: SharedPopupBackdrop;
private cancelRun: () => void = null;
private static storageKey(): string {
return 'homePopup:v1:' + Utils.apiBaseUrl() + ':' + String(cc.fx.GameConfig.GM_INFO.uid);
}
private static makePolicy(login: boolean): HomePopupPolicy {
const key = this.storageKey(), gm = cc.fx.GameConfig.GM_INFO;
const session = this.sessions[key] || (this.sessions[key] = {
shown: {}, pending: cc.fx.StorageMessage.getStorage(key + ':pending') || {},
});
return new HomePopupPolicy(cc.fx.StorageMessage.getStorage(key), login, ActivityScheduleService.now(),
Number(gm.level), cc.fx.GameConfig.getNewPlayerHomeLevel(), value => cc.fx.StorageMessage.setStorage(key, value),
session, () => cc.fx.StorageMessage.setStorage(key + ':pending', session.pending), () => ActivityScheduleService.now());
}
static beginLogin(): void {
this.loginPending = true;
delete this.sessions[this.storageKey()];
// Initialize migration before the player crosses the tutorial threshold this session.
this.makePolicy(false);
}
static shouldReturnForTutorial(): boolean { return this.makePolicy(false).forced; }
/** May be called in gameplay; the activity adapter enqueues its validated view on home. */
static requestPresentation(key: string): boolean { return this.makePolicy(false).request(key); }
onLoad(): void {
this.backdrop = SharedPopupBackdrop.get(this.node);
this.recordKey = HomePopupQueue.storageKey();
this.accountToken = cc.fx.GameConfig.GM_INFO.token;
this.policy = HomePopupQueue.makePolicy(HomePopupQueue.loginPending);
this.session = HomePopupQueue.sessions[this.recordKey];
if (this.policy.record.monthlyRewardPending) this.policy.request('monthlyReward');
HomePopupQueue.loginPending = false;
}
completePreparation(source: string): void { this.sources.delete(source); }
holdPreparation(source: string): void { if (this.collecting) this.sources.add(source); }
start(): void {
if (!this.node.getChildByName('sevenDayGift')) this.completePreparation('seven');
}
canCollect(key: string, type?: PopupType): boolean {
return !!this.policy && (this.collecting || this.policy.hasPending(key)) && !cc.fx.GameConfig.isGameplayEntryPreparing()
&& this.recordKey === HomePopupQueue.storageKey() && this.accountToken === cc.fx.GameConfig.GM_INFO.token
&& this.session === HomePopupQueue.sessions[this.recordKey]
&& this.policy.eligible(key, typeof type === 'function' ? type() : type);
}
unavailable(key: string): void {
if (this.policy.ended || this.recordKey !== HomePopupQueue.storageKey()
|| this.accountToken !== cc.fx.GameConfig.GM_INFO.token || this.session !== HomePopupQueue.sessions[this.recordKey]) return;
this.policy.unavailable(key);
}
requestPresentation(key: string): boolean {
if (this.policy.ended || this.recordKey !== HomePopupQueue.storageKey()
|| this.accountToken !== cc.fx.GameConfig.GM_INFO.token || this.session !== HomePopupQueue.sessions[this.recordKey]) return false;
return this.policy.request(key);
}
wasShownToday(key: string): boolean { return !!this.policy.record.shown[key]; }
markShown(key: string): void { if (this.isRunning(key)) this.policy.shown(key); }
isRunning(key: string): boolean {
return !this.policy.ended && this.running === key && !cc.fx.GameConfig.isGameplayEntryPreparing()
&& this.recordKey === HomePopupQueue.storageKey() && this.accountToken === cc.fx.GameConfig.GM_INFO.token
&& this.session === HomePopupQueue.sessions[this.recordKey];
}
/** A manual popup may have opened while this automatic task was loading. */
canPresent(key: string): boolean { return this.isRunning(key) && !this.hasVisiblePopup(key); }
get monthlyRewardPending(): boolean { return !!this.policy.record.monthlyRewardPending; }
setMonthlyRewardPending(): void {
this.policy.record.monthlyRewardPending = true;
const current: PopupRecord = cc.fx.StorageMessage.getStorage(this.recordKey) || this.policy.record;
current.monthlyRewardPending = true;
cc.fx.StorageMessage.setStorage(this.recordKey, current);
this.requestPresentation('monthlyReward');
}
cancelBatch(): void {
this.policy.end(); this.collecting = false; this.tasks.length = 0; this.running = '';
const cancel = this.cancelRun; this.cancelRun = null;
try { if (cancel) cancel(); }
finally { if (this.backdrop && cc.isValid(this.backdrop, true)) this.backdrop.release(this); }
}
public has(key: string): boolean {
return this.running === key || this.tasks.some(task => task.key === key);
}
public static get(canvas: cc.Node): HomePopupQueue {
return canvas.getComponent(HomePopupQueue) || canvas.addComponent(HomePopupQueue);
}
public enqueue(key: string, priority: number, run: PopupRun, type?: PopupType): boolean {
const managed = !!HOME_POPUP_RULES[key];
if (managed && !this.canCollect(key, type)) return false;
if (this.policy.ended || this.running === key || this.tasks.some(task => task.key === key)) return false;
// Keep accepted daily/login candidates pending until actual visibility, including load failures.
this.policy.request(key, typeof type === 'function' ? type() : type);
priority = key === 'ordersAfterPassSettlement' ? 33.25
: key.indexOf('rewardAfterPassSettlement:') === 0 ? 33.5 : this.policy.order(key, priority);
this.tasks.push({ key, priority, run, type });
this.tasks.sort((a, b) => a.priority - b.priority);
return true;
}
update() {
if (!this.policy || this.policy.ended) return;
if (cc.fx.GameConfig.isGameplayEntryPreparing() || this.recordKey !== HomePopupQueue.storageKey()
|| this.accountToken !== cc.fx.GameConfig.GM_INFO.token
|| this.session !== HomePopupQueue.sessions[this.recordKey]) { this.cancelBatch(); return; }
// Collect one initial snapshot. Foreground refreshes/expiry polling cannot start another batch.
if (this.collecting) {
if (this.sources.size && Date.now() - this.preparedAt < 20000) return;
if (this.sources.size) cc.warn('[HomePopupQueue] Initial checks timed out', Array.from(this.sources));
this.collecting = false;
}
if (this.running) {
this.markVisibleRunning();
this.backdrop.setWaiting(this, !this.hasUnmanagedPresentation());
if (!this.settledRun || this.hasVisiblePopup()) return;
this.running = '';
this.cancelRun = null;
}
if (this.hasVisiblePopup()) { this.backdrop.setWaiting(this, !this.hasUnmanagedPresentation()); return; }
// Select the next eligible popup before releasing the batch's backdrop.
let task: typeof this.tasks[0];
while (this.tasks.length) {
const candidate = this.tasks.shift();
if (this.policy.begin(candidate.key, typeof candidate.type === 'function' ? candidate.type() : candidate.type)) {
task = candidate; break;
}
}
if (!task) { this.backdrop.release(this); return; }
this.running = task.key;
this.settledRun = false;
// Inventory recovery already owns an input lock until its save completes.
const cancellable = ['pyramidDelivery', 'ordersAfterPassSettlement', 'starterPackActivation'].indexOf(task.key) < 0;
this.backdrop.hold(this, cancellable ? () => this.cancelBatch() : null);
let finished = false;
const done = () => {
if (finished || !this.isRunning(task.key)) return;
finished = true;
this.markVisibleRunning();
this.settledRun = true;
};
try {
const cancel = task.run(done);
if (typeof cancel === 'function' && this.isRunning(task.key)) this.cancelRun = cancel;
}
catch (error) { done(); cc.error("[HomePopupQueue] " + task.key, error); }
}
private markVisibleRunning(): void {
const home: any = this.node.getComponent('JiaZai');
// These flows own their overlay for the whole task. Other adapters explicitly
// acknowledge their own activation so a concurrent manual open is never counted.
const named = { battlePassSettlement: 'PassSettlement', cloudRiseResult: 'CloudRisePanel' };
const root = named[this.running] && this.node.getChildByName(named[this.running]);
if (root && cc.isValid(root, true) && root.activeInHierarchy) this.markShown(this.running);
if (this.running.indexOf('rewardAfterPassSettlement:') === 0 && home?.RewardNode?.activeInHierarchy) this.markShown(this.running);
}
private hasUnmanagedPresentation(): boolean {
const visible = (node: cc.Node) => node && cc.isValid(node, true) && node.activeInHierarchy;
const home: any = this.node.getComponent('JiaZai');
// Full-screen transitions and gacha artwork have no modal dim mask. Loading
// flags (e.g. CoinMadnessHost.isPopupActive) must NOT hide the bridge.
return !!(visible(this.node.getChildByName('zhuanchang')) || home && visible(home.gachaNode));
}
private hasVisiblePopup(presenting?: string): boolean {
const visible = (node: cc.Node) => node && cc.isValid(node, true) && node.activeInHierarchy;
const coinMadness: any = this.node.getComponent("CoinMadnessHost");
if (presenting !== 'goldMinerDailyReminder' && presenting !== 'coinMadnessSettlement'
&& coinMadness && typeof coinMadness.isPopupActive === 'function' && coinMadness.isPopupActive()) return true;
// JiaZai hides this node when the home-entry transition animation completes.
if (visible(this.node.getChildByName("zhuanchang"))) return true;
if (visible(this.node.getChildByName("Transfer"))) return true;
// Rank is the modal; JiaZai.RankNode is permanent content under Career.
if (visible(this.node.getChildByName("Rank"))) return true;
if (visible(this.node.getChildByName("CloudRisePanel"))) return true;
if (visible(this.node.getChildByName('PassSettlement'))) return true;
if (visible(this.node.getChildByName('PyramidSettlement'))) return true;
if (visible(this.node.getChildByName("PyramidPanel")) || visible(this.node.getChildByName("PyramidDelivery"))) return true;
const seven = this.node.getChildByName("sevenDayGift");
if (seven && seven.children.some(visible)) return true;
const home: any = this.node.getComponent("JiaZai");
if (!home) return false;
if (home.passContentLoading) return true;
return ["RewardNode", "getcard", "monthlyCardNode", "actionpNode", "winStreakNode", "passCheckNode", "passIntroNode", "jungleTreasureNode", "jungleOverNode", "dailyQuestsNode", "ShareNode", "shopNode", "AvatarNode", "gachaNode"]
.some(key => visible(home[key]));
}
onDestroy() { this.cancelBatch(); this.running = ""; }
}