From b9664de9bd226d7ae29b8fa5d901a16c61a78458 Mon Sep 17 00:00:00 2001 From: Art_Sakura <1754798088@qq.com> Date: Thu, 29 Oct 2026 14:28:19 +0800 Subject: [PATCH] =?UTF-8?q?fixed=20#=20=E4=BF=AE=E6=AD=A3=E6=A0=B7?= =?UTF-8?q?=E5=BC=8FBUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mengsan/tools/test-battle-hud.cjs | 64 ++++++++++++- mengsan/ui/hand/input.js | 20 ++-- mengsan/ui/hud/index.js | 151 +++++++++++++++++++++++++----- mengsan/ui/hud/style.css | 16 ++-- update.js | 64 ++++++++++++- 5 files changed, 274 insertions(+), 41 deletions(-) diff --git a/mengsan/tools/test-battle-hud.cjs b/mengsan/tools/test-battle-hud.cjs index 6163b67..f3e7240 100644 --- a/mengsan/tools/test-battle-hud.cjs +++ b/mengsan/tools/test-battle-hud.cjs @@ -7,6 +7,20 @@ const app = path.resolve(__dirname, '../../../..'); const packages = process.env.CODEX_NODE_MODULES || 'C:/Users/Diuse/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules'; const { chromium } = require(path.join(packages, 'playwright')); const prefix = '/extension/' + encodeURIComponent('术樱包') + '/mengsan'; +const holdStatus = async (page, locator, pointerType = 'mouse') => { + const box = await locator.boundingBox(); + const point = {x:box.x + box.width / 2,y:box.y + box.height / 2}; + if (pointerType === 'touch') { + await locator.dispatchEvent('pointerdown', {button:0,pointerId:44, + pointerType,clientX:point.x,clientY:point.y}); + await page.waitForTimeout(550); + await page.evaluate(() => window.dispatchEvent( + new PointerEvent('pointerup', {pointerId:44,pointerType:'touch'}))); + } else { + await page.mouse.move(point.x,point.y); await page.mouse.down(); + await page.waitForTimeout(550); await page.mouse.up(); + } +}; // Native themes use different button sizes. Drag onto an exposed point on the // original player, rather than assuming its center is clear in the dense fixture. const exposedTarget = (page,index) => page.evaluate(index => { @@ -265,17 +279,59 @@ const server = http.createServer((req, res) => { await page.evaluate(()=>{game.players[7].hp=0;hud.refresh();}); assert.equal(await page.evaluate(()=>game.players[7].style.cssText===originalEnemyPresentation[6].style&&!game.players[7].classList.contains('mengsan-hud-defeated-shuying')),true,'death keeps the original battlefield presentation');checks++; await page.evaluate(()=>{game.players[7].hp=40;game.me.storage.mengsanVulnerable_shuying=3;hud.refresh();}); - assert.match(await page.locator('.mengsan-hud-statuses-shuying').innerText(),/3/);checks++; + assert.equal(await page.locator('.mengsan-hud-statuses-shuying').innerText(),'','status chips contain icons only');checks++; + const heldStatus = page.locator('.mengsan-hud-statuses-shuying button').first(); + await heldStatus.dispatchEvent('pointerdown', {button:0,pointerId:45, + pointerType:'touch',clientX:10,clientY:10}); + await page.waitForTimeout(150); + await page.evaluate(() => { + game.me.storage.mengsanWeak_shuying=1; hud.refresh(); + }); + await page.waitForTimeout(450); + assert.equal(await page.locator('dialog.mengsan-hud-dialog-shuying').count(),1,'unrelated status update preserves an active hold');checks++; + await page.evaluate(() => window.dispatchEvent( + new PointerEvent('pointerup', {pointerId:45,pointerType:'touch'}))); + await page.evaluate(() => { + game.me.storage.mengsanVulnerable_shuying=5; hud.refresh(); + }); + assert.match(await page.locator('dialog.mengsan-hud-dialog-shuying').innerText(),/易伤 5/,'open details show the current value');checks++; + await page.evaluate(() => { + delete game.me.storage.mengsanVulnerable_shuying; hud.refresh(); + }); + assert.match(await page.locator('dialog.mengsan-hud-dialog-shuying').innerText(),/已失效/,'removed state is explicitly expired');checks++; + await page.getByRole('button',{name:'关闭',exact:true}).click(); + await page.evaluate(() => { + delete game.me.storage.mengsanWeak_shuying; + game.me.storage.mengsanVulnerable_shuying=3; hud.refresh(); + }); await page.evaluate(()=>{for(const key of ['Weak','Frail','Artifact','Constrict','Tangled','Shrink','Plow','Slippery'])game.me.storage['mengsan'+key+'_shuying']=2;hud.refresh();}); assert.equal(await page.evaluate(()=>{const row=document.querySelector('.mengsan-hud-statuses-shuying').getBoundingClientRect();return [...document.querySelectorAll('.mengsan-hud-statuses-shuying button')].every(n=>{const r=n.getBoundingClientRect();return r.bottom<=row.bottom+1&&r.right<=row.right+1;});}),true,'matrix overflow keeps the visible statuses and more button inside the grid');checks++; - await page.getByRole('button',{name:'查看全部状态',exact:true}).click(); + // Compact icon chips fit more states; register enough real display fields + // to exercise overflow on wide screens as well as phones. + await page.evaluate(async prefix => { + const {bindCombatStatusSource} = await import(prefix+'/battle/status-sources.js'); + const keys=['turnStrength','turnEnergy','turnBlock','turnLoseHp', + 'infernoLoseHp','lossDamage','lossStrength','vulnerableBonus', + 'attackBlock','retaliation','colossus','freeAttack','noDraw', + 'noEnergy','doubleTap','plating','exhaustBlock','exhaustDraw', + 'vulnerableDraw','juggle','endAttack','retrieveAttack','bodyguard', + 'autoStrike','firstBlock','blockDamage','barricade','corruption']; + bindCombatStatusSource(game.me,session,'overflow-test',() => ({ + powers:Object.fromEntries(keys.map(key => [key,1])), + })); + hud.refresh(); + },prefix); + await holdStatus(page,page.getByRole('button',{ + name:'长按查看全部状态',exact:true})); assert.ok(await page.getByRole('dialog').isVisible(),'overflow states remain available in the details');checks++; await page.getByRole('button',{name:'关闭',exact:true}).click(); assert.equal(await page.evaluate(()=>{const s=document.querySelector('.mengsan-hud-center-shuying').getBoundingClientRect(),p=game.me.getBoundingClientRect();return s.bottom<=p.top&&p.top-s.bottom<=13;}),true,'status chips automatically appear just above the portrait');checks++; - await page.locator('.mengsan-hud-statuses-shuying button').first().click(); + await page.locator('.mengsan-hud-statuses-shuying button').first().tap(); + assert.equal(await page.locator('dialog.mengsan-hud-dialog-shuying').count(),0,'short tap does not open status details');checks++; + await holdStatus(page,page.locator('.mengsan-hud-statuses-shuying button').first()); assert.ok(await page.locator('dialog.mengsan-hud-dialog-shuying').isVisible());checks++; await page.getByRole('button',{name:'关闭',exact:true}).click(); - await page.locator('.mengsan-hud-statuses-shuying button').first().tap(); + await holdStatus(page,page.locator('.mengsan-hud-statuses-shuying button').first(),'touch'); assert.ok(await page.locator('dialog.mengsan-hud-dialog-shuying').isVisible(),'touch opens the player status details');checks++; await page.getByRole('button',{name:'关闭',exact:true}).click(); await page.evaluate(()=>game.uncheck()); diff --git a/mengsan/ui/hand/input.js b/mengsan/ui/hand/input.js index c942dd4..80ed210 100644 --- a/mengsan/ui/hand/input.js +++ b/mengsan/ui/hand/input.js @@ -81,19 +81,25 @@ export function createHandInput(player, session, adapter, fan, env, refresh, log aim = document.createElementNS("http://www.w3.org/2000/svg", "svg"); aim.classList.add("mengsan-hand-aim-shuying"); aim.setAttribute("aria-hidden", "true"); + aim.setAttribute("preserveAspectRatio", "none"); const path = document.createElementNS(aim.namespaceURI, "path"); aim.appendChild(path); document.body.appendChild(aim); } const rect = card.getBoundingClientRect(); const x = rect.left + rect.width / 2, y = rect.top + 12; aim.setAttribute("viewBox", `0 0 ${window.innerWidth} ${window.innerHeight}`); - // clientX/Y 与卡牌矩形都是视口坐标,SVG 本身却可能受到 body zoom/transform 影响。 - // 用 SVG 实际屏幕矩阵反变换,不能重复除 documentZoom 或假定 SVG 没有偏移。 - const matrix = aim.getScreenCTM(); - if (!matrix) { removeAim(); return; } - const inverse = matrix.inverse(); - const start = new window.DOMPoint(x, y).matrixTransform(inverse); - const end = new window.DOMPoint(point.x, point.y).matrixTransform(inverse); + // Use painted bounds instead of relying on WebView CTM handling of zoom. + // With preserveAspectRatio=none, viewport-to-viewBox scaling is linear. + const bounds = aim.getBoundingClientRect(); + if (!(bounds.width > 0 && bounds.height > 0)) { + removeAim(); return; + } + const scaleX = window.innerWidth / bounds.width; + const scaleY = window.innerHeight / bounds.height; + const start = { x: (x - bounds.left) * scaleX, + y: (y - bounds.top) * scaleY }; + const end = { x: (point.x - bounds.left) * scaleX, + y: (point.y - bounds.top) * scaleY }; if (![start.x, start.y, end.x, end.y].every(Number.isFinite)) { removeAim(); return; } // 十周年金色细线(本地 CONFIG.LINE_COLOR/LINE_WIDTH),不采用尖塔箭头/骨节造型。 aim.firstElementChild.setAttribute("d", `M ${start.x} ${start.y} L ${end.x} ${end.y}`); diff --git a/mengsan/ui/hud/index.js b/mengsan/ui/hud/index.js index a0e8590..e5f456f 100644 --- a/mengsan/ui/hud/index.js +++ b/mengsan/ui/hud/index.js @@ -55,56 +55,160 @@ export function mountBattleHUD(player, battle, env) { const statuses = create('div', 'mengsan-hud-statuses-shuying', center); statuses.setAttribute('aria-label', '玩家当前状态'); const link = create('link', '', null); link.rel = 'stylesheet'; link.href = `${assetBase}/ui/hud/style.css`; + let detailTarget = null, detailId = null, detailBody = null; + let detailSignature = null; const closeDialog = () => { if (!dialog) return; const current = dialog; dialog = null; + detailTarget = null; detailId = null; detailBody = null; + detailSignature = null; if (current.open) current.close(); current.remove(); if (previousFocus?.isConnected) previousFocus.focus(); }; - const openDetails = target => { - if (disposed || !battle.session.active) return; + const refreshDetails = () => { + if (!dialog || !detailBody) return; + const entries = collectCombatStatuses(detailTarget, battle) + .filter(entry => detailId === null || entry.id === detailId); + const signature = JSON.stringify(entries); + if (signature === detailSignature) return; + detailSignature = signature; detailBody.replaceChildren(); + for (const entry of entries) { + const row = create('section', 'mengsan-hud-detail-row-shuying', + detailBody); + create('strong', '', row, `${entry.name}${entry.value == null ? '' : ` ${entry.value}${entry.unit ? ` ${entry.unit}` : ''}`}`); + create('p', '', row, entry.detail); + } + if (!entries.length) create('p', '', detailBody, + detailId === null ? '暂无已登记状态。' : '该状态已失效。'); + }; + const openDetails = (target, id = null) => { + if (disposed || frozen || !battle.session.active) return; closeDialog(); previousFocus = document.activeElement; dialog = create('dialog', 'mengsan-hud-dialog-shuying', document.body); const current = dialog; create('h2', '', dialog, `${nameOf(target)} · 当前状态`); const close = create('button', 'mengsan-hud-dialog-close-shuying', dialog, '关闭'); close.addEventListener('click', closeDialog); - const entries = collectCombatStatuses(target, battle); - for (const entry of entries) { - const row = create('section', 'mengsan-hud-detail-row-shuying', dialog); - create('strong', '', row, `${entry.name}${entry.value == null ? '' : ` ${entry.value}${entry.unit ? ` ${entry.unit}` : ''}`}`); - create('p', '', row, entry.detail); - } - if (!entries.length) create('p', '', dialog, '暂无已登记状态。'); + detailTarget = target; detailId = id; + detailBody = create('div', 'mengsan-hud-detail-body-shuying', dialog); + refreshDetails(); // Unknown/native marks retain their engine intro and are never hidden by the HUD. dialog.addEventListener('cancel', event => { event.preventDefault(); closeDialog(); }); dialog.addEventListener('close', () => { if (dialog === current) closeDialog(); }); for (const type of ['pointerdown', 'pointerup', 'click', 'touchstart', 'touchend', 'keydown']) dialog.addEventListener(type, event => event.stopPropagation()); dialog.showModal(); close.focus(); }; + let statusHold = null, statusTimer = null; + const cancelStatusHold = () => { + if (statusTimer !== null) window.clearTimeout(statusTimer); + statusHold?.button.classList.remove('is-pressed'); + statusHold = null; statusTimer = null; + }; + const statusButton = event => { + const button = event.target.closest?.('.mengsan-hud-status-shuying'); + return button && statuses.contains(button) ? button : null; + }; + const statusDown = event => { + cancelStatusHold(); + const button = statusButton(event); + if (!button || event.button !== 0 || disposed || frozen) return; + statusHold = { button, id: event.pointerId, + x: event.clientX, y: event.clientY }; + button.classList.add('is-pressed'); + statusTimer = window.setTimeout(() => { + cancelStatusHold(); + if (button.isConnected) { + openDetails(player, button.dataset.statusId || null); + } + }, 500); + }; + const statusMove = event => { + if (event.pointerId !== statusHold?.id) return; + if (Math.hypot(event.clientX - statusHold.x, + event.clientY - statusHold.y) > 8) cancelStatusHold(); + }; + const statusUp = event => { + if (event.pointerId === statusHold?.id) cancelStatusHold(); + }; + const statusListeners = [ + [statuses, 'pointerdown', statusDown], + [statuses, 'scroll', cancelStatusHold], + [window, 'pointermove', statusMove, true], + [window, 'pointerup', statusUp, true], + [window, 'pointercancel', statusUp, true], + [window, 'blur', cancelStatusHold], + ]; + for (const args of statusListeners) { + args[0].addEventListener(...args.slice(1)); + } + const stopStatusInput = () => { + cancelStatusHold(); + for (const args of statusListeners) { + args[0].removeEventListener(...args.slice(1)); + } + }; + statuses.addEventListener('click', event => { + event.preventDefault(); + const button = statusButton(event); + // Pointer clicks require a hold; keyboard activation stays accessible. + if (button && event.detail === 0) { + openDetails(player, button.dataset.statusId || null); + } + }); + statuses.addEventListener('contextmenu', event => event.preventDefault()); let statusSignature = null; function renderStatuses() { const entries = collectCombatStatuses(player, battle); - const columns = layout === 'narrow' ? 1 : 2; + const columns = Math.max(1, + Math.floor(((center.clientWidth || 76) + 6) / 38)); const rows = Math.max(1, Math.floor((statuses.clientHeight || 80) / 38)); const maximum = Math.max(0, columns * rows - (entries.length > columns * rows ? 1 : 0)); const signature = JSON.stringify([maximum, entries]); if (signature !== statusSignature) { - statusSignature = signature; statuses.replaceChildren(); + statusSignature = signature; center.hidden = entries.length === 0; + const existing = new Map([...statuses.children].map(button => + [button.dataset.statusId || null, button])); + const wantedButtons = []; for (const entry of entries.slice(0, maximum)) { - const button = create('button', 'mengsan-hud-status-shuying', statuses); - button.setAttribute('aria-label', `${entry.name} ${entry.value ?? ''} ${entry.unit},查看状态详情`); - if (entry.icon) { - const image = create('img', '', button); image.src = `${assetBase}/assets/status/${entry.icon}`; image.alt = ''; - image.addEventListener('error', () => image.remove(), { once: true }); + const button = existing.get(entry.id) || + create('button', 'mengsan-hud-status-shuying', null); + button.dataset.statusId = entry.id; + button.setAttribute('aria-label', `${entry.name} ${entry.value ?? ''} ${entry.unit},长按查看状态详情`); + button.setAttribute('aria-haspopup', 'dialog'); + if (button.dataset.icon !== (entry.icon || '')) { + button.dataset.icon = entry.icon || ''; + button.replaceChildren(); + button.classList.remove('mengsan-hud-status-fallback-shuying'); + if (entry.icon) { + const image = create('img', '', button); + image.src = `${assetBase}/assets/status/${entry.icon}`; + image.alt = ''; + image.addEventListener('error', () => { + image.remove(); + button.classList.add('mengsan-hud-status-fallback-shuying'); + }, { once: true }); + } } - create('span', '', button, entry.name); - if (entry.value != null) create('b', '', button, entry.value); - button.addEventListener('click', () => openDetails(player)); + wantedButtons.push(button); } if (entries.length > maximum) { - const more = create('button', 'mengsan-hud-status-shuying', statuses, `+${entries.length - maximum}`); - more.setAttribute('aria-label', '查看全部状态'); more.addEventListener('click', () => openDetails(player)); + const more = existing.get(null) || create('button', + 'mengsan-hud-status-shuying mengsan-hud-status-more-shuying', + null); + more.setAttribute('aria-label', '长按查看全部状态'); + more.setAttribute('aria-haspopup', 'dialog'); + wantedButtons.push(more); + } + if (statusHold && !wantedButtons.includes(statusHold.button)) { + cancelStatusHold(); + } + for (const button of [...statuses.children]) { + if (!wantedButtons.includes(button)) button.remove(); + } + for (const [index, button] of wantedButtons.entries()) { + if (statuses.children[index] !== button) { + statuses.insertBefore(button, statuses.children[index] || null); + } } } const wanted = new Set(); @@ -236,7 +340,8 @@ export function mountBattleHUD(player, battle, env) { text(name, nameOf(player)); text(hp, `生命 ${player.hp} / ${player.maxHp}`); text(block, `格挡 ${player.hujia || 0}`); healthFill.style.width = `${Math.max(0, Math.min(100, player.maxHp > 0 ? player.hp / player.maxHp * 100 : 0))}%`; text(energy, `费用 ${player.storage.mengsanEnergy_shuying ?? 0} / ${player.storage.mengsanMaxEnergy_shuying ?? 0}`); - syncActors(); syncExternalEquipment(); renderStatuses(); syncControls(); syncNotifications(); + syncActors(); syncExternalEquipment(); renderStatuses(); + refreshDetails(); syncControls(); syncNotifications(); overlays.refresh({ footerHeight:hudHeightFor(), status:_status }); } function schedule() { @@ -246,6 +351,7 @@ export function mountBattleHUD(player, battle, env) { const timer = window.setInterval(schedule, 200); function freeze() { if(disposed || frozen)return; + stopStatusInput(); frozen=true;window.clearInterval(timer); if(frame!==null){window.cancelAnimationFrame(frame);frame=null;} observer?.disconnect();resize?.disconnect();closeDialog();overlays.dispose(); @@ -253,6 +359,7 @@ export function mountBattleHUD(player, battle, env) { } function dispose() { if (disposed) return; disposed = true; + stopStatusInput(); window.clearInterval(timer); if (frame !== null) window.cancelAnimationFrame(frame); observer?.disconnect(); resize?.disconnect(); window.removeEventListener('resize', schedule); document.removeEventListener('visibilitychange', schedule); diff --git a/mengsan/ui/hud/style.css b/mengsan/ui/hud/style.css index b5d1048..1a66a2d 100644 --- a/mengsan/ui/hud/style.css +++ b/mengsan/ui/hud/style.css @@ -19,11 +19,16 @@ .mengsan-hud-health-track-shuying>span { height:100%;background:#9bb78d; } .mengsan-hud-energy-shuying { color:#eacb87;font-size:16px;font-variant-numeric:tabular-nums;white-space:nowrap; } .mengsan-hud-center-shuying { left:40px;right:auto;top:auto;bottom:var(--mengsan-side-dock-bottom);width:var(--mengsan-profile-width);height:var(--mengsan-status-height); } -.mengsan-hud-shuying .mengsan-hud-statuses-shuying { display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-auto-rows:32px;align-content:end;gap:6px;overflow:hidden;white-space:nowrap;height:100%; } -.mengsan-hud-shuying .mengsan-hud-status-shuying { display:flex;align-items:center;gap:4px;max-width:100%;min-width:0;min-height:32px;padding:3px 6px;background:transparent;border-radius:10px; } -.mengsan-hud-status-shuying span { overflow:hidden;text-overflow:ellipsis;font-size:12px; } -.mengsan-hud-status-shuying b { font-variant-numeric:tabular-nums;font-size:13px; } -.mengsan-hud-status-shuying img { width:22px;height:22px;object-fit:contain; } +.mengsan-hud-shuying .mengsan-hud-statuses-shuying { display:grid;grid-template-columns:repeat(auto-fill,32px);grid-auto-rows:32px;align-content:end;gap:6px;overflow:hidden;white-space:nowrap;height:100%; } +.mengsan-hud-shuying .mengsan-hud-status-shuying { display:flex;align-items:center;justify-content:center;width:32px;height:32px;min-width:0;min-height:32px;padding:2px;background:transparent;border-radius:6px;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;touch-action:pan-y; } +.mengsan-hud-status-shuying img { width:26px;height:26px;object-fit:contain;pointer-events:none; } +.mengsan-hud-status-shuying.is-pressed { border-color:#dcc18e;background:#ffffff12; } +.mengsan-hud-status-more-shuying::after { content:'\2022\2022\2022';letter-spacing:1px; } +.mengsan-hud-status-fallback-shuying::after { content:'';width:12px;height:12px;border:2px solid #dcc18e;transform:rotate(45deg); } +/* Equipment Spine effects live on this body canvas (native z-index 7). + Keep them above the portrait (25) and below action controls (30). + Inline elevations used by awakening animations still take precedence. */ +body.mengsan-hud-active-shuying>#decadeUI-canvas { z-index:28; } .mengsan-hud-hand-shuying { position:absolute!important;left:var(--mengsan-hud-left)!important;right:var(--mengsan-hud-right)!important;bottom:68px!important;top:auto!important;width:calc(100% - var(--mengsan-hud-left) - var(--mengsan-hud-right))!important;height:calc(var(--mengsan-hud-height,274px) - 126px)!important;z-index:24!important;background:none!important;border:0!important;transform:none!important;pointer-events:none; } .mengsan-hud-hand-shuying .handcards>.card { pointer-events:auto; } .mengsan-hud-mebg-shuying,.mengsan-hud-hand-shuying>.fakeme.avatar { display:none!important; } @@ -114,7 +119,6 @@ body.mengsan-hud-active-shuying #arena>.mengsan-hud-controls-shuying>.mengsan-hu body.mengsan-hud-active-shuying #arena .mengsan-hud-native-skills-shuying>div { width:100%!important;min-width:0!important;box-sizing:border-box!important;padding:8px 10px!important;min-height:40px!important;font:14px/20px "Microsoft YaHei",sans-serif!important;white-space:normal!important; } [data-mengsan-hud-layout="narrow"] .mengsan-hud-center-shuying { left:22px; } [data-mengsan-hud-layout="short"] .mengsan-hud-shuying .mengsan-hud-statuses-shuying { height:100%; } -[data-mengsan-hud-layout="narrow"] .mengsan-hud-shuying .mengsan-hud-statuses-shuying { grid-template-columns:1fr; } .mengsan-hud-prompt-shuying { box-sizing:border-box!important;height:auto!important;min-height:0!important;overflow:auto!important;padding:2px 6px!important;background:none!important;border:0!important;box-shadow:none!important;text-align:center!important;z-index:32!important;color:#f6edcb!important;text-shadow:0 1px 3px #000,0 0 6px #000!important; } body.mengsan-hud-active-shuying .mengsan-hud-card-intro-shuying { box-sizing:border-box!important;z-index:90!important;height:auto!important;min-height:0!important;overflow:auto!important;border-radius:14px!important;background:#202020f5!important;box-shadow:0 8px 24px #0007!important;color:#f4f4f4!important;transition:none!important; } body.mengsan-hud-active-shuying #window>.mengsan-hud-card-intro-shuying { z-index:90!important; } diff --git a/update.js b/update.js index 01092ba..09363a0 100644 --- a/update.js +++ b/update.js @@ -12,6 +12,7 @@ let sessionVerified = new Set(); let verificationContext = null; let verificationDirty = false; let fullVerification = false; +const progressLayers = new WeakMap(); function cacheContext() { if (lib.node?.fs && typeof window.__dirname == "string") return `node:${window.__dirname}`; @@ -265,12 +266,69 @@ function setBusy(shuYing, busy) { } function ensureProgressNode(shuYing) { - if (!shuYing.text.parentNode) { - document.body.appendChild(shuYing.text); + const node = shuYing.text; + Object.assign(node.style, { + position: "fixed", inset: "14px auto auto 50%", margin: "0", + maxWidth: "calc(100vw - 28px)", zIndex: "2147483647", + }); + // Native modal dialogs outrank every z-index. A manual popover is non-modal + // and joins their top layer without intercepting game input or focus. + if (typeof node.showPopover == "function") { + if (node.parentNode !== document.body) document.body.appendChild(node); + node.setAttribute("popover", "manual"); + if (node.matches(":popover-open")) node.hidePopover(); + node.showPopover(); + } else { + // Old WebViews can display progress inside the active modal's top + // layer without opening another modal or making the game inert. + let layer = progressLayers.get(node); + if (!layer) { + layer = { dialogs: [], observer: null }; + layer.sync = (records = []) => { + const initial = layer.dialogs.length === 0; + const dialogs = [...document.querySelectorAll('dialog[open]')] + .filter(dialog => { + try { return dialog.matches(':modal'); } + catch { return true; } + }); + layer.dialogs = layer.dialogs.filter(dialog => + dialogs.includes(dialog)); + for (const dialog of dialogs) { + if (!layer.dialogs.includes(dialog)) layer.dialogs.push(dialog); + } + if (initial) { + const focused = document.activeElement?.closest('dialog'); + if (dialogs.includes(focused)) { + layer.dialogs = layer.dialogs.filter(dialog => + dialog !== focused); + layer.dialogs.push(focused); + } + } + for (const record of records) { + if (record.type !== 'attributes' || + !dialogs.includes(record.target)) continue; + layer.dialogs = layer.dialogs.filter(dialog => + dialog !== record.target); + layer.dialogs.push(record.target); + } + const host = layer.dialogs[layer.dialogs.length - 1] || + document.body; + if (node.parentNode !== host) host.appendChild(node); + }; + layer.observer = new MutationObserver(layer.sync); + layer.observer.observe(document.body, { + childList: true, subtree: true, + attributes: true, attributeFilter: ['open'], + }); + progressLayers.set(node, layer); + } + layer.sync(); } } function removeProgressNode(shuYing) { + progressLayers.get(shuYing.text)?.observer.disconnect(); + progressLayers.delete(shuYing.text); if (shuYing.text.parentNode) { shuYing.text.parentNode.removeChild(shuYing.text); } @@ -356,6 +414,8 @@ function setProgress(shuYing, options = {}) { const now = Date.now(); if (!options.force && now - (shuYing._progressRendered || 0) < 120) return; shuYing._progressRendered = now; + // Re-promote after any Mengsan modal opened since the previous update. + ensureProgressNode(shuYing); const current = Math.max(0, Number(options.current) || 0); const total = Math.max(0, Number(options.total) || 0);