Files
noname-shuying/mengsan/ui/hand/index.js
T
2026-09-30 16:59:10 +08:00

201 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createHandAdapter } from "./adapter.js";
import { createHandOverlays } from "./overlays.js";
import { createHandDiagnostics } from "./diagnostics.js";
export async function mountHandUI(player, session, env) {
const { ui, document, window, cardCost, styleURL } = env;
const adapter = createHandAdapter(player, ui);
const overlays = createHandOverlays(document, cardCost);
const restores = [];
const diagnose = createHandDiagnostics(env.game, session.id);
const style = document.createElement("link");
style.rel = "stylesheet";
style.href = styleURL;
let disposed = false;
let observer, resizeObserver, pointer = null, nativeInput = false;
let finishStyleLoad = null, refreshTimer = null;
const touchScrollClasses = new Map();
let layoutEnabled = false, warned = false;
const log = (message, error) => {
diagnose(message, error);
env.log?.(`梦三手牌 UI:${message}`);
};
function ownClass(node, name) {
if (!node || node.classList.contains(name)) return;
node.classList.add(name);
restores.push(() => node.classList.remove(name));
}
function ownAttribute(node, name, value) {
const previous = node.getAttribute(name);
node.setAttribute(name, value);
restores.push(() => {
if (node.getAttribute(name) !== value) return;
if (previous === null) node.removeAttribute(name);
else node.setAttribute(name, previous);
});
}
function nativeLayout() {
if (ui.me !== adapter.root) return;
try { ui.updateh?.(true); ui.updatehl?.(); }
catch (error) { log("原手牌布局刷新失败", error); }
}
function setLayout(enabled) {
if (layoutEnabled === enabled) return;
layoutEnabled = enabled;
for (const zone of adapter.zones) {
zone.classList.toggle("mengsan-hand-layout-shuying", enabled);
}
for (const container of adapter.containers.filter(Boolean)) {
container.classList.toggle("mengsan-hand-scroll-shuying", enabled);
}
if (!enabled) nativeLayout();
}
function refresh() {
if (disposed) return;
if (!session.active || !adapter.zones.some(zone => zone.isConnected)) {
dispose();
return;
}
const supported = adapter.supported();
setLayout(supported && !nativeInput);
if (!supported && !warned) {
warned = true;
log("当前容器不支持独立排列,已保留原排列与费用/强化标记");
}
overlays.sync(adapter.cards());
}
function listen(target, type, listener, options) {
target.addEventListener(type, listener, options);
restores.push(() => target.removeEventListener(type, listener, options));
}
function beginInput(event) {
const card = event.target.closest?.(".card");
if (event.button !== 0 || pointer ||
!adapter.zones.includes(card?.parentNode)) {
return;
}
pointer = { id: event.pointerId, x: event.clientX, y: event.clientY,
touch: event.pointerType === "touch" };
}
function moveInput(event) {
if (!pointer || pointer.id !== event.pointerId || nativeInput) return;
if (Math.hypot(event.clientX - pointer.x, event.clientY - pointer.y) < 8) {
return;
}
if (pointer.touch && Math.abs(event.clientX - pointer.x) >
Math.abs(event.clientY - pointer.y)) return;
nativeInput = true;
setLayout(false);
}
function endInput(event) {
if (event?.pointerId != null && event.pointerId !== pointer?.id) return;
restoreTouchScroll();
pointer = null;
if (disposed || !nativeInput) return;
nativeInput = false;
window.clearTimeout(refreshTimer);
refreshTimer = window.setTimeout(refresh, 0);
}
function restoreTouchScroll() {
for (const [container, hadScroll] of touchScrollClasses) {
if (!hadScroll) container.classList.remove("scrollh");
}
touchScrollClasses.clear();
}
function scrollWheel(event) {
if (!layoutEnabled || event.ctrlKey || event.metaKey) return;
const container = event.currentTarget;
if (container.scrollWidth <= container.clientWidth) return;
const scale = event.deltaMode === 1 ? 16 :
event.deltaMode === 2 ? container.clientWidth : 1;
container.scrollLeft += (event.deltaX || event.deltaY) * scale;
event.preventDefault();
event.stopImmediatePropagation();
}
function allowTouchScroll(event) {
const container = event.currentTarget;
if (!layoutEnabled || container.scrollWidth <= container.clientWidth) {
return;
}
if (!touchScrollClasses.has(container)) {
touchScrollClasses.set(container,
container.classList.contains("scrollh"));
}
container.classList.add("scrollh");
}
function dispose() {
if (disposed) return;
disposed = true;
finishStyleLoad?.();
window.clearTimeout(refreshTimer);
restoreTouchScroll();
observer?.disconnect();
resizeObserver?.disconnect();
overlays.dispose();
setLayout(false);
for (const restore of restores.reverse()) restore();
style.remove();
pointer = null;
diagnose("已卸载手牌 UI");
}
session.ownResource({}, dispose);
try {
await new Promise((resolve, reject) => {
const timer = window.setTimeout(() => finish(
new Error("梦三手牌样式加载超时")
), 10000);
const finish = error => {
window.clearTimeout(timer);
finishStyleLoad = null;
style.onload = style.onerror = null;
if (error) reject(error);
else resolve();
};
finishStyleLoad = finish;
style.onload = () => finish();
style.onerror = () => finish(new Error("梦三手牌样式加载失败"));
document.head.appendChild(style);
});
if (!session.active || disposed) { dispose(); return { refresh, dispose }; }
for (const zone of adapter.zones) {
ownClass(zone, "mengsan-hand-zone-shuying");
}
for (const container of adapter.containers.filter(Boolean)) {
if (!container.hasAttribute("tabindex")) {
ownAttribute(container, "tabindex", "0");
}
ownAttribute(container, "aria-label", "梦三手牌,可横向滚动");
listen(container, "wheel", scrollWheel,
{ capture: true, passive: false });
listen(container, "touchmove", allowTouchScroll, true);
listen(container, "touchend", () => restoreTouchScroll(), true);
listen(container, "touchcancel", () => restoreTouchScroll(), true);
}
observer = new window.MutationObserver(refresh);
for (const zone of adapter.zones) {
observer.observe(zone, { childList: true });
}
if (adapter.root?.parentNode) {
observer.observe(adapter.root.parentNode, { childList: true });
}
if (window.ResizeObserver && adapter.root) {
resizeObserver = new window.ResizeObserver(refresh);
resizeObserver.observe(adapter.root);
}
if (window.PointerEvent && adapter.root) {
listen(adapter.root, "pointerdown", beginInput, true);
listen(window, "pointermove", moveInput, true);
listen(window, "pointerup", endInput, true);
listen(window, "pointercancel", endInput, true);
listen(window, "blur", () => endInput());
}
refresh();
diagnose(`已挂载手牌 UI,独立排列=${layoutEnabled}`);
return { refresh, dispose };
} catch (error) {
log("必需模块挂载失败,停止进入战斗", error);
dispose();
throw error;
}
}