This commit is contained in:
guanchao
2026-10-27 19:54:38 +08:00
parent 29dec73da6
commit 7cec10e4a1
47 changed files with 1430 additions and 23 deletions
+9 -2
View File
@@ -1,5 +1,6 @@
import { popupContent } from "./PopupContent";
import PopupAnimator, { ActivityPopupAnimationType as PopupType } from "./ActivityPopupAnimator";
import SharedPopupBackdrop from "./SharedPopupBackdrop";
import ActivityScheduleService from "./module/Config/ActivityScheduleService";
import { canClaimPass, passTier } from './module/Config/BattlePassModel';
import { getNewShopProduct } from './module/Config/ShopCatalog';
@@ -2280,9 +2281,15 @@ export default class JiaZai extends cc.Component {
}
if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) { if (onClosed) onClosed(); return; }
if (!JiaZai.cachedRewardPrefab || !cc.isValid(JiaZai.cachedRewardPrefab, true) || !this.isHomeBundleReady('publicUI')) {
const backdrop = SharedPopupBackdrop.get(this.node), transition = {};
let cancelled = false;
const release = () => { if (cc.isValid(backdrop, true)) backdrop.release(transition); };
backdrop.hold(transition, () => {
cancelled = true; release(); if (onClosed) onClosed();
});
this.loadHomePopupPrefab('publicUI', 'prefab/RewardWindow', 'cachedRewardPrefab', 5).then(
() => this.openRewardWindow(data, month, onClosed),
error => { cc.warn('[首页奖励窗口] 加载失败', error); if (onClosed) onClosed(); });
() => { try { if (!cancelled) this.openRewardWindow(data, month, onClosed); } finally { release(); } },
error => { release(); cc.warn('[首页奖励窗口] 加载失败', error); if (!cancelled && onClosed) onClosed(); });
return;
}
if (!this.RewardNode) {
+45
View File
@@ -0,0 +1,45 @@
import SharedPopupBackdrop, { PopupBackdropSource } from './SharedPopupBackdrop';
const { ccclass } = cc._decorator;
/** Attach only to a full-screen dim mask, never to an activity's artwork.
* Keep the original node and input components so prefab event bindings still work. */
@ccclass
export default class PopupBackdropSourceComponent extends cc.Component {
private manager: SharedPopupBackdrop = null;
private source: PopupBackdropSource = null;
private renderers: Array<{ component: cc.RenderComponent; enabled: boolean }> = [];
private originalOpacity = 255;
onLoad(): void {
this.originalOpacity = this.node.opacity;
this.renderers = this.node.getComponents(cc.RenderComponent).map(component => ({ component, enabled: component.enabled }));
const visual = new cc.Node('ManagedBackdropVisual');
visual.active = false;
this.node.addChild(visual);
visual.addComponent(cc.Graphics);
this.source = { node: this.node, visual, active: false };
}
onEnable(): void {
let canvas = this.node.parent;
while (canvas && canvas.name !== 'Canvas') canvas = canvas.parent;
if (!canvas) return;
this.manager = SharedPopupBackdrop.get(canvas);
this.renderers.forEach(item => item.component.enabled = false);
this.source.active = true;
this.manager.register(this.source);
}
onDisable(): void {
if (this.source) { this.source.active = false; this.source.visual.active = false; }
this.node.opacity = this.originalOpacity;
this.renderers.forEach(item => { if (cc.isValid(item.component, true)) item.component.enabled = item.enabled; });
if (this.manager && cc.isValid(this.manager, true)) this.manager.refresh();
}
onDestroy(): void {
if (this.manager && cc.isValid(this.manager, true)) this.manager.unregister(this.source);
if (this.source && cc.isValid(this.source.visual, true)) this.source.visual.destroy();
this.manager = null; this.source = null;
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"ver": "1.1.0",
"uuid": "613ae721-73db-477d-9efa-765981d1641b",
"importer": "typescript",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
+1
View File
@@ -243,6 +243,7 @@ export default class PyramidView extends cc.Component {
this.settlementQueued = true;
HomePopupQueue.get(this.node).enqueue("pyramidSettlement", 4, done => {
void this.showSettlements(done);
return () => { if (this.finishSettlement) this.finishSettlement(); };
});
}
private async showSettlements(done: () => void): Promise<void> {
+196
View File
@@ -0,0 +1,196 @@
import LoadingCatAnimation from './LoadingCatAnimation';
const { ccclass } = cc._decorator;
export interface PopupBackdropSource {
node: cc.Node;
visual: cc.Node;
active: boolean;
}
// Match HomeScene/Canvas/Loading/New Sprite(Splash), including activity handoffs.
export const POPUP_BACKDROP_OPACITY = 200;
const FADE_SECONDS = 0.15;
/** Scene-owned backdrop lifetime. Existing mask locations preserve nested draw order
* and click handlers; the scene bridge covers the gap between asynchronous popups. */
@ccclass
export default class SharedPopupBackdrop extends cc.Component {
private sources: PopupBackdropSource[] = [];
private holds = new Map<object, { cancel: () => void; waiting: boolean }>();
private bridge: cc.Node = null;
private message: cc.Node = null;
private cancelButton: cc.Node = null;
private gapSeconds = 0;
private fading = false;
private bridgeOpacity = POPUP_BACKDROP_OPACITY;
private wasVisible = false;
private disposed = false;
static get(canvas: cc.Node): SharedPopupBackdrop {
return canvas.getComponent(SharedPopupBackdrop) || canvas.addComponent(SharedPopupBackdrop);
}
register(source: PopupBackdropSource): void {
if (this.sources.indexOf(source) < 0) this.sources.push(source);
this.refresh();
}
unregister(source: PopupBackdropSource): void {
this.sources = this.sources.filter(item => item !== source);
this.refresh();
}
hold(owner: object, cancel: () => void): void {
this.holds.set(owner, { cancel, waiting: true });
this.refresh();
}
setWaiting(owner: object, waiting: boolean): void {
const hold = this.holds.get(owner);
if (hold) hold.waiting = waiting;
}
release(owner: object): void {
this.holds.delete(owner);
this.refresh();
}
/** Compare actual hierarchy order, including sibling zIndex, not registration time. */
private path(node: cc.Node): cc.Node[] {
const path: cc.Node[] = [];
while (node && node !== this.node) { path.unshift(node); node = node.parent; }
return node === this.node ? path : [];
}
private compare(a: cc.Node, b: cc.Node): number {
const left = this.path(a), right = this.path(b);
for (let i = 0; i < Math.min(left.length, right.length); i++) {
if (left[i] !== right[i]) return left[i].zIndex - right[i].zIndex
|| left[i].getSiblingIndex() - right[i].getSiblingIndex();
}
return left.length - right.length;
}
/** Draw in the source's local coordinates, covering the canvas even inside scaled panels. */
static paint(visual: cc.Node, canvas: cc.Node, opacity = POPUP_BACKDROP_OPACITY): void {
const parent = visual.parent;
const width = canvas.width, height = canvas.height;
const corners = [[-canvas.anchorX * width, -canvas.anchorY * height],
[(1 - canvas.anchorX) * width, -canvas.anchorY * height],
[(1 - canvas.anchorX) * width, (1 - canvas.anchorY) * height],
[-canvas.anchorX * width, (1 - canvas.anchorY) * height]];
const graphics = visual.getComponent(cc.Graphics);
// Creator 2.4 WebGL Graphics writes fillColor directly to vertex colors;
// node.opacity does not change that alpha. Keep it 255 for Canvas parity.
visual.opacity = 255;
graphics.clear(); graphics.fillColor = cc.color(0, 0, 0, Math.round(opacity));
corners.forEach(([x, y], index) => {
const point = parent.convertToNodeSpaceAR(canvas.convertToWorldSpaceAR(cc.v2(x, y)));
if (index) graphics.lineTo(point.x, point.y); else graphics.moveTo(point.x, point.y);
});
graphics.close(); graphics.fill();
}
private ensureBridge(): void {
if (this.bridge) return;
this.bridge = new cc.Node('SharedPopupBackdrop');
this.bridge.active = false;
this.node.addChild(this.bridge, cc.macro.MAX_ZINDEX - 3);
this.bridge.addComponent(cc.Graphics);
this.bridge.addComponent(cc.BlockInputEvents);
const label = (name: string, text: string, y: number): cc.Node => {
const node = new cc.Node(name); this.bridge.addChild(node); node.y = y;
const textLabel = node.addComponent(cc.Label);
textLabel.string = text; textLabel.fontSize = 28; textLabel.lineHeight = 36;
node.setContentSize(320, 70);
return node;
};
const template = this.node.getChildByName('Loading');
if (template) {
this.message = new cc.Node('LoadingContent');
this.message.active = false;
this.bridge.addChild(this.message);
// Reuse the authored cat, blocks and label, excluding the full-screen mask.
['New Label', 'block1', 'block2', 'block3', 'block4', 'cat'].forEach(name => {
const child = template.getChildByName(name);
if (child) this.message.addChild(cc.instantiate(child));
});
}
const loadingLabel = this.message && this.message.getChildByName('New Label');
this.cancelButton = label('CancelWaiting', '暂不等待', loadingLabel ? loadingLabel.y - 90 : -330);
this.cancelButton.on(cc.Node.EventType.TOUCH_END, (event: cc.Event) => {
event.stopPropagation();
// Never allow a stale click to cancel a popup that has already appeared.
if (!this.bridge.active || this.gapSeconds < 10 || this.visibleSources().length) return;
const callbacks = Array.from(this.holds.values()).filter(hold => hold.waiting && hold.cancel).map(hold => hold.cancel);
callbacks.forEach(cancel => cancel());
});
}
private setLoadingVisible(visible: boolean): void {
if (!this.message || this.message.active === visible) return;
if (visible) LoadingCatAnimation.play(this.message);
else LoadingCatAnimation.stop(this.message);
}
private visibleSources(): PopupBackdropSource[] {
return this.sources.filter(source => source.active && cc.isValid(source.node, true)
&& cc.isValid(source.visual, true) && source.node.activeInHierarchy && this.path(source.node).length > 0);
}
refresh(dt = 0): void {
if (this.disposed || !cc.isValid(this.node, true)) return;
this.sources = this.sources.filter(source => cc.isValid(source.node, true) && cc.isValid(source.visual, true));
const visible = this.visibleSources().sort((a, b) => this.compare(a.node, b.node));
const top = visible[visible.length - 1];
this.sources.forEach(source => {
source.visual.active = source === top;
if (source === top) {
source.node.opacity = 255;
SharedPopupBackdrop.paint(source.visual, this.node);
}
});
if (top) {
this.setLoadingVisible(false);
if (this.bridge) this.bridge.active = false;
this.gapSeconds = 0; this.fading = false; this.wasVisible = true;
return;
}
const waiting = Array.from(this.holds.values()).some(hold => hold.waiting);
// An unmanaged full-screen view may temporarily own presentation.
if (this.holds.size && !waiting) {
this.setLoadingVisible(false);
if (this.bridge) this.bridge.active = false;
this.gapSeconds = 0;
return;
}
if (!waiting && !this.wasVisible) return;
this.ensureBridge();
this.bridge.active = true;
this.bridge.setContentSize(this.node.getContentSize());
if (waiting) {
this.bridgeOpacity = POPUP_BACKDROP_OPACITY;
this.gapSeconds += dt; this.fading = false; this.wasVisible = true;
} else {
if (!this.fading) { this.bridgeOpacity = POPUP_BACKDROP_OPACITY; this.fading = true; }
this.bridgeOpacity = Math.max(0, this.bridgeOpacity - POPUP_BACKDROP_OPACITY * dt / FADE_SECONDS);
this.gapSeconds = 0;
if (!this.bridgeOpacity) { this.bridge.active = false; this.wasVisible = false; this.fading = false; }
}
SharedPopupBackdrop.paint(this.bridge, this.node, this.bridgeOpacity);
this.setLoadingVisible(waiting && this.gapSeconds >= 0.8);
this.cancelButton.active = waiting && this.gapSeconds >= 10
&& Array.from(this.holds.values()).every(hold => !!hold.cancel);
}
lateUpdate(dt: number): void { this.refresh(dt); }
onDestroy(): void {
this.setLoadingVisible(false);
this.disposed = true; this.holds.clear();
this.sources.forEach(source => { if (cc.isValid(source.visual, true)) source.visual.active = false; });
this.sources = [];
if (this.bridge && cc.isValid(this.bridge, true)) this.bridge.destroy();
this.bridge = null;
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"ver": "1.1.0",
"uuid": "74f85741-5830-4f3a-aee1-07f7a134534a",
"importer": "typescript",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
+14 -6
View File
@@ -3,6 +3,7 @@ import HomePopupQueue from '../../home_popup_queue/HomePopupQueue';
import GoldMinerService, { GoldMinerDailyReminder } from './GoldMinerService';
import Utils from '../module/Pay/Utils';
import ActivityBundleLoader from '../module/Config/ActivityBundleLoader';
import PopupBackdropSource from '../PopupBackdropSource';
const { ccclass } = cc._decorator;
const BUNDLE = 'coin_madness';
@@ -103,6 +104,7 @@ export default class CoinMadnessHost extends cc.Component {
this.finishQueue = done;
this.automaticPopupKey = 'coinMadnessSettlement';
this.load();
return () => this.close();
});
}
}
@@ -251,6 +253,7 @@ export default class CoinMadnessHost extends cc.Component {
this.finishQueue = done;
this.automaticPopupKey = 'goldMinerDailyReminder';
this.load(reminder);
return () => this.close();
});
}
private load(reminder?: GoldMinerDailyReminder) {
@@ -292,8 +295,7 @@ export default class CoinMadnessHost extends cc.Component {
// Recheck after async loading: account, day or activity may have changed in the queue.
if (reminder && !this.dailyReminderCurrent(service, reminder)) { this.close(); return; }
this.setEntryLoading(false);
this.panel = cc.instantiate(prefab); this.node.addChild(this.panel);
this.panel.zIndex = cc.macro.MAX_ZINDEX - 1;
this.panel = this.mountPopup(prefab, 'Dim', cc.macro.MAX_ZINDEX - 1);
this.panel.getComponent('CoinMadnessPanel').initialize(initial.data, {
initialReady: true,
progressScope: Utils.serverUrl() + ':' + String(cc.fx.GameConfig.GM_INFO.uid),
@@ -337,8 +339,7 @@ export default class CoinMadnessHost extends cc.Component {
this.refreshCoinDisplay();
this.clearHelp();
if (this.panel) { this.panel.removeFromParent(); this.panel.destroy(); }
this.panel = cc.instantiate(prefab);
this.node.addChild(this.panel); this.panel.zIndex = cc.macro.MAX_ZINDEX - 1;
this.panel = this.mountPopup(prefab, 'Dim', cc.macro.MAX_ZINDEX - 1);
this.panel.getComponent('CoinMadnessSettlement').initialize(data, {
real: !!this.service,
close: () => {
@@ -358,11 +359,10 @@ export default class CoinMadnessHost extends cc.Component {
this.loading = false;
if (!this.alive || requestId !== this.requestId) { this.release(bundle); return; }
if (error) { this.toast('说明加载失败,请重试'); return; }
this.help = cc.instantiate(prefab); this.node.addChild(this.help);
this.help = this.mountPopup(prefab, 'Mask', cc.macro.MAX_ZINDEX);
this.help.setContentSize(cc.view.getVisibleSize());
const content = this.help.getChildByName('Content');
content.scale = Math.min(this.help.width / content.width, this.help.height / content.height);
this.help.zIndex = cc.macro.MAX_ZINDEX;
// Match the pass description's staggered backOut entrance, preserving authored scales.
this.helpAnimationParts().forEach((part, i) => {
const scaleX = part.scaleX, scaleY = part.scaleY;
@@ -377,6 +377,14 @@ export default class CoinMadnessHost extends cc.Component {
});
});
}
private mountPopup(prefab: cc.Prefab, maskName: string, zIndex: number): cc.Node {
const popup = cc.instantiate(prefab);
this.node.addChild(popup, zIndex);
// Cached/remote prefabs may predate the adapter serialized in local assets.
const mask = popup.getChildByName(maskName);
if (mask && !mask.getComponent(PopupBackdropSource)) mask.addComponent(PopupBackdropSource);
return popup;
}
private clearHelp() {
if (!this.help) return;
this.helpAnimationParts().forEach(part => cc.Tween.stopAllByTarget(part));