From 7cec10e4a1284bac3d5ebe9edc2b1e30fbbebbee Mon Sep 17 00:00:00 2001 From: guanchao <18612113337@163.com> Date: Tue, 27 Oct 2026 19:54:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/Scene/HomeScene.fire | 104 +++++ assets/Script/JiaZai.ts | 11 +- assets/Script/PopupBackdropSource.ts | 45 ++ assets/Script/PopupBackdropSource.ts.meta | 10 + assets/Script/PyramidView.ts | 1 + assets/Script/SharedPopupBackdrop.ts | 196 +++++++++ assets/Script/SharedPopupBackdrop.ts.meta | 10 + assets/Script/coin_madness/CoinMadnessHost.ts | 20 +- assets/action_bundle/prefab/Avatar.prefab | 26 ++ assets/action_bundle/prefab/Share.prefab | 13 + .../action_bundle/prefab/dailyQuests.prefab | 13 + assets/action_bundle/prefab/newbieGift.prefab | 26 ++ assets/action_bundle/prefab/winStreak.prefab | 13 + assets/action_bundle/prefab/winStreak2.prefab | 13 + .../cloud_rise/stage1/prefab/CloudRise.prefab | 13 + .../cloud_rise/stage2/prefab/CloudRise.prefab | 13 + .../cloud_rise/stage3/prefab/CloudRise.prefab | 13 + .../cloud_rise_home/prefab/CloudRise.prefab | 26 ++ assets/coin_madness/prefab/CoinMadness.prefab | 13 + .../prefab/CoinMadnessHelp.prefab | 13 + .../prefab/CoinMadnessSettlement.prefab | 13 + assets/home_popup_queue/HomePopupQueue.ts | 49 ++- assets/jungle_treasure/prefab/jungle.prefab | 13 + .../jungle_treasure/prefab/jungleOver.prefab | 13 + .../prefab/BenefitsReward.prefab | 13 + .../prefab/BenefitsWindow.prefab | 13 + assets/passCheck/prefab/PassSettlement.prefab | 26 ++ assets/passCheck/prefab/PassUnlock.prefab | 13 + .../passCheck/prefab/passCheckWindow.prefab | 13 + assets/publicUI/prefab/RewardWindow.prefab | 13 + .../pyramid/prefab/PyramidSettlement.prefab | 13 + assets/pyramid/scripts/PyramidPanel.ts | 3 + .../seven_day_gift/prefab/sevenDayGift.prefab | 13 + assets/shop/prefab/buyActivate.prefab | 13 + assets/shop/prefab/monthlyCard.prefab | 26 ++ assets/shop/prefab/onMCReward.prefab | 13 + assets/shop/prefab/passCheck.prefab | 26 ++ assets/shop/prefab/shop2.prefab | 26 ++ tools/check-shared-popup-backdrop-render.cjs | 125 ++++++ tools/coin-madness/check-daily-reminder.cjs | 2 +- tools/coin-madness/check-entry-loading.cjs | 2 +- tools/coin-madness/check-popup-queue.cjs | 4 +- tools/coin-madness/check-settlement-entry.cjs | 4 +- tools/coin-madness/preview.cjs | 9 +- tools/test-activity-resource-integration.cjs | 2 +- tools/test-home-popup-policy.cjs | 1 + tools/test-shared-popup-backdrop.cjs | 400 ++++++++++++++++++ 47 files changed, 1430 insertions(+), 23 deletions(-) create mode 100644 assets/Script/PopupBackdropSource.ts create mode 100644 assets/Script/PopupBackdropSource.ts.meta create mode 100644 assets/Script/SharedPopupBackdrop.ts create mode 100644 assets/Script/SharedPopupBackdrop.ts.meta create mode 100644 tools/check-shared-popup-backdrop-render.cjs create mode 100644 tools/test-shared-popup-backdrop.cjs diff --git a/assets/Scene/HomeScene.fire b/assets/Scene/HomeScene.fire index c43f5ba6..33705f2e 100644 --- a/assets/Scene/HomeScene.fire +++ b/assets/Scene/HomeScene.fire @@ -19400,6 +19400,9 @@ }, { "__id__": 453 + }, + { + "__id__": 752 } ], "_prefab": null, @@ -20284,6 +20287,9 @@ }, { "__id__": 474 + }, + { + "__id__": 753 } ], "_prefab": null, @@ -22386,6 +22392,9 @@ }, { "__id__": 519 + }, + { + "__id__": 754 } ], "_prefab": null, @@ -23227,6 +23236,9 @@ }, { "__id__": 538 + }, + { + "__id__": 755 } ], "_prefab": null, @@ -24111,6 +24123,9 @@ }, { "__id__": 557 + }, + { + "__id__": 756 } ], "_prefab": null, @@ -26899,6 +26914,9 @@ }, { "__id__": 618 + }, + { + "__id__": 757 } ], "_prefab": { @@ -28763,6 +28781,9 @@ }, { "__id__": 666 + }, + { + "__id__": 759 } ], "_prefab": null, @@ -30020,6 +30041,9 @@ }, { "__id__": 694 + }, + { + "__id__": 758 } ], "_prefab": { @@ -32130,5 +32154,85 @@ "__id__": 749 }, "_id": "67zCbKKV9PvYlB9x+eob0L" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 451 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 471 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 517 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 535 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 555 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 614 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 691 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 664 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/Script/JiaZai.ts b/assets/Script/JiaZai.ts index 554fc047..48ba3967 100644 --- a/assets/Script/JiaZai.ts +++ b/assets/Script/JiaZai.ts @@ -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) { diff --git a/assets/Script/PopupBackdropSource.ts b/assets/Script/PopupBackdropSource.ts new file mode 100644 index 00000000..7d3eebde --- /dev/null +++ b/assets/Script/PopupBackdropSource.ts @@ -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; + } +} diff --git a/assets/Script/PopupBackdropSource.ts.meta b/assets/Script/PopupBackdropSource.ts.meta new file mode 100644 index 00000000..b1e984f9 --- /dev/null +++ b/assets/Script/PopupBackdropSource.ts.meta @@ -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": {} +} \ No newline at end of file diff --git a/assets/Script/PyramidView.ts b/assets/Script/PyramidView.ts index 4a4e9017..5b837701 100644 --- a/assets/Script/PyramidView.ts +++ b/assets/Script/PyramidView.ts @@ -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 { diff --git a/assets/Script/SharedPopupBackdrop.ts b/assets/Script/SharedPopupBackdrop.ts new file mode 100644 index 00000000..d53d1df4 --- /dev/null +++ b/assets/Script/SharedPopupBackdrop.ts @@ -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 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; + } +} diff --git a/assets/Script/SharedPopupBackdrop.ts.meta b/assets/Script/SharedPopupBackdrop.ts.meta new file mode 100644 index 00000000..d011693e --- /dev/null +++ b/assets/Script/SharedPopupBackdrop.ts.meta @@ -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": {} +} \ No newline at end of file diff --git a/assets/Script/coin_madness/CoinMadnessHost.ts b/assets/Script/coin_madness/CoinMadnessHost.ts index f0805b88..9c07ce77 100644 --- a/assets/Script/coin_madness/CoinMadnessHost.ts +++ b/assets/Script/coin_madness/CoinMadnessHost.ts @@ -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)); diff --git a/assets/action_bundle/prefab/Avatar.prefab b/assets/action_bundle/prefab/Avatar.prefab index 2fa35a35..89fe6aba 100644 --- a/assets/action_bundle/prefab/Avatar.prefab +++ b/assets/action_bundle/prefab/Avatar.prefab @@ -155,6 +155,9 @@ }, { "__id__": 5 + }, + { + "__id__": 1286 } ], "_prefab": { @@ -36644,6 +36647,9 @@ }, { "__id__": 951 + }, + { + "__id__": 1287 } ], "_prefab": { @@ -49450,5 +49456,25 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 949 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/action_bundle/prefab/Share.prefab b/assets/action_bundle/prefab/Share.prefab index 96fa0b53..5980b50f 100644 --- a/assets/action_bundle/prefab/Share.prefab +++ b/assets/action_bundle/prefab/Share.prefab @@ -140,6 +140,9 @@ }, { "__id__": 5 + }, + { + "__id__": 64 } ], "_prefab": { @@ -2291,5 +2294,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/action_bundle/prefab/dailyQuests.prefab b/assets/action_bundle/prefab/dailyQuests.prefab index eba63f87..795046f0 100644 --- a/assets/action_bundle/prefab/dailyQuests.prefab +++ b/assets/action_bundle/prefab/dailyQuests.prefab @@ -101,6 +101,9 @@ }, { "__id__": 5 + }, + { + "__id__": 327 } ], "_prefab": { @@ -12134,5 +12137,15 @@ }, "fileId": "45f5e1951b6d442aaac89bf1e412ae0d", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] diff --git a/assets/action_bundle/prefab/newbieGift.prefab b/assets/action_bundle/prefab/newbieGift.prefab index fcadb12e..bb30121e 100644 --- a/assets/action_bundle/prefab/newbieGift.prefab +++ b/assets/action_bundle/prefab/newbieGift.prefab @@ -143,6 +143,9 @@ }, { "__id__": 5 + }, + { + "__id__": 233 } ], "_prefab": { @@ -7699,6 +7702,9 @@ }, { "__id__": 209 + }, + { + "__id__": 234 } ], "_prefab": { @@ -8521,5 +8527,25 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 207 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/action_bundle/prefab/winStreak.prefab b/assets/action_bundle/prefab/winStreak.prefab index d049814f..0e3f32af 100644 --- a/assets/action_bundle/prefab/winStreak.prefab +++ b/assets/action_bundle/prefab/winStreak.prefab @@ -122,6 +122,9 @@ }, { "__id__": 5 + }, + { + "__id__": 66 } ], "_prefab": { @@ -2391,5 +2394,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/action_bundle/prefab/winStreak2.prefab b/assets/action_bundle/prefab/winStreak2.prefab index 0ebf8211..a8660cee 100644 --- a/assets/action_bundle/prefab/winStreak2.prefab +++ b/assets/action_bundle/prefab/winStreak2.prefab @@ -137,6 +137,9 @@ }, { "__id__": 5 + }, + { + "__id__": 108 } ], "_prefab": { @@ -3907,5 +3910,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/cloud_rise/stage1/prefab/CloudRise.prefab b/assets/cloud_rise/stage1/prefab/CloudRise.prefab index 197810ad..abc8060d 100644 --- a/assets/cloud_rise/stage1/prefab/CloudRise.prefab +++ b/assets/cloud_rise/stage1/prefab/CloudRise.prefab @@ -10977,6 +10977,9 @@ }, { "__id__": 295 + }, + { + "__id__": 603 } ], "_prefab": { @@ -22562,5 +22565,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 293 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/cloud_rise/stage2/prefab/CloudRise.prefab b/assets/cloud_rise/stage2/prefab/CloudRise.prefab index c0aa0ec7..c35f935c 100644 --- a/assets/cloud_rise/stage2/prefab/CloudRise.prefab +++ b/assets/cloud_rise/stage2/prefab/CloudRise.prefab @@ -11997,6 +11997,9 @@ }, { "__id__": 323 + }, + { + "__id__": 631 } ], "_prefab": { @@ -23582,5 +23585,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 321 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/cloud_rise/stage3/prefab/CloudRise.prefab b/assets/cloud_rise/stage3/prefab/CloudRise.prefab index 0bcebc7b..1e91bcd6 100644 --- a/assets/cloud_rise/stage3/prefab/CloudRise.prefab +++ b/assets/cloud_rise/stage3/prefab/CloudRise.prefab @@ -12661,6 +12661,9 @@ }, { "__id__": 341 + }, + { + "__id__": 649 } ], "_prefab": { @@ -24246,5 +24249,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 339 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/cloud_rise_home/prefab/CloudRise.prefab b/assets/cloud_rise_home/prefab/CloudRise.prefab index 19107ca0..2aa94e04 100644 --- a/assets/cloud_rise_home/prefab/CloudRise.prefab +++ b/assets/cloud_rise_home/prefab/CloudRise.prefab @@ -98,6 +98,9 @@ }, { "__id__": 4 + }, + { + "__id__": 179 } ], "_prefab": { @@ -5185,6 +5188,9 @@ }, { "__id__": 138 + }, + { + "__id__": 180 } ], "_prefab": { @@ -6716,5 +6722,25 @@ }, "fileId": "cloudRiseOverviewChest", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 135 + }, + "_enabled": true, + "_id": "" } ] diff --git a/assets/coin_madness/prefab/CoinMadness.prefab b/assets/coin_madness/prefab/CoinMadness.prefab index 9d4d47a7..8e302b0b 100644 --- a/assets/coin_madness/prefab/CoinMadness.prefab +++ b/assets/coin_madness/prefab/CoinMadness.prefab @@ -95,6 +95,9 @@ "_components": [ { "__id__": 3 + }, + { + "__id__": 1045 } ], "_prefab": { @@ -38443,5 +38446,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/coin_madness/prefab/CoinMadnessHelp.prefab b/assets/coin_madness/prefab/CoinMadnessHelp.prefab index d0f565e8..def3242d 100644 --- a/assets/coin_madness/prefab/CoinMadnessHelp.prefab +++ b/assets/coin_madness/prefab/CoinMadnessHelp.prefab @@ -116,6 +116,9 @@ }, { "__id__": 7 + }, + { + "__id__": 45 } ], "_prefab": { @@ -1591,5 +1594,15 @@ "_isTrimmedMode": false, "_atlas": null, "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 4 + }, + "_enabled": true, + "_id": "" } ] diff --git a/assets/coin_madness/prefab/CoinMadnessSettlement.prefab b/assets/coin_madness/prefab/CoinMadnessSettlement.prefab index 06beb45f..c009a3d9 100644 --- a/assets/coin_madness/prefab/CoinMadnessSettlement.prefab +++ b/assets/coin_madness/prefab/CoinMadnessSettlement.prefab @@ -95,6 +95,9 @@ "_components": [ { "__id__": 3 + }, + { + "__id__": 180 } ], "_prefab": { @@ -6533,5 +6536,15 @@ "gap": -5.04, "colonWidth": 0.36, "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] diff --git a/assets/home_popup_queue/HomePopupQueue.ts b/assets/home_popup_queue/HomePopupQueue.ts index 5ed8d24f..27f35a98 100644 --- a/assets/home_popup_queue/HomePopupQueue.ts +++ b/assets/home_popup_queue/HomePopupQueue.ts @@ -1,15 +1,17 @@ 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: (done: () => void) => void; type?: PopupType }> = []; + private tasks: Array<{ key: string; priority: number; run: PopupRun; type?: PopupType }> = []; private running = ""; private policy: HomePopupPolicy; private collecting = true; @@ -19,6 +21,8 @@ export default class HomePopupQueue extends cc.Component { 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); @@ -42,6 +46,7 @@ export default class HomePopupQueue extends cc.Component { /** 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); @@ -87,7 +92,12 @@ export default class HomePopupQueue extends cc.Component { cc.fx.StorageMessage.setStorage(this.recordKey, current); this.requestPresentation('monthlyReward'); } - cancelBatch(): void { this.policy.end(); this.collecting = false; this.tasks.length = 0; this.running = ''; } + 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); @@ -97,7 +107,7 @@ export default class HomePopupQueue extends cc.Component { return canvas.getComponent(HomePopupQueue) || canvas.addComponent(HomePopupQueue); } - public enqueue(key: string, priority: number, run: (done: () => void) => void, type?: PopupType): boolean { + 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; @@ -123,22 +133,37 @@ export default class HomePopupQueue extends cc.Component { } if (this.running) { this.markVisibleRunning(); + this.backdrop.setWaiting(this, !this.hasUnmanagedPresentation()); if (!this.settledRun || this.hasVisiblePopup()) return; this.running = ''; + this.cancelRun = null; } - if (!this.tasks.length || this.hasVisiblePopup()) return; - const task = this.tasks.shift(); - if (!this.policy.begin(task.key, typeof task.type === 'function' ? task.type() : task.type)) return; + 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) return; + if (finished || !this.isRunning(task.key)) return; finished = true; this.markVisibleRunning(); this.settledRun = true; }; - try { task.run(done); } + 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); } } @@ -152,6 +177,14 @@ export default class HomePopupQueue extends cc.Component { 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"); diff --git a/assets/jungle_treasure/prefab/jungle.prefab b/assets/jungle_treasure/prefab/jungle.prefab index 1cab757b..90ee337f 100644 --- a/assets/jungle_treasure/prefab/jungle.prefab +++ b/assets/jungle_treasure/prefab/jungle.prefab @@ -184,6 +184,9 @@ }, { "__id__": 5 + }, + { + "__id__": 34 } ], "_prefab": { @@ -1222,5 +1225,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "_id": "" } ] diff --git a/assets/jungle_treasure/prefab/jungleOver.prefab b/assets/jungle_treasure/prefab/jungleOver.prefab index ae9f4cd3..4d133c73 100644 --- a/assets/jungle_treasure/prefab/jungleOver.prefab +++ b/assets/jungle_treasure/prefab/jungleOver.prefab @@ -128,6 +128,9 @@ }, { "__id__": 5 + }, + { + "__id__": 75 } ], "_prefab": { @@ -2675,5 +2678,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] diff --git a/assets/mini_program_benefits/prefab/BenefitsReward.prefab b/assets/mini_program_benefits/prefab/BenefitsReward.prefab index 197854a5..e6221e20 100644 --- a/assets/mini_program_benefits/prefab/BenefitsReward.prefab +++ b/assets/mini_program_benefits/prefab/BenefitsReward.prefab @@ -92,6 +92,9 @@ "_components": [ { "__id__": 3 + }, + { + "__id__": 62 } ], "_prefab": { @@ -2178,5 +2181,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/mini_program_benefits/prefab/BenefitsWindow.prefab b/assets/mini_program_benefits/prefab/BenefitsWindow.prefab index 6e8ed061..78b49e6f 100644 --- a/assets/mini_program_benefits/prefab/BenefitsWindow.prefab +++ b/assets/mini_program_benefits/prefab/BenefitsWindow.prefab @@ -106,6 +106,9 @@ }, { "__id__": 6 + }, + { + "__id__": 12 } ], "_prefab": { @@ -378,5 +381,15 @@ "rewardPrefab": { "__uuid__": "ed22cca6-aecf-4707-b134-729ac6799a1e" } + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/passCheck/prefab/PassSettlement.prefab b/assets/passCheck/prefab/PassSettlement.prefab index b0e4fbe8..c10f77b1 100644 --- a/assets/passCheck/prefab/PassSettlement.prefab +++ b/assets/passCheck/prefab/PassSettlement.prefab @@ -91,6 +91,9 @@ }, { "__id__": 4 + }, + { + "__id__": 376 } ], "_prefab": { @@ -13110,6 +13113,9 @@ }, { "__id__": 351 + }, + { + "__id__": 377 } ], "_prefab": { @@ -14031,5 +14037,25 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 349 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/passCheck/prefab/PassUnlock.prefab b/assets/passCheck/prefab/PassUnlock.prefab index ff344104..8090b9fe 100644 --- a/assets/passCheck/prefab/PassUnlock.prefab +++ b/assets/passCheck/prefab/PassUnlock.prefab @@ -102,6 +102,9 @@ }, { "__id__": 6 + }, + { + "__id__": 35 } ], "_prefab": { @@ -1238,5 +1241,15 @@ "_isTrimmedMode": true, "_atlas": null, "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "_id": "" } ] diff --git a/assets/passCheck/prefab/passCheckWindow.prefab b/assets/passCheck/prefab/passCheckWindow.prefab index d5262b30..932bf82d 100644 --- a/assets/passCheck/prefab/passCheckWindow.prefab +++ b/assets/passCheck/prefab/passCheckWindow.prefab @@ -113,6 +113,9 @@ }, { "__id__": 5 + }, + { + "__id__": 29 } ], "_prefab": { @@ -1066,5 +1069,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/publicUI/prefab/RewardWindow.prefab b/assets/publicUI/prefab/RewardWindow.prefab index c789f870..43962e8c 100644 --- a/assets/publicUI/prefab/RewardWindow.prefab +++ b/assets/publicUI/prefab/RewardWindow.prefab @@ -101,6 +101,9 @@ }, { "__id__": 4 + }, + { + "__id__": 13 } ], "_prefab": { @@ -390,5 +393,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/pyramid/prefab/PyramidSettlement.prefab b/assets/pyramid/prefab/PyramidSettlement.prefab index cd2daa95..e9059fcd 100644 --- a/assets/pyramid/prefab/PyramidSettlement.prefab +++ b/assets/pyramid/prefab/PyramidSettlement.prefab @@ -93,6 +93,9 @@ }, { "__id__": 4 + }, + { + "__id__": 108 } ], "_prefab": null, @@ -5467,5 +5470,15 @@ "__uuid__": "138e1ec6-4f74-4512-ac0e-7475bce3546e" }, "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] diff --git a/assets/pyramid/scripts/PyramidPanel.ts b/assets/pyramid/scripts/PyramidPanel.ts index 0e1d5ad2..f078547f 100644 --- a/assets/pyramid/scripts/PyramidPanel.ts +++ b/assets/pyramid/scripts/PyramidPanel.ts @@ -1,4 +1,5 @@ import PopupAnimator, { ActivityPopupAnimationType as PopupType } from "../../Script/ActivityPopupAnimator"; +import PopupBackdropSource from "../../Script/PopupBackdropSource"; import Service from "../../Script/module/Config/PyramidService"; import PyramidDigits from "./PyramidDigits"; import PyramidAnimationDebug from "./PyramidAnimationDebug"; @@ -82,6 +83,7 @@ export default class PyramidPanel extends cc.Component { const graphics = shield.addComponent(cc.Graphics); graphics.fillColor = new cc.Color(0, 0, 0, 165); graphics.rect(-size.width / 2, -size.height / 2, size.width, size.height); graphics.fill(); shield.addComponent(cc.BlockInputEvents); this.node.addComponent(cc.BlockInputEvents); + shield.addComponent(PopupBackdropSource); const bundle = await Service.bundle().catch(error => { if (this.isAlive()) throw error; return null; }); if (!this.isAlive()) return; this.bundle = bundle; @@ -445,6 +447,7 @@ export default class PyramidPanel extends cc.Component { const dim = shade.addComponent(cc.Graphics); dim.fillColor = new cc.Color(0, 0, 0, 190); dim.rect(-shade.width / 2, -shade.height / 2, shade.width, shade.height); dim.fill(); shade.addComponent(cc.BlockInputEvents); shade.on(cc.Node.EventType.TOUCH_END, () => this.closeHelp()); + shade.addComponent(PopupBackdropSource); const box = this.sprite(modal, "ProbabilityBox", "probability/background", 0, 0, 902, 1378); const sprite = box.getComponent(cc.Sprite), frame = sprite.spriteFrame; frame.insetTop = 200; frame.insetBottom = 125; frame.insetLeft = frame.insetRight = 60; diff --git a/assets/seven_day_gift/prefab/sevenDayGift.prefab b/assets/seven_day_gift/prefab/sevenDayGift.prefab index 45ac76ae..9313b9cf 100644 --- a/assets/seven_day_gift/prefab/sevenDayGift.prefab +++ b/assets/seven_day_gift/prefab/sevenDayGift.prefab @@ -101,6 +101,9 @@ }, { "__id__": 5 + }, + { + "__id__": 260 } ], "_prefab": { @@ -9607,5 +9610,15 @@ }, "fileId": "sevenDayNumber6", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] diff --git a/assets/shop/prefab/buyActivate.prefab b/assets/shop/prefab/buyActivate.prefab index 4d84480a..ce0fde31 100644 --- a/assets/shop/prefab/buyActivate.prefab +++ b/assets/shop/prefab/buyActivate.prefab @@ -98,6 +98,9 @@ }, { "__id__": 4 + }, + { + "__id__": 83 } ], "_prefab": { @@ -3069,5 +3072,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] diff --git a/assets/shop/prefab/monthlyCard.prefab b/assets/shop/prefab/monthlyCard.prefab index d48bf849..5fd37e87 100644 --- a/assets/shop/prefab/monthlyCard.prefab +++ b/assets/shop/prefab/monthlyCard.prefab @@ -128,6 +128,9 @@ }, { "__id__": 5 + }, + { + "__id__": 122 } ], "_prefab": { @@ -3487,6 +3490,9 @@ }, { "__id__": 95 + }, + { + "__id__": 123 } ], "_prefab": { @@ -4423,5 +4429,25 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 93 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/shop/prefab/onMCReward.prefab b/assets/shop/prefab/onMCReward.prefab index e031a7d3..378f0269 100644 --- a/assets/shop/prefab/onMCReward.prefab +++ b/assets/shop/prefab/onMCReward.prefab @@ -106,6 +106,9 @@ }, { "__id__": 5 + }, + { + "__id__": 30 } ], "_prefab": { @@ -1038,5 +1041,15 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/shop/prefab/passCheck.prefab b/assets/shop/prefab/passCheck.prefab index 02aac4ce..c005bf75 100644 --- a/assets/shop/prefab/passCheck.prefab +++ b/assets/shop/prefab/passCheck.prefab @@ -122,6 +122,9 @@ }, { "__id__": 5 + }, + { + "__id__": 277 } ], "_prefab": { @@ -9178,6 +9181,9 @@ }, { "__id__": 250 + }, + { + "__id__": 278 } ], "_prefab": { @@ -10136,5 +10142,25 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 248 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/assets/shop/prefab/shop2.prefab b/assets/shop/prefab/shop2.prefab index 395cd75d..4f2dbd25 100644 --- a/assets/shop/prefab/shop2.prefab +++ b/assets/shop/prefab/shop2.prefab @@ -134,6 +134,9 @@ }, { "__id__": 5 + }, + { + "__id__": 1142 } ], "_prefab": { @@ -41575,6 +41578,9 @@ }, { "__id__": 1115 + }, + { + "__id__": 1143 } ], "_prefab": { @@ -42523,5 +42529,25 @@ }, "fileId": "", "sync": false + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_id": "" + }, + { + "__type__": "613aechc9tHfZ76dlmB0WQb", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 1113 + }, + "_enabled": true, + "_id": "" } ] \ No newline at end of file diff --git a/tools/check-shared-popup-backdrop-render.cjs b/tools/check-shared-popup-backdrop-render.cjs new file mode 100644 index 00000000..2b55106b --- /dev/null +++ b/tools/check-shared-popup-backdrop-render.cjs @@ -0,0 +1,125 @@ +// Standalone Creator pixel regression. All browser requests are served from local files; +// no preview server, game login, or activity API is used. +// Optional: COCOS_CREATOR_PATH, PLAYWRIGHT_MODULE, PNGJS_MODULE, BROWSER_CHANNEL. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const ts = require('typescript'); +const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright'); +const { PNG } = require(process.env.PNGJS_MODULE || 'pngjs'); +const homeScene = JSON.parse(fs.readFileSync('assets/Scene/HomeScene.fire')); +const authoredLoading = homeScene.find(n => n.__type__ === 'cc.Node' && n._name === 'Loading' && homeScene[n._parent?.__id__]?._name === 'Canvas'); +const artwork = node => ({ name: node._name, x: node._trs.array[0], y: node._trs.array[1], opacity: node._opacity, + width: node._contentSize.width, height: node._contentSize.height, + components: node._components.map(ref => homeScene[ref.__id__]).filter(c => ['cc.Sprite','cc.Label'].includes(c.__type__)), + children: node._children.map(ref => artwork(homeScene[ref.__id__])) }); +const loadingArtwork = authoredLoading._children.map(ref => homeScene[ref.__id__]).filter(n => n._name !== 'New Sprite(Splash)').map(artwork); + +(async () => { + const browser = await chromium.launch({ headless: true, channel: process.env.BROWSER_CHANNEL || 'msedge' }); + try { + const page = await browser.newPage({ viewport: { width: 360, height: 640 } }); + const errors = []; + page.on('pageerror', error => { errors.push(error.message); if (errors.length < 6) console.error('PAGE ERROR', error.message); }); + page.on('console', message => { if (message.type() === 'error') console.error('BROWSER', message.text()); }); + const creator = path.join(process.env.COCOS_CREATOR_PATH || 'C:/ProgramData/cocos/editors/Creator/2.4.15', 'resources'); + const resident = (name, dependency) => { + const file = 'assets/Script/' + name + '.ts'; + const uuid = JSON.parse(fs.readFileSync(file + '.meta')).uuid; + const hex = uuid.replaceAll('-', ''), alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + const id = hex.slice(0,5) + hex.slice(5).match(/.{3}/g).map(s => alphabet[parseInt(s,16)>>6] + alphabet[parseInt(s,16)&63]).join(''); + const code = ts.transpileModule(fs.readFileSync(file,'utf8'), {compilerOptions:{target:1,module:1,experimentalDecorators:true}}).outputText; + return '(function(){var exports={};function require(){return '+dependency+';}cc._RF.push({},'+JSON.stringify(id)+','+JSON.stringify(name)+');'+code+';cc._RF.pop();return exports;})()'; + }; + await page.route('http://127.0.0.1:8765/**', route => { + const url = new URL(route.request().url()).pathname.replace(/^\/assets\//, '/'); + if (url === '/host.js') return route.fulfill({contentType:'text/javascript', body:'var animationExports='+resident('LoadingCatAnimation','null')+';var backdropExports='+resident('SharedPopupBackdrop','animationExports')+';window.fixtureBackdropSource='+resident('PopupBackdropSource','backdropExports')+';'}); + const file = url === '/engine.js' ? path.join(creator,'engine/bin/cocos2d-js-for-preview.js') + : url.startsWith('/internal/') ? path.join(creator,'cocos2d-x/simulator/win32/assets',url) + : path.join('library',url.replace(/^\/native\//, '/imports/')); + if (!fs.existsSync(file)) { console.error('MISSING',url); return route.fulfill({status:404,body:'missing'}); } + return route.fulfill({path:file}); + }); + await page.route('http://127.0.0.1:8765/backdrop-check', route => route.fulfill({ + contentType: 'text/html; charset=utf-8', body: ` + ` + })); + await page.goto('http://127.0.0.1:8765/backdrop-check'); + await page.waitForFunction(() => window.ready, null, { timeout: 15000 }); + const results = []; + async function verify(stage, expected = 55) { + await page.evaluate(() => new Promise(resolve => cc.director.once(cc.Director.EVENT_AFTER_DRAW, resolve))); + const png = PNG.sync.read(await page.screenshot()); + const offset = (30 * png.width + 30) * 4; + const rgb = Array.from(png.data.slice(offset, offset + 3)); + results.push({ stage, rgb }); + assert.ok(rgb.every(c => Math.abs(c - expected) <= 1), stage + ': ' + rgb); + assert.equal(await page.evaluate(() => Object.values(check).filter(x => x && x.sprite).some(x => x.root.activeInHierarchy && x.sprite.enabled)), false); + } + await verify('loading'); + await page.evaluate(() => { check.loading.root.active = false; }); + await verify('bridge'); + await page.waitForTimeout(900); + await verify('bridge with cat loading'); + const loadingState = await page.evaluate(() => { + const content=check.manager.message; + return { active:content.active, children:content.children.map(n=>n.name), text:content.getChildByName('New Label').getComponent(cc.Label).string, + y:content.getChildByName('cat').y, runId:content.__loadingCatAnimationState.runId }; + }); + assert.equal(loadingState.active,true); + assert.equal(loadingState.text,'加载中,请稍等喵~'); + assert.equal(loadingState.children.includes('Mask'),false); + assert.equal(loadingState.children.includes('New Sprite(Splash)'),false); + await page.waitForTimeout(230); + const after = await page.evaluate(() => ({y:check.manager.message.getChildByName('cat').y,runId:check.manager.message.__loadingCatAnimationState.runId})); + assert.notEqual(after.y,loadingState.y,'cat actually animates in Creator'); + assert.equal(after.runId,loadingState.runId,'animation must not restart every frame'); + if (process.env.LOADING_SCREENSHOT) await page.screenshot({path:process.env.LOADING_SCREENSHOT}); + await page.evaluate(() => { check.main = check.add('CoinMadness',32766,175); }); + await verify('activity'); + assert.equal(await page.evaluate(()=>check.manager.message.active),false); + await page.evaluate(() => { check.loading.root.active = true; }); + await verify('loading above activity'); + await page.evaluate(() => { check.loading.root.active = false; check.help = check.add('Help',32767,190); }); + await verify('help above activity'); + await page.evaluate(() => { check.help.root.active = false; }); + await verify('activity restored'); + await page.evaluate(() => { check.main.root.active = false; check.manager.release(check.owner); }); + await page.waitForTimeout(250); + await verify('closed',255); + assert.deepEqual(errors,[]); + console.log(JSON.stringify({passed:true,renderer:'Cocos Creator 2.4.15 WebGL',results})); + } finally { await browser.close(); } +})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/tools/coin-madness/check-daily-reminder.cjs b/tools/coin-madness/check-daily-reminder.cjs index 338650cf..da4ddfed 100644 --- a/tools/coin-madness/check-daily-reminder.cjs +++ b/tools/coin-madness/check-daily-reminder.cjs @@ -58,7 +58,7 @@ function hostFixture() { const cc = { ...f.cc, Component: class {}, _decorator: { ccclass: c => c }, isValid: () => true, Prefab: class {}, macro: { MAX_ZINDEX: 100 }, Director: { EVENT_AFTER_DRAW: 1 }, director: { once(_e, cb) { cb(); } }, assetManager: { loadBundle(_name, cb) { cb(null, bundle); }, getBundle: () => bundle, removeBundle() {} }, - instantiate: () => ({ destroy() {}, getComponent: () => ({ initialize() { initialized++; } }) }) }; + instantiate: () => ({ destroy() {}, getChildByName: () => null, getComponent: () => ({ initialize() { initialized++; } }) }) }; const exports = {}; new Function('require', 'exports', 'cc', hostCode)(name => { if (name.includes('ActivityBundleLoader')) return require('../activity-loader-test-support.cjs')(cc); diff --git a/tools/coin-madness/check-entry-loading.cjs b/tools/coin-madness/check-entry-loading.cjs index c71988da..01a1d97a 100644 --- a/tools/coin-madness/check-entry-loading.cjs +++ b/tools/coin-madness/check-entry-loading.cjs @@ -6,7 +6,7 @@ const flush=()=>new Promise(r=>setImmediate(r)); const first=new Promise((a,b)=>{resolve=a;reject=b}); const service={readForDisplay:()=>first,refresh:()=>new Promise(()=>{}),stopPolling(){},settlementNotice:0}; const bundle={load:(path,type,cb)=>asset=cb,releaseAll(){}}; - const panel={getComponent:()=>({initialize(data,api){assert(api.initialReady);events.push('initialize')}}),destroy(){}}; + const panel={getChildByName:()=>null,getComponent:()=>({initialize(data,api){assert(api.initialReady);events.push('initialize')}}),destroy(){}}; const cc={fx:{GameConfig:{GM_INFO:{uid:"test"}}},Component:class{},_decorator:{ccclass:x=>x},assetManager:{loadBundle:(name,cb)=>cb(null,bundle),getBundle:()=>bundle,removeBundle(){}},Prefab:class{},macro:{MAX_ZINDEX:100},instantiate:()=>{events.push('instantiate');return panel},director:{once:(e,cb)=>cb()},Director:{EVENT_AFTER_DRAW:1}}; const out={};new Function('require','exports','cc',code)(name=>name.includes('ActivityBundleLoader')?require('../activity-loader-test-support.cjs')(cc):({default:{serverUrl:()=> 'test'}}),out,cc); const host=new out.default();host.service=service;host.toast=()=>{};host.refreshCoinDisplay=()=>{};host.clearHelp=()=>{}; diff --git a/tools/coin-madness/check-popup-queue.cjs b/tools/coin-madness/check-popup-queue.cjs index fa23a16a..49f7e84f 100644 --- a/tools/coin-madness/check-popup-queue.cjs +++ b/tools/coin-madness/check-popup-queue.cjs @@ -6,7 +6,9 @@ const cc = { Component: class {}, _decorator: { ccclass: c => c }, isValid: () = cc.fx = { GameConfig: { GM_INFO: { uid: 'one', level: 80 }, getNewPlayerHomeLevel: () => 15, isGameplayEntryPreparing: () => false }, StorageMessage: { getStorage: () => null, setStorage() {} } }; const policy = {}; new Function('exports', ts.transpileModule(fs.readFileSync('assets/home_popup_queue/HomePopupPolicy.ts','utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2017,module:ts.ModuleKind.CommonJS}}).outputText)(policy); -const dependency = name => name.includes('HomePopupPolicy') ? policy : { default: name.includes('ActivityScheduleService') ? {now:()=>Date.now()} : {apiBaseUrl:()=> 'test'} }; +const dependency = name => name.includes('HomePopupPolicy') ? policy : { default: name.includes('SharedPopupBackdrop') + ? { get: () => ({ hold() {}, release() {}, setWaiting() {} }) } + : name.includes('ActivityScheduleService') ? {now:()=>Date.now()} : {apiBaseUrl:()=> 'test'} }; const out = {}; new Function('exports', 'cc', 'require', code)(out, cc, dependency); for (const host of [null, {}, { isPopupActive: () => false }, { isPopupActive: () => true }]) { diff --git a/tools/coin-madness/check-settlement-entry.cjs b/tools/coin-madness/check-settlement-entry.cjs index 52a032dc..551533bc 100644 --- a/tools/coin-madness/check-settlement-entry.cjs +++ b/tools/coin-madness/check-settlement-entry.cjs @@ -7,8 +7,8 @@ const bundle={load(path,type,cb){loaded.push(path);cb(null,{});}}; const cc={Component:class{},_decorator:{ccclass:C=>C},Prefab:class{},macro:{MAX_ZINDEX:999}, fx:{GameConfig:{GM_INFO:{canIos:true}}},find:()=>entry, assetManager:{loadBundle(name,cb){cb(null,bundle);}}, - instantiate:()=>({getComponent:()=>({initialize(data,api){assert.equal(data.state.settlementCoins,500);options=api;}})})}; -const exportsHost={};new Function('require','exports','cc',code)(name=>name.includes('ActivityBundleLoader')?require('../activity-loader-test-support.cjs')(cc):({}),exportsHost,cc); + instantiate:()=>({getChildByName:()=>null,getComponent:()=>({initialize(data,api){assert.equal(data.state.settlementCoins,500);options=api;}})})}; +const exportsHost={};new Function('require','exports','cc',code)(name=>name.includes('HomePopupQueue')?({default:{get:()=>({markShown(){}})}}):name.includes('ActivityBundleLoader')?require('../activity-loader-test-support.cjs')(cc):({}),exportsHost,cc); const host=new exportsHost.default();host.node={addChild(){},getComponent(){return null}}; host.service=service;host.entry=entry;host.refreshCoinDisplay=host.clearHelp=host.layoutEntry=host.refreshEntryBadge=()=>{}; host.close=()=>{host.panel=null;}; diff --git a/tools/coin-madness/preview.cjs b/tools/coin-madness/preview.cjs index 922ee9bf..39470e77 100644 --- a/tools/coin-madness/preview.cjs +++ b/tools/coin-madness/preview.cjs @@ -56,7 +56,14 @@ const server = http.createServer((req,res)=>{ const source=fs.readFileSync(path.join(root,'assets/Script/coin_madness/CoinMadnessHost.ts'),'utf8'); const output=ts.transpileModule(source,{compilerOptions:{target:ts.ScriptTarget.ES5,module:ts.ModuleKind.CommonJS,experimentalDecorators:true}}).outputText; const loader=ts.transpileModule(fs.readFileSync(path.join(root,'assets/Script/module/Config/ActivityBundleLoader.ts'),'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES5,module:ts.ModuleKind.CommonJS}}).outputText; - res.setHeader('Content-Type','text/javascript');return res.end('(function(){var loaderExports={};(function(exports){'+loader+'})(loaderExports);var exports={};function require(name){if(name.includes("ActivityBundleLoader"))return loaderExports;if(name.includes("GoldMinerService"))return {default:{testEnabled:()=>!!window.testService?.testAvailable(),current:()=>window.testService||null}};if(name.includes("Utils"))return {default:{serverUrl:()=>"fixture://"}};if(name.includes("MiniGameSdk"))return {MiniGameSdk:{API:{showToast:console.log}}};return {default:{get:()=>({enqueue:(k,p,run)=>run(()=>{})})}};}cc._RF.push({},"CoinMadnessHost","CoinMadnessHost");'+output+';cc._RF.pop();})();'); + const resident = (name, dependency) => { + const file='assets/Script/'+name+'.ts', uuid=JSON.parse(fs.readFileSync(path.join(root,file+'.meta'))).uuid; + const hex=uuid.replaceAll('-',''), id=hex.slice(0,5)+hex.slice(5).match(/.{3}/g).map(s=>alphabet[parseInt(s,16)>>6]+alphabet[parseInt(s,16)&63]).join(''); + const code=ts.transpileModule(fs.readFileSync(path.join(root,file),'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES5,module:ts.ModuleKind.CommonJS,experimentalDecorators:true}}).outputText; + return '(function(){var exports={};function require(){return '+dependency+';}cc._RF.push({},'+JSON.stringify(id)+','+JSON.stringify(name)+');'+code+';cc._RF.pop();return exports;})();'; + }; + const backdropBootstrap='(function(){var animationExports='+resident('LoadingCatAnimation','null')+'var backdropExports='+resident('SharedPopupBackdrop','animationExports')+'var sourceExports='+resident('PopupBackdropSource','backdropExports')+'window.fixtureBackdropSource=sourceExports;})();'; + res.setHeader('Content-Type','text/javascript');return res.end(backdropBootstrap+'(function(){var loaderExports={};(function(exports){'+loader+'})(loaderExports);var exports={};function require(name){if(name.includes("PopupBackdropSource"))return window.fixtureBackdropSource;if(name.includes("ActivityBundleLoader"))return loaderExports;if(name.includes("GoldMinerService"))return {default:{testEnabled:()=>!!window.testService?.testAvailable(),current:()=>window.testService||null}};if(name.includes("Utils"))return {default:{serverUrl:()=>"fixture://"}};if(name.includes("MiniGameSdk"))return {MiniGameSdk:{API:{showToast:console.log}}};return {default:{get:()=>({enqueue:(k,p,run)=>run(()=>{})})}};}cc._RF.push({},"CoinMadnessHost","CoinMadnessHost");'+output+';cc._RF.pop();})();'); } if(/^\/native\/[a-f0-9]{2}\/[a-f0-9-]+\.png$/.test(url)){ const target=path.join(root,'library/imports',url.slice('/native/'.length)); diff --git a/tools/test-activity-resource-integration.cjs b/tools/test-activity-resource-integration.cjs index 683faeef..74ce492d 100644 --- a/tools/test-activity-resource-integration.cjs +++ b/tools/test-activity-resource-integration.cjs @@ -16,7 +16,7 @@ function miner() { const cc = { Component: class {}, _decorator: { ccclass: c => c }, Prefab: class {}, warn() {}, fx: { GameConfig: { GM_INFO: { uid: 'player' } } }, macro: { MAX_ZINDEX: 100 }, isValid: n => !!n, director: { once: (_event, done) => draws.push(done) }, Director: { EVENT_AFTER_DRAW: 1 }, - instantiate: () => ({ zIndex: 0, destroy() {}, removeFromParent() {}, getComponent: () => ({ initialize() { calls.initialize++; } }) }) }; + instantiate: () => ({ zIndex: 0, destroy() {}, removeFromParent() {}, getChildByName: () => null, getComponent: () => ({ initialize() { calls.initialize++; } }) }) }; const Host = load('assets/Script/coin_madness/CoinMadnessHost.ts', cc, name => { if (name.includes('ActivityBundleLoader')) return { default: { acquireBundle() { calls.acquire++; return lease; } } }; if (name.includes('Utils')) return { default: { serverUrl: () => 'test' } }; diff --git a/tools/test-home-popup-policy.cjs b/tools/test-home-popup-policy.cjs index 94e36ee5..0448a1cd 100644 --- a/tools/test-home-popup-policy.cjs +++ b/tools/test-home-popup-policy.cjs @@ -132,6 +132,7 @@ function harness() { setStorage: (k, v) => storage.set(k, structuredClone(v)) } } }; const Queue = moduleAt('assets/home_popup_queue/HomePopupQueue.ts', { + '../Script/SharedPopupBackdrop': { default: { get: () => ({ hold() {}, release() {}, setWaiting() {} }) } }, './HomePopupPolicy': policyModule, '../Script/module/Config/ActivityScheduleService': { default: { now: () => now } }, '../Script/module/Pay/Utils': { default: { apiBaseUrl: () => server } } diff --git a/tools/test-shared-popup-backdrop.cjs b/tools/test-shared-popup-backdrop.cjs new file mode 100644 index 00000000..2d9ca25e --- /dev/null +++ b/tools/test-shared-popup-backdrop.cjs @@ -0,0 +1,400 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const ts = require('typescript'); +const path = require('node:path'); + +function fixture() { + class Component { get enabledInHierarchy() { return this.enabled !== false && this.node.activeInHierarchy; } } + class RenderComponent extends Component { enabled = true; } + class Graphics extends RenderComponent { + points = []; + clear() { this.points = []; } + moveTo(x, y) { this.points.push([x, y]); } + lineTo(x, y) { this.points.push([x, y]); } + close() {} fill() { this.paintedAlpha = this.fillColor.a; } + } + class Sprite extends RenderComponent {} + class BlockInputEvents extends Component {} + class Label extends RenderComponent {} + class Node { + constructor(name) { + this.name = name; this.children = []; this.components = []; this.handlers = {}; + this.active = true; this.opacity = 255; this.zIndex = 0; + this.width = 1080; this.height = 1920; this.anchorX = this.anchorY = .5; + this.x = this.y = 0; this.scaleX = this.scaleY = 1; + } + get activeInHierarchy() { return !this.destroyed && this.active && (!this.parent || this.parent.activeInHierarchy); } + addChild(node, z = 0) { node.parent = this; node.zIndex = z; this.children.push(node); } + getSiblingIndex() { return this.parent ? this.parent.children.indexOf(this) : 0; } + getComponent(Type) { return this.components.find(c => typeof Type === 'string' ? c.constructor.name === Type : c instanceof Type) || null; } + getComponents(Type) { return this.components.filter(c => c instanceof Type); } + addComponent(Type) { const c = new Type(); c.node = this; this.components.push(c); return c; } + getChildByName(name) { return this.children.find(n => n.name === name) || null; } + setContentSize(w, h) { this.width = typeof w === 'object' ? w.width : w; this.height = typeof w === 'object' ? w.height : h; } + getContentSize() { return { width: this.width, height: this.height }; } + convertToWorldSpaceAR(p) { + const point = { x: p.x * this.scaleX + this.x, y: p.y * this.scaleY + this.y }; + return this.parent ? this.parent.convertToWorldSpaceAR(point) : point; + } + convertToNodeSpaceAR(p) { + const point = this.parent ? this.parent.convertToNodeSpaceAR(p) : p; + return { x: (point.x - this.x) / this.scaleX, y: (point.y - this.y) / this.scaleY }; + } + on(event, fn) { this.handlers[event] = fn; } + stopAllActions() {} + destroy() { this.destroyed = true; } + } + Node.EventType = { TOUCH_END: 'touchend' }; + const cc = { Node, Component, RenderComponent, Sprite, Graphics, Label, BlockInputEvents, + _decorator: { ccclass: c => c }, macro: { MAX_ZINDEX: 32767 }, Color: { BLACK: { r: 0, g: 0, b: 0, a: 255 } }, + color: (r, g, b, a = 255) => ({ r, g, b, a, _val: ((a << 24) | (b << 16) | (g << 8) | r) >>> 0 }), + isValid: n => !!n && !n.destroyed, v2: (x, y) => ({ x, y }), warn() {}, error() {} }; + const load = (file, deps = {}) => { + const exports = {}; + const code = ts.transpileModule(fs.readFileSync(file, 'utf8'), { compilerOptions: { target: 7, module: 1, experimentalDecorators: true } }).outputText; + new Function('exports', 'require', 'cc', code)(exports, name => { + assert.ok(name in deps, 'unexpected dependency ' + name); return deps[name]; + }, cc); + return exports; + }; + const runningTweens = new Set(); + cc.Tween = { stopAllByTarget: target => runningTweens.delete(target) }; + cc.tween = target => ({ delay() { return this; }, to() { return this; }, call() { return this; }, start() { runningTweens.add(target); } }); + cc.instantiate = node => { + const copy = new Node(node.name); + for (const key of ['x', 'y', 'scaleX', 'scaleY', 'opacity', 'active', 'width', 'height']) copy[key] = node[key]; + node.components.forEach(c => { const props = { ...c }; delete props.node; Object.assign(copy.addComponent(c.constructor), props); }); + node.children.forEach(child => copy.addChild(cc.instantiate(child))); + return copy; + }; + const animationModule = load('assets/Script/LoadingCatAnimation.ts'); + const managerModule = load('assets/Script/SharedPopupBackdrop.ts', { './LoadingCatAnimation': animationModule }); + const Source = load('assets/Script/PopupBackdropSource.ts', { './SharedPopupBackdrop': managerModule }).default; + const canvas = new Node('Canvas'), manager = managerModule.default.get(canvas); + const template = new Node('Loading'); template.active = false; canvas.addChild(template); + const scene = JSON.parse(fs.readFileSync('assets/Scene/HomeScene.fire')); + const authored = scene.find(n => n.__type__ === 'cc.Node' && n._name === 'Loading' && scene[n._parent?.__id__]?._name === 'Canvas'); + const fromScene = data => { + const node = new Node(data._name); node.x = data._trs.array[0]; node.y = data._trs.array[1]; node.opacity = data._opacity; + for (const ref of data._components) { + const c = scene[ref.__id__]; + if (c.__type__ === 'cc.Sprite') node.addComponent(Sprite).spriteFrame = c._spriteFrame; + if (c.__type__ === 'cc.Label') Object.assign(node.addComponent(Label), { string: c._string, fontSize: c._fontSize }); + } + data._children.forEach(ref => node.addChild(fromScene(scene[ref.__id__]))); + return node; + }; + authored._children.forEach(ref => template.addChild(fromScene(scene[ref.__id__]))); + function popup(name, parent = canvas, z = 0) { + const root = new Node(name); parent.addChild(root, z); + const mask = new Node('mask'); root.addChild(mask); mask.opacity = 150; + const sprite = mask.addComponent(Sprite), input = mask.addComponent(BlockInputEvents); + mask.on('touchend', () => {}); + const source = mask.addComponent(Source); source.onLoad(); source.onEnable(); + return { root, mask, source, sprite, input, visual: source.source.visual, + close() { root.active = false; source.onDisable(); }, + open() { root.active = true; source.onEnable(); } }; + } + return { cc, Node, canvas, manager, managerModule, Source, load, popup, template, runningTweens, alpha: node => node.getComponent(Graphics).paintedAlpha }; +} + +test('A closes before B finishes loading: every rendered frame has one equally dark backdrop', () => { + const f = fixture(), owner = {}; + f.manager.hold(owner, () => {}); + const a = f.popup('A'); f.manager.lateUpdate(.016); + assert.equal(a.visual.active, true); assert.equal(a.sprite.enabled, false); + assert.equal(f.alpha(a.visual), 200); assert.equal(f.manager.bridge.active, false); + a.close(); + assert.equal(f.manager.bridge.active, true, 'bridge takes over synchronously, before next queue update'); + for (let frame = 0; frame < 80; frame++) { + f.manager.lateUpdate(.016); + assert.equal(f.manager.bridge.active, true); assert.equal(f.alpha(f.manager.bridge), 200); + } + assert.equal(f.manager.message.active, true); + const b = f.popup('B'); f.manager.lateUpdate(.016); + assert.equal(f.manager.bridge.active, false); assert.equal(b.visual.active, true); + assert.equal(f.alpha(b.visual), 200); assert.equal(a.visual.active, false); + f.manager.release(owner); assert.equal(b.visual.active, true, 'releasing queue ownership cannot hide a manual/visible modal'); + b.close(); f.manager.lateUpdate(.075); + assert.ok(f.alpha(f.manager.bridge) > 0 && f.alpha(f.manager.bridge) < 200); + f.manager.lateUpdate(.08); assert.equal(f.manager.bridge.active, false); +}); + +test('nested confirmation dims its parent without stacking masks or changing event bindings', () => { + const f = fixture(), parent = f.popup('Activity'); + const handler = parent.mask.handlers.touchend; + const confirmation = f.popup('Confirmation', parent.root); + f.manager.lateUpdate(.016); + assert.equal(parent.visual.active, false); assert.equal(confirmation.visual.active, true); + assert.equal(parent.input.node, parent.mask); assert.equal(parent.mask.handlers.touchend, handler); + confirmation.close(); f.manager.lateUpdate(.016); + assert.equal(parent.visual.active, true); assert.equal(confirmation.visual.active, false); + assert.equal(f.manager.bridge, null, 'no scene bridge needed for nested handoff'); +}); + +test('shared opacity matches the original Home Loading mask, which also has an adapter', () => { + const f = fixture(), scene = JSON.parse(fs.readFileSync('assets/Scene/HomeScene.fire')); + const loading = scene.find(n => n.__type__ === 'cc.Node' && n._name === 'Loading' && scene[n._parent?.__id__]?._name === 'Canvas'); + const mask = loading._children.map(ref => scene[ref.__id__]).find(n => n._name === 'New Sprite(Splash)'); + assert.equal(mask._opacity, 200); + assert.equal(f.managerModule.POPUP_BACKDROP_OPACITY, mask._opacity); + assert.ok(mask._components.some(ref => scene[ref.__id__].__type__ === '613aechc9tHfZ76dlmB0WQb')); +}); + +test('Gold Miner mounts old and new remote prefabs with exactly one managed mask', () => { + for (const serialized of [false, true]) { + const f = fixture(); + f.cc.instantiate = prefab => prefab; + const Host = f.load('assets/Script/coin_madness/CoinMadnessHost.ts', { + '../Sdk/MiniGameSdk': {}, '../../home_popup_queue/HomePopupQueue': {}, + './GoldMinerService': {}, '../module/Pay/Utils': {}, '../module/Config/ActivityBundleLoader': {}, + '../PopupBackdropSource': { default: f.Source } + }).default; + const host = f.canvas.addComponent(Host); + const mounted = []; + for (const [name, maskName] of [['CoinMadness', 'Dim'], ['CoinMadnessSettlement', 'Dim'], ['CoinMadnessHelp', 'Mask']]) { + const root = new f.Node(name), mask = new f.Node(maskName); + root.addChild(mask); mask.opacity = maskName === 'Dim' ? 175 : 190; + const sprite = mask.addComponent(f.cc.Sprite); + const handler = () => {}; mask.on('touchend', handler); + // Emulate Creator lifecycle: serialized components enable on mounting; + // newly added components enable immediately on an active scene node. + if (serialized) { const source = mask.addComponent(f.Source); source.onLoad(); } + const add = mask.addComponent.bind(mask); + mask.addComponent = Type => { const c = add(Type); c.onLoad(); c.onEnable(); return c; }; + const mount = f.canvas.addChild.bind(f.canvas); + f.canvas.addChild = (node, z) => { + mount(node, z); + const source = mask.getComponent(f.Source); if (source) source.onEnable(); + }; + assert.equal(host.mountPopup(root, maskName, name.endsWith('Help') ? 100 : 99), root); + f.canvas.addChild = mount; + f.manager.lateUpdate(.016); + const sources = mask.getComponents(f.Source); + assert.equal(sources.length, 1); + assert.equal(sprite.enabled, false, 'the old Sprite must not stack with the managed backdrop'); + assert.equal(mask.handlers.touchend, handler); + assert.equal(f.alpha(sources[0].source.visual), 200); + assert.equal(sources[0].source.visual.active, true); + assert.equal(f.manager.visibleSources().filter(s => s.visual.active).length, 1); + mounted.push({ root, source: sources[0] }); + } + const help = mounted.pop(); help.root.active = false; help.source.onDisable(); + assert.equal(mounted[1].source.source.visual.active, true, 'closing help restores the underlying managed mask'); + } +}); + +test('topmost sibling wins by zIndex, including a popup opened after a higher reward', () => { + const f = fixture(), high = f.popup('Reward', f.canvas, 1001), low = f.popup('Activity', f.canvas, 2); + f.manager.lateUpdate(.016); + assert.equal(high.visual.active, true); assert.equal(low.visual.active, false); + high.close(); assert.equal(low.visual.active, true); + high.open(); assert.equal(high.visual.active, true); assert.equal(low.visual.active, false); +}); + +test('loading, legacy mask, queued bridge and activity never darken the screen together', () => { + const f = fixture(), owner = {}; + const loading = f.popup('Loading', f.canvas, f.cc.macro.MAX_ZINDEX); + const countLayers = () => { + let layers = 0; + const visit = node => { + if (!node.activeInHierarchy) return; + for (const c of node.components) { + if (c.enabled === false) continue; + if (c instanceof f.cc.Sprite || c instanceof f.cc.Graphics && c.paintedAlpha > 0) layers++; + } + node.children.forEach(visit); + }; + visit(f.canvas); return layers; + }; + const check = () => { + assert.equal(countLayers(), 1, 'exactly one rendered backdrop, including original Sprite masks'); + const active = f.manager.visibleSources().find(s => s.visual.active); + const visual = active ? active.visual : f.manager.bridge; + assert.equal(f.alpha(visual), 200); + assert.equal(visual.opacity, 255, 'Canvas must not multiply fill alpha by another node opacity'); + }; + f.manager.hold(owner, () => {}); check(); + loading.close(); check(); // The bridge covers asynchronous mounting. + const main = f.popup('CoinMadness', f.canvas, f.cc.macro.MAX_ZINDEX - 1); check(); + loading.open(); check(); // A second request can show Loading above the existing activity. + assert.equal(main.visual.active, false); + loading.close(); check(); + const help = f.popup('Help', f.canvas, f.cc.macro.MAX_ZINDEX); check(); + help.close(); check(); + main.close(); check(); + f.manager.release(owner); f.manager.lateUpdate(.075); + assert.equal(f.alpha(f.manager.bridge), 100, 'fade updates the drawn alpha in the same frame'); + assert.equal(countLayers(), 1); + f.manager.lateUpdate(.08); assert.equal(countLayers(), 0); +}); + +const creator = process.env.COCOS_CREATOR_PATH || 'C:/ProgramData/cocos/editors/Creator/2.4.15'; +const renderer = path.join(creator, 'resources/engine/cocos2d/core/renderer'); +test('Creator WebGL receives alpha 200 for managed masks and 100 halfway through the fade', { + skip: !fs.existsSync(path.join(renderer, 'assembler.js')) && 'set COCOS_CREATOR_PATH to run the engine check' +}, () => { + const f = fixture(); + f.cc.gfx = { VertexFormat: class {} }; + const base = f.load(path.join(renderer, 'assembler.js'), { './webgl/vertex-format': {}, './assembler-pool': {} }); + const GraphicsAssembler = f.load(path.join(renderer, 'webgl/assemblers/graphics/index.js'), { + '../../../assembler': base, '../../../../../renderer/core/input-assembler': {}, '../../mesh-buffer': {}, + '../../../index': {}, '../../../../graphics/graphics': { LineJoin: {}, LineCap: {} }, + '../../../../graphics/types': { PointFlags: {} }, './earcut': {}, './impl': {} + }).default; + // Execute the installed engine's real fill/vertex-color methods; geometry allocation is irrelevant to alpha. + const vertexAlpha = node => { + const assembler = new GraphicsAssembler(), data = new ArrayBuffer(16); + assembler._buffer = { vertexStart: 0, meshbuffer: { _vData: new Float32Array(data), _uintVData: new Uint32Array(data) } }; + assembler._expandFill = function () { this._vset(0, 0); }; + assembler.fill({ node, _fillColor: node.getComponent(f.cc.Graphics).fillColor, _impl: {} }); + return assembler._buffer.meshbuffer._uintVData[2] >>> 24; + }; + const popup = f.popup('CoinMadness'); + assert.equal(vertexAlpha(popup.visual), 200); + popup.close(); assert.equal(vertexAlpha(f.manager.bridge), 200); + f.manager.lateUpdate(.075); assert.equal(vertexAlpha(f.manager.bridge), 100); + f.manager.lateUpdate(.08); assert.equal(vertexAlpha(f.manager.bridge), 0); +}); + +test('backdrop covers the canvas inside translated/scaled parents and after viewport resize', () => { + const f = fixture(), p = f.popup('Scaled'); + p.root.scaleX = .5; p.root.scaleY = .4; p.root.x = 70; p.root.y = -20; + f.canvas.width = 1200; f.canvas.height = 2400; + f.manager.lateUpdate(.016); + const points = p.visual.getComponent(f.cc.Graphics).points.map(([x, y]) => p.mask.convertToWorldSpaceAR({ x, y })); + assert.deepEqual(points, [{ x: -600, y: -1200 }, { x: 600, y: -1200 }, { x: 600, y: 1200 }, { x: -600, y: 1200 }]); +}); + +test('slow loading has feedback, cancellation releases the input shield, and stale clicks do nothing', () => { + const f = fixture(), owner = {}; let cancelled = 0; + f.manager.hold(owner, () => { cancelled++; f.manager.release(owner); }); + f.manager.lateUpdate(.7); assert.equal(f.manager.message.active, false); + f.manager.lateUpdate(.2); assert.equal(f.manager.message.active, true); + f.manager.lateUpdate(10); assert.equal(f.manager.cancelButton.active, true); + assert.ok(f.manager.bridge.getComponent(f.cc.BlockInputEvents)); + f.manager.cancelButton.handlers.touchend({ stopPropagation() {} }); + assert.equal(cancelled, 1); f.manager.lateUpdate(.2); assert.equal(f.manager.bridge.active, false); + f.manager.hold(owner, () => { cancelled++; }); f.manager.lateUpdate(11); + f.popup('NowReady'); f.manager.cancelButton.handlers.touchend({ stopPropagation() {} }); + assert.equal(cancelled, 1); +}); + +test('bridge reuses authored cat loading content without its mask and stops animation on handoff', () => { + const f = fixture(), owner = {}; + f.manager.hold(owner, () => f.manager.release(owner)); + f.manager.lateUpdate(.9); + const content = f.manager.message, cat = content.getChildByName('cat'); + assert.equal(content.active, true); + assert.deepEqual(content.children.map(n => n.name), ['New Label', 'block1', 'block2', 'block3', 'block4', 'cat']); + for (const node of content.children) { + const authored = f.template.getChildByName(node.name); + assert.notEqual(node, authored); + assert.equal(node.x, authored.x); assert.equal(node.y, authored.y); + if (node.getComponent(f.cc.Sprite)) assert.equal(node.getComponent(f.cc.Sprite).spriteFrame, authored.getComponent(f.cc.Sprite).spriteFrame); + } + assert.equal(content.getChildByName('New Label').getComponent(f.cc.Label).string, '加载中,请稍等喵~'); + assert.ok(f.manager.cancelButton.y < content.getChildByName('New Label').y - 70); + assert.equal(f.template.active, false, 'the separate home Loading is not opened'); + assert.ok(f.runningTweens.has(cat)); + const runId = content.__loadingCatAnimationState.runId; + f.manager.lateUpdate(.1); + assert.equal(content.__loadingCatAnimationState.runId, runId, 'do not restart animation every frame'); + const activity = f.popup('Activity'); + assert.equal(content.active, false); assert.equal(f.runningTweens.has(cat), false); + activity.close(); f.manager.lateUpdate(.9); + assert.ok(f.runningTweens.has(cat)); + f.manager.release(owner); + assert.equal(content.active, false); assert.equal(f.runningTweens.has(cat), false); + f.manager.hold(owner, () => {}); f.manager.lateUpdate(.9); f.manager.onDestroy(); + assert.equal(f.runningTweens.has(cat), false); +}); + +test('scene cleanup drops leases and visuals; another scene gets a fresh manager', () => { + const f = fixture(); f.manager.hold({}, () => assert.fail('cleanup is not user cancellation')); + const p = f.popup('A'); + const node = f.manager.bridge; f.manager.onDestroy(); + assert.equal(node.destroyed, true); assert.equal(p.visual.active, false); assert.equal(f.manager.holds.size, 0); + p.close(); f.manager.refresh(); assert.equal(f.manager.bridge, null); + const other = new f.Node('Canvas'); assert.notEqual(f.managerModule.default.get(other), f.manager); +}); + +test('queue retains the backdrop across failed/skipped tasks and ignores late callbacks after cancellation', () => { + const f = fixture(), policyModule = f.load('assets/home_popup_queue/HomePopupPolicy.ts'); + f.cc.fx = { GameConfig: { GM_INFO: { uid: 'one', token: 'token', level: 80 }, getNewPlayerHomeLevel: () => 15, + isGameplayEntryPreparing: () => false }, StorageMessage: { getStorage: () => null, setStorage() {} } }; + const Queue = f.load('assets/home_popup_queue/HomePopupQueue.ts', { + './HomePopupPolicy': policyModule, '../Script/SharedPopupBackdrop': f.managerModule, + '../Script/module/Config/ActivityScheduleService': { default: { now: () => Date.now() } }, + '../Script/module/Pay/Utils': { default: { apiBaseUrl: () => 'test' } } + }).default; + const queue = f.canvas.addComponent(Queue); queue.onLoad(); queue.sources.clear(); + let delayed, cleanup = 0; + queue.enqueue('failed', 0, done => done()); queue.enqueue('next', 1, done => { + delayed = done; return () => { cleanup++; }; + }); + queue.update(); f.manager.lateUpdate(.016); assert.equal(f.manager.bridge.active, true); + queue.update(); assert.equal(queue.running, 'next'); assert.equal(f.alpha(f.manager.bridge), 200); + class CoinMadnessHost extends f.cc.Component { isPopupActive() { return true; } } + f.canvas.addComponent(CoinMadnessHost); + queue.update(); f.manager.lateUpdate(.9); + assert.equal(f.manager.bridge.active, true, 'a loading flag is not an already-visible popup'); + assert.equal(f.manager.message.active, true); + queue.cancelBatch(); delayed(); f.manager.lateUpdate(.2); + assert.equal(queue.running, ''); assert.equal(f.manager.bridge.active, false); + assert.equal(queue.canPresent('next'), false); + queue.cancelBatch(); assert.equal(cleanup, 1, 'loading input shields are cleaned up exactly once'); +}); + +for (const cancel of [false, true]) test(`manual reward handoff retains the backdrop during a cold load; cancel=${cancel}`, async () => { + const f = fixture(), a = f.popup('Activity'); + f.cc.fx = { GameConfig: { isGameplayEntryPreparing: () => false } }; + const ast = ts.createSourceFile('JiaZai.ts', fs.readFileSync('assets/Script/JiaZai.ts', 'utf8'), 99, true); + const cls = ast.statements.find(ts.isClassDeclaration); + const method = cls.members.find(m => m.name?.getText(ast) === 'openRewardWindow').getText(ast); + const code = ts.transpileModule(`class Subject { ${method} }; return Subject;`, { compilerOptions: { target: 7 } }).outputText; + const Subject = new Function('cc', 'JiaZai', 'HomePopupQueue', 'SharedPopupBackdrop', code)(f.cc, + { cachedRewardPrefab: null }, { get: () => ({ has: () => false }) }, f.managerModule.default); + const home = new Subject(); home.node = f.canvas; home.isHomeRuntimeAlive = () => true; + let ready, closed = 0, opened = 0; + const request = new Promise(resolve => ready = resolve); + home.loadHomePopupPrefab = () => request; + home.openRewardWindow([{ type: 'coin', count: 10 }], undefined, () => closed++); + a.close(); f.manager.lateUpdate(11); + assert.equal(f.manager.bridge.active, true); + if (cancel) f.manager.cancelButton.handlers.touchend({ stopPropagation() {} }); + home.openRewardWindow = () => { opened++; f.popup('Reward'); }; + ready(); await request; await Promise.resolve(); + assert.equal(opened, cancel ? 0 : 1); assert.equal(closed, cancel ? 1 : 0); + assert.equal(f.manager.holds.size, 0); + f.manager.lateUpdate(.2); assert.equal(f.manager.bridge.active, false); +}); + +test('activity prefab adapters cover dim masks while preserving original render and input components', () => { + const uuid = JSON.parse(fs.readFileSync('assets/Script/PopupBackdropSource.ts.meta')).uuid; + const h = uuid.replaceAll('-', ''), alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + let id = h.slice(0, 5); for (let i = 5; i < 32; i += 3) { const n = parseInt(h.slice(i, i + 3), 16); id += alphabet[n >> 6] + alphabet[n & 63]; } + const folders = ['action_bundle', 'shop', 'passCheck', 'cloud_rise_home', 'coin_madness', 'pyramid', + 'seven_day_gift', 'jungle_treasure', 'mini_program_benefits', 'publicUI']; + const files = folders.flatMap(folder => fs.readdirSync(`assets/${folder}/prefab`).filter(file => file.endsWith('.prefab')) + .map(file => path.join('assets', folder, 'prefab', file))) + .concat(['stage1', 'stage2', 'stage3'].map(stage => `assets/cloud_rise/${stage}/prefab/CloudRise.prefab`), 'assets/Scene/HomeScene.fire'); + let boundFiles = 0; + for (const file of files) { + const current = JSON.parse(fs.readFileSync(file)); + const components = current.map((item, i) => item.__type__ === id ? i : -1).filter(i => i >= 0); + if (components.length) boundFiles++; + for (const index of components) { + const mask = current[current[index].node.__id__]; + assert.ok(mask._color.r < 40 && mask._color.g < 40 && mask._color.b < 40, file + ': only dim backgrounds'); + assert.ok(mask._components.some(ref => ['cc.Sprite', 'cc.Graphics'].includes(current[ref.__id__].__type__)), file); + if (current[mask._parent?.__id__]?._name === 'Loading') { + assert.equal(file, 'assets/Scene/HomeScene.fire'); + assert.equal(mask._name, 'New Sprite(Splash)', 'only the backdrop, never the loading artwork'); + } + assert.ok(mask._components.some(ref => ref.__id__ === index)); + } + } + assert.equal(boundFiles, 29); +});