213 lines
12 KiB
TypeScript
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 = ""; }
|
|
}
|