From d5715c9f5a7031916e60ea00d51d65ce96e02078 Mon Sep 17 00:00:00 2001 From: Art_Sakura <1754798088@qq.com> Date: Thu, 29 Oct 2026 15:21:11 +0800 Subject: [PATCH] =?UTF-8?q?fixed=20#=20=E4=BF=AE=E5=A4=8D=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=88=97=E8=A1=A8=E5=BC=82=E5=B8=B8=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension.js | 2 +- mengsan/tools/test-battle-hud.cjs | 13 +++++++++-- mengsan/ui/gm-manager.js | 13 ++++++++++- mengsan/ui/hand/input.js | 38 +++++++++++++++++++++++++----- mengsan/ui/hand/style.css | 6 ++--- mengsan/ui/hud/index.js | 39 ++++++++++++++++++++----------- mengsan/ui/relic-strip.js | 5 ++-- mengsan/ui/relics.js | 21 +++++++++++------ mengsan/ui/style.css | 7 ++++++ 9 files changed, 107 insertions(+), 37 deletions(-) diff --git a/extension.js b/extension.js index 840cd89..547b953 100644 --- a/extension.js +++ b/extension.js @@ -6,7 +6,7 @@ import { configureDiagnostics } from "./diagnostics/index.js"; export const type = "extension"; export let shuYing = new Object(); -const shuYingLocalVersion = "2.0.2.3-rc.7"; +const shuYingLocalVersion = "2.0.2.3-rc.8"; shuYing.name = "术樱包"; shuYing.editable = false; diff --git a/mengsan/tools/test-battle-hud.cjs b/mengsan/tools/test-battle-hud.cjs index f3e7240..cf176c0 100644 --- a/mengsan/tools/test-battle-hud.cjs +++ b/mengsan/tools/test-battle-hud.cjs @@ -322,12 +322,21 @@ const server = http.createServer((req, res) => { hud.refresh(); },prefix); await holdStatus(page,page.getByRole('button',{ - name:'长按查看全部状态',exact:true})); + 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().tap(); - assert.equal(await page.locator('dialog.mengsan-hud-dialog-shuying').count(),0,'short tap does not open status details');checks++; + assert.equal(await page.locator('dialog.mengsan-hud-dialog-shuying').count(),1,'short tap opens the status list');checks++; + const statusList = await page.locator('.mengsan-hud-detail-body-shuying').evaluate(body => ({ + rows:body.querySelectorAll('details').length, + position:getComputedStyle(body).position, + minHeight:Math.min(...[...body.querySelectorAll('summary')].map(row=>row.getBoundingClientRect().height)), + height:body.getBoundingClientRect().height, + scrollable:body.scrollHeight>body.clientHeight, + })); + assert.ok(statusList.rows>20&&statusList.position!=='absolute'&&statusList.minHeight>=48&&statusList.height>0&&statusList.scrollable,'status list keeps all entries in normal flow with large tap targets: '+JSON.stringify(statusList));checks++; + await page.getByRole('button',{name:'关闭',exact:true}).click(); 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(); diff --git a/mengsan/ui/gm-manager.js b/mengsan/ui/gm-manager.js index e28af91..20c53e9 100644 --- a/mengsan/ui/gm-manager.js +++ b/mengsan/ui/gm-manager.js @@ -8,6 +8,7 @@ import { SLIMED_NAME, createSlimedData } from "../cards/slimed-card.js"; import { WOUND_NAME, createWoundData } from "../cards/wound-card.js"; import { INFECTION_NAME, createInfectionData } from "../cards/infection-card.js"; import { getDiagnostics } from "../../diagnostics/index.js"; +import { _status } from "../../../../noname.js"; const statusFactories = { [DAZED_NAME]: createDazedData, @@ -132,7 +133,17 @@ export function mountGMManager(session, owner, piles, battle, resources, game) { diagnostics.info("temporaryCard.added", { cardName: data.name, cardId: id, }); - try { game.check(); } + try { + // check() reuses the choices from before directgain. + // Invalidate them without clearing the user's selection. + const event = _status.event; + if (event?.player === owner) { + delete event._cardChoice; + delete event._targetChoice; + delete event._skillChoice; + } + game.check(); + } catch (error) { game.log("GM 临时牌已入手,选择状态刷新失败", error.message); diff --git a/mengsan/ui/hand/input.js b/mengsan/ui/hand/input.js index 80ed210..146187c 100644 --- a/mengsan/ui/hand/input.js +++ b/mengsan/ui/hand/input.js @@ -69,7 +69,8 @@ export function createHandInput(player, session, adapter, fan, env, refresh, log event.name !== "chooseToUse" || event.player !== player || !event.isMine?.() || event.skill || event.complexSelect || event.custom?.replace?.card || event.custom?.replace?.target || event.custom?.replace?.confirm || !env.isActiveCardUse(event, player) || !env.canPayCard?.(player, card) || - ui.selected.cards.length || ui.selected.targets.length || ui.selected.buttons.length || + (ui.selected.cards.length && (ui.selected.cards.length !== 1 || ui.selected.cards[0] !== card)) || + ui.selected.targets.length || ui.selected.buttons.length || !card.classList.contains("selectable") || card.classList.contains("noclick") || status.mousedragging || status.dragged || status.paused2) return false; const cards = get.select(event.selectCard); @@ -83,19 +84,22 @@ export function createHandInput(player, session, adapter, fan, env, refresh, log aim.setAttribute("aria-hidden", "true"); aim.setAttribute("preserveAspectRatio", "none"); const path = document.createElementNS(aim.namespaceURI, "path"); - aim.appendChild(path); document.body.appendChild(aim); + // Keep the SVG in the same zoom/transform space as the real hand. + // Android WebView can paint a body-fixed SVG in a different viewport. + aim.appendChild(path); ui.arena.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}`); + const width = ui.arena.clientWidth, height = ui.arena.clientHeight; + aim.setAttribute("viewBox", `0 0 ${width} ${height}`); // 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 scaleX = width / bounds.width; + const scaleY = height / bounds.height; const start = { x: (x - bounds.left) * scaleX, y: (y - bounds.top) * scaleY }; const end = { x: (point.x - bounds.left) * scaleX, @@ -163,7 +167,7 @@ export function createHandInput(player, session, adapter, fan, env, refresh, log if (!state.moved && Math.hypot(point.x - state.x, point.y - state.y) < 9) return; if (!state.moved) { state.moved = true; - engineCall(() => ui.click.card.call(state.card)); + if (!ui.selected.cards.includes(state.card)) engineCall(() => ui.click.card.call(state.card)); state.selected = ui.selected.cards.includes(state.card); const targets = range(state.event); if (!state.selected || !live(state) || !targets) { cleanup(true); return; } @@ -235,6 +239,28 @@ export function createHandInput(player, session, adapter, fan, env, refresh, log for (const type of ["mousedown", "mousemove", "mouseup", "click", "touchstart", "touchmove", "touchend"]) { listen(window, type, compatibility, { capture: true, passive: false }); } + } else if (adapter.root && status && get) { + // Older Android WebViews expose TouchEvent but no PointerEvent. + // Route them through the same selection and drawing code. + const touchEvent = (event, touch, type) => ({ + button: 0, isPrimary: true, pointerId: touch.identifier, + target: event.target, clientX: touch.clientX, clientY: touch.clientY, + type, preventDefault: () => event.preventDefault(), + stopImmediatePropagation: () => event.stopImmediatePropagation(), + }); + const options = { capture: true, passive: false }; + listen(window, "touchstart", event => { + if (event.touches.length !== 1) { cancel(); return; } + start(touchEvent(event, event.touches[0], "pointerdown")); + }, options); + for (const [type, handler] of [["touchmove", move], ["touchend", end], ["touchcancel", end]]) { + listen(window, type, event => { + const touch = Array.from(event.changedTouches).find(item => item.identifier === gesture?.id); + if (touch) handler(touchEvent(event, touch, type === "touchcancel" ? "pointercancel" : type)); + }, options); + } + listen(window, "click", compatibility, options); + listen(window, "blur", cancel); } listen(adapter.root, "pointerover", hover); listen(adapter.root, "pointerleave", () => { if (!gesture) fan.setHover(null); }); diff --git a/mengsan/ui/hand/style.css b/mengsan/ui/hand/style.css index 4c783f7..508068b 100644 --- a/mengsan/ui/hand/style.css +++ b/mengsan/ui/hand/style.css @@ -77,10 +77,10 @@ transform-origin: 50% 50% !important; } .mengsan-hand-aim-shuying { - position: fixed; + position: absolute; inset: 0; - width: 100vw; - height: 100vh; + width: 100%; + height: 100%; z-index: 1000; pointer-events: none; overflow: visible; diff --git a/mengsan/ui/hud/index.js b/mengsan/ui/hud/index.js index e5f456f..6fe99d5 100644 --- a/mengsan/ui/hud/index.js +++ b/mengsan/ui/hud/index.js @@ -67,29 +67,40 @@ export function mountBattleHUD(player, battle, env) { }; const refreshDetails = () => { if (!dialog || !detailBody) return; - const entries = collectCombatStatuses(detailTarget, battle) - .filter(entry => detailId === null || entry.id === detailId); + const entries = collectCombatStatuses(detailTarget, battle); const signature = JSON.stringify(entries); if (signature === detailSignature) return; + const expanded = detailSignature === null ? new Set(detailId ? [detailId] : []) : + new Set([...detailBody.querySelectorAll('details[open]')].map(row => row.dataset.statusId)); detailSignature = signature; detailBody.replaceChildren(); + if (detailId && !entries.some(entry => entry.id === detailId)) { + create('p', '', detailBody, '该状态已失效。'); + } for (const entry of entries) { - const row = create('section', 'mengsan-hud-detail-row-shuying', + const row = create('details', 'mengsan-info-row-shuying', detailBody); - create('strong', '', row, `${entry.name}${entry.value == null ? '' : ` ${entry.value}${entry.unit ? ` ${entry.unit}` : ''}`}`); + row.dataset.statusId = entry.id; + row.open = expanded.has(entry.id); + const summary = create('summary', '', row); + if (entry.icon) { + const icon = create('img', 'mengsan-relic-icon-shuying', summary); + icon.src = `${assetBase}/assets/status/${entry.icon}`; icon.alt = ''; + } + create('strong', '', summary, `${entry.name}${entry.value == null ? '' : ` ${entry.value}${entry.unit ? ` ${entry.unit}` : ''}`}`); create('p', '', row, entry.detail); } - if (!entries.length) create('p', '', detailBody, - detailId === null ? '暂无已登记状态。' : '该状态已失效。'); + if (!entries.length && !detailId) create('p', '', detailBody, '暂无已登记状态。'); }; 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); + dialog = create('dialog', 'mengsan-hud-dialog-shuying mengsan-deck-dialog-shuying mengsan-relic-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 header = create('header', 'ms-deck-header', dialog); + create('h2', '', header, `${nameOf(target)} · 当前状态`); + const close = create('button', 'ms-deck-button', header, '关闭'); close.addEventListener('click', closeDialog); detailTarget = target; detailId = id; - detailBody = create('div', 'mengsan-hud-detail-body-shuying', dialog); + detailBody = create('div', 'mengsan-hud-detail-body-shuying mengsan-relic-list-shuying', dialog); refreshDetails(); // Unknown/native marks retain their engine intro and are never hidden by the HUD. dialog.addEventListener('cancel', event => { event.preventDefault(); closeDialog(); }); @@ -149,8 +160,8 @@ export function mountBattleHUD(player, battle, env) { statuses.addEventListener('click', event => { event.preventDefault(); const button = statusButton(event); - // Pointer clicks require a hold; keyboard activation stays accessible. - if (button && event.detail === 0) { + // Every entry opens the whole list; precise long presses are optional. + if (button && !dialog) { openDetails(player, button.dataset.statusId || null); } }); @@ -173,7 +184,7 @@ export function mountBattleHUD(player, battle, env) { 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-label', `${entry.name} ${entry.value ?? ''} ${entry.unit},点击查看状态列表`); button.setAttribute('aria-haspopup', 'dialog'); if (button.dataset.icon !== (entry.icon || '')) { button.dataset.icon = entry.icon || ''; @@ -195,7 +206,7 @@ export function mountBattleHUD(player, battle, env) { const more = existing.get(null) || create('button', 'mengsan-hud-status-shuying mengsan-hud-status-more-shuying', null); - more.setAttribute('aria-label', '长按查看全部状态'); + more.setAttribute('aria-label', '点击查看全部状态'); more.setAttribute('aria-haspopup', 'dialog'); wantedButtons.push(more); } diff --git a/mengsan/ui/relic-strip.js b/mengsan/ui/relic-strip.js index 514076a..9dadf3c 100644 --- a/mengsan/ui/relic-strip.js +++ b/mengsan/ui/relic-strip.js @@ -2,7 +2,7 @@ export function mountRelicStrip(root, getRelics, open, createIcon, document, window) { const strip = document.createElement("div"); strip.className = "mengsan-hand-relics-shuying"; - strip.setAttribute("aria-label", "持有遗物,长按查看描述"); + strip.setAttribute("aria-label", "持有遗物,点击查看列表"); const before = root.style.getPropertyValue("--mengsan-relic-space"); const priority = root.style.getPropertyPriority("--mengsan-relic-space"); let pending = null, timer = null, signature = null, disposed = false; @@ -38,7 +38,6 @@ export function mountRelicStrip(root, getRelics, open, createIcon, const click = event => { stop(event); event.preventDefault(); - if (event.detail !== 0) return; const button = event.target.closest?.("button"); const relic = getRelics().find(item => item.id === button?.dataset.relicId); if (relic) open(relic); @@ -71,7 +70,7 @@ export function mountRelicStrip(root, getRelics, open, createIcon, const button = document.createElement("button"); button.type = "button"; button.dataset.relicId = relic.id; - button.setAttribute("aria-label", `${relic.name},长按查看描述`); + button.setAttribute("aria-label", `${relic.name},点击查看遗物列表`); button.setAttribute("aria-haspopup", "dialog"); const fallback = document.createElement("span"); fallback.textContent = relic.name; diff --git a/mengsan/ui/relics.js b/mengsan/ui/relics.js index 2b03663..1b7a15b 100644 --- a/mengsan/ui/relics.js +++ b/mengsan/ui/relics.js @@ -26,22 +26,29 @@ export function mountRelics(session, run, battleRelics, { stripRoot = ui.me } = button.textContent = `遗物 ${relics.length}`; strip?.refresh(); if (!list) return; + const expanded = new Set([...list.querySelectorAll('details[open]')].map(row => row.dataset.relicId)); + if (!list.children.length && selected) expanded.add(selected); list.replaceChildren(); if (!relics.length) { const empty = document.createElement("p"); empty.textContent = "本次征程尚未获得遗物。"; list.appendChild(empty); } - for (const relic of selected ? relics.filter(r => r.id === selected) : relics) { - const row = document.createElement("section"); - const icon=createRelicIcon(relic.image);if(icon)row.appendChild(icon); - const name = document.createElement("h3"); + for (const relic of relics) { + const row = document.createElement("details"); + row.className = "mengsan-info-row-shuying"; + row.dataset.relicId = relic.id; + row.open = expanded.has(relic.id); + const summary = document.createElement("summary"); + const icon=createRelicIcon(relic.image);if(icon)summary.appendChild(icon); + const name = document.createElement("strong"); name.textContent = relic.name; + summary.appendChild(name); const description = document.createElement("p"); description.textContent = relic.description; const status = document.createElement("small"); status.textContent = `${relic.pool} · ${relic.tier}${relic.ancient ? ` · ${relic.ancient}` : ""} · ${battleRelics.status(relic)}`; - row.append(name, description, status); + row.append(summary, description, status); list.appendChild(row); } }; @@ -53,11 +60,11 @@ export function mountRelics(session, run, battleRelics, { stripRoot = ui.me } = const currentDialog = dialog; dialog.className = "mengsan-deck-dialog-shuying " + "mengsan-relic-dialog-shuying"; - dialog.setAttribute("aria-label", relic?.name || "遗物(道具)"); + dialog.setAttribute("aria-label", "遗物(道具)"); const header = document.createElement("header"); header.className = "ms-deck-header"; const title = document.createElement("h2"); - title.textContent = relic?.name || "遗物(道具)"; + title.textContent = "遗物(道具)"; const dismiss = document.createElement("button"); dismiss.type = "button"; dismiss.className = "ms-deck-button"; diff --git a/mengsan/ui/style.css b/mengsan/ui/style.css index 13f37dd..4776be4 100644 --- a/mengsan/ui/style.css +++ b/mengsan/ui/style.css @@ -1434,6 +1434,13 @@ body.mengsan-battle-ui-shuying :is( } .mengsan-relic-dialog-shuying { max-width: 620px; } .mengsan-relic-list-shuying { overflow-y: auto; padding: 12px 0; } +.mengsan-deck-dialog-shuying .mengsan-relic-list-shuying { position:relative;display:block;flex:1;min-height:0;overscroll-behavior:contain;touch-action:pan-y; } +.mengsan-info-row-shuying { display:block;border-bottom:1px solid #b9975d55;padding:8px 0; } +.mengsan-info-row-shuying>summary { display:list-item;min-height:48px;padding:8px 10px;cursor:pointer;touch-action:manipulation;color:#ffe0a1;line-height:32px; } +.mengsan-info-row-shuying>summary:focus-visible { outline:2px solid #ffe0a1;outline-offset:-2px; } +.mengsan-info-row-shuying>summary>.mengsan-relic-icon-shuying { width:32px;height:32px;vertical-align:middle;margin-right:12px; } +.mengsan-info-row-shuying>summary>strong { display:inline;vertical-align:middle;overflow-wrap:anywhere; } +.mengsan-info-row-shuying>p,.mengsan-info-row-shuying>small { display:block;padding:8px 12px;white-space:pre-line;overflow-wrap:anywhere; } .mengsan-relic-list-shuying section { padding: 14px 0; border-bottom: 1px solid #b9975d55;