fixed # 修复状态列表异常的bug

This commit is contained in:
2026-10-29 15:21:11 +08:00
parent 4036041819
commit d5715c9f5a
9 changed files with 107 additions and 37 deletions
+1 -1
View File
@@ -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;
+11 -2
View File
@@ -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();
+12 -1
View File
@@ -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);
+32 -6
View File
@@ -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); });
+3 -3
View File
@@ -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;
+25 -14
View File
@@ -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);
}
+2 -3
View File
@@ -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;
+14 -7
View File
@@ -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";
+7
View File
@@ -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;