mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-30 03:30:03 +08:00
fixed # 修复状态列表异常的bug
This commit is contained in:
+1
-1
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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); });
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user