fixed # 补全梦三怪物、卡牌

fixed # 修复天书乱斗中,过关技能不会重置次数的问题
This commit is contained in:
2026-10-03 04:42:19 +08:00
parent b005a7ff15
commit c448da39c2
131 changed files with 5021 additions and 246 deletions
+89
View File
@@ -0,0 +1,89 @@
// 只覆盖命名空间内的 CSS 变量,不移动或复制真实卡牌。
export function fanSlots(count, width, cardWidth = 112, cardHeight = 156) {
if (!Number.isSafeInteger(count) || count < 0 || !Number.isFinite(width) || width <= 0) return [];
const step = count > 1 ? Math.min(cardWidth * 0.68,
Math.max(0, width - cardWidth - 32) / (count - 1)) : 0;
const half = (count - 1) / 2;
const angleStep = count > 1 ? Math.min(4, 13 / half) : 0;
return Array.from({ length: count }, (_, index) => {
const offset = index - half;
return { x: width / 2 + offset * step - cardWidth / 2,
y: -cardHeight + 28 + (half ? Math.pow(offset / half, 2) * 20 : 0),
angle: offset * angleStep, z: index + 1 };
});
}
export function createHandFan(adapter, env) {
const { window } = env;
const owned = new Map();
let enabled = false, hover = null, dragging = null;
function variable(card, key, value) {
let record = owned.get(card);
if (!record) { record = new Map(); owned.set(card, record); }
if (!record.has(key)) record.set(key, {
before: card.style.getPropertyValue(key), priority: card.style.getPropertyPriority(key), last: null,
});
const entry = record.get(key);
entry.last = value;
if (card.style.getPropertyValue(key) !== value) card.style.setProperty(key, value);
}
function release(card) {
for (const name of ["mengsan-hand-hover-shuying", "mengsan-hand-drag-shuying", "mengsan-hand-free-drag-shuying"]) card.classList.remove(name);
for (const [key, record] of owned.get(card) || []) {
if (card.style.getPropertyValue(key) !== record.last) continue;
if (record.before) card.style.setProperty(key, record.before, record.priority);
else card.style.removeProperty(key);
}
owned.delete(card);
}
function setHover(card) {
if (hover === card) return;
hover?.classList.remove("mengsan-hand-hover-shuying");
hover = enabled && !dragging ? card : null;
hover?.classList.add("mengsan-hand-hover-shuying");
}
function sync(active) {
enabled = active;
const cards = adapter.cards().filter(card => !card.classList.contains("display-none"));
const current = new Set(cards);
for (const card of owned.keys()) if (!active || !current.has(card)) release(card);
if (!active) { setHover(null); return; }
const small = window.innerWidth <= 640 || window.innerHeight <= 500;
const width = adapter.containers[0]?.clientWidth || adapter.root.clientWidth;
const slots = fanSlots(cards.length, width, small ? 82 : 112, small ? 114 : 156);
cards.forEach((card, index) => {
const slot = slots[index];
if (!slot) return;
variable(card, "--mengsan-fan-x", `${slot.x}px`);
variable(card, "--mengsan-fan-y", `${slot.y}px`);
variable(card, "--mengsan-fan-angle", `${slot.angle}deg`);
variable(card, "--mengsan-fan-z", `${slot.z}`);
});
if (hover && !current.has(hover)) setHover(null);
}
function drag(card, point, targeted) {
if (dragging && dragging !== card) stopDrag();
setHover(null);
dragging = card;
card.classList.add("mengsan-hand-drag-shuying");
card.classList.toggle("mengsan-hand-free-drag-shuying", !targeted);
const zone = card.parentNode;
const rect = zone.getBoundingClientRect();
// CSS 像素相对于真实父容器:兼容 documentZoom/缩放,不能直接拿屏幕坐标当布局坐标。
const scaleX = rect.width / zone.offsetWidth || 1;
const scaleY = rect.height / zone.offsetHeight || scaleX;
const rootRect = adapter.root.getBoundingClientRect();
const small = window.innerWidth <= 640 || window.innerHeight <= 500;
const width = small ? 82 : 112, height = small ? 114 : 156;
const x = targeted ? rootRect.left + rootRect.width / 2 : point.x;
const y = targeted ? rootRect.bottom - height * scaleY / 2 - 6 : point.y;
variable(card, "--mengsan-drag-x", `${(x - rect.left) / scaleX - width / 2}px`);
variable(card, "--mengsan-drag-y", `${(y - rect.top) / scaleY - height / 2}px`);
}
function stopDrag() {
dragging?.classList.remove("mengsan-hand-drag-shuying", "mengsan-hand-free-drag-shuying");
dragging = null;
}
return { sync, setHover, drag, stopDrag,
dispose() { stopDrag(); setHover(null); for (const card of owned.keys()) release(card); } };
}
+36 -6
View File
@@ -1,11 +1,15 @@
import { createHandAdapter } from "./adapter.js";
import { createHandOverlays } from "./overlays.js";
import { createHandDiagnostics } from "./diagnostics.js";
import { createHandFan } from "./fan.js";
import { createHandInput } from "./input.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 overlays = createHandOverlays(document, card => cardCost(card, player));
const fan = createHandFan(adapter, env);
let input = null;
const restores = [];
const diagnose = createHandDiagnostics(env.game, session.id);
const style = document.createElement("link");
@@ -58,6 +62,8 @@ export async function mountHandUI(player, session, env) {
}
const supported = adapter.supported();
setLayout(supported && !nativeInput);
fan.sync(supported && !nativeInput);
input?.reconcile();
if (!supported && !warned) {
warned = true;
log("当前容器不支持独立排列,已保留原排列与费用/强化标记");
@@ -68,17 +74,35 @@ export async function mountHandUI(player, session, env) {
target.addEventListener(type, listener, options);
restores.push(() => target.removeEventListener(type, listener, options));
}
// 不可使用的真实手牌不能触发旧手势的横排回退;选中牌保留原生交互。
function canBeginNativeInput(card) {
if (!adapter.zones.includes(card?.parentNode) ||
card.classList.contains("noclick") ||
(!card.classList.contains("selectable") &&
!card.classList.contains("selected"))) return false;
const current = env._status?.event;
return !(current?.name === "chooseToUse" && current.player === player &&
env.isActiveCardUse?.(current, player) &&
env.canPayCard?.(player, card) === false);
}
function beginInput(event) {
if (input?.owns()) return;
const card = event.target.closest?.(".card");
if (event.button !== 0 || pointer ||
!adapter.zones.includes(card?.parentNode)) {
!canBeginNativeInput(card)) {
return;
}
pointer = { id: event.pointerId, x: event.clientX, y: event.clientY,
pointer = { card, 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 (input?.owns()) return;
if (!pointer || pointer.id !== event.pointerId) return;
if (!canBeginNativeInput(pointer.card)) { endInput(event); return; }
if (nativeInput) return;
// 未接管的上拖(包括响应牌或已选牌)不属于手牌排序,不退回横排。
const dx = event.clientX - pointer.x, dy = event.clientY - pointer.y;
if (dy < 0 && Math.abs(dy) >= Math.abs(dx)) return;
if (Math.hypot(event.clientX - pointer.x, event.clientY - pointer.y) < 8) {
return;
}
@@ -131,6 +155,8 @@ export async function mountHandUI(player, session, env) {
restoreTouchScroll();
observer?.disconnect();
resizeObserver?.disconnect();
input?.dispose();
fan.dispose();
overlays.dispose();
setLayout(false);
for (const restore of restores.reverse()) restore();
@@ -157,6 +183,8 @@ export async function mountHandUI(player, session, env) {
document.head.appendChild(style);
});
if (!session.active || disposed) { dispose(); return { refresh, dispose }; }
ownClass(adapter.root, "mengsan-hand-root-shuying");
ownClass(document.body, "mengsan-hand-ui-shuying");
for (const zone of adapter.zones) {
ownClass(zone, "mengsan-hand-zone-shuying");
}
@@ -164,7 +192,7 @@ export async function mountHandUI(player, session, env) {
if (!container.hasAttribute("tabindex")) {
ownAttribute(container, "tabindex", "0");
}
ownAttribute(container, "aria-label", "梦三手牌,可横向滚动");
ownAttribute(container, "aria-label", "梦三扇形手牌,悬停查看;上拖出牌,拖向目标后松手确认");
listen(container, "wheel", scrollWheel,
{ capture: true, passive: false });
listen(container, "touchmove", allowTouchScroll, true);
@@ -173,8 +201,10 @@ export async function mountHandUI(player, session, env) {
}
observer = new window.MutationObserver(refresh);
for (const zone of adapter.zones) {
observer.observe(zone, { childList: true });
observer.observe(zone, { childList: true, subtree: true,
attributes: true, attributeFilter: ["class"] });
}
input = createHandInput(player, session, adapter, fan, env, refresh, log);
if (adapter.root?.parentNode) {
observer.observe(adapter.root.parentNode, { childList: true });
}
+255
View File
@@ -0,0 +1,255 @@
// 梦三局部输入适配:使用真实 ui.click/card/target 和 game.check 提交,不自行 useCard。
export function createHandInput(player, session, adapter, fan, env, refresh, log) {
const { ui, game, get, _status: status, document, window } = env;
const listeners = [];
const maskedControls = new Set(), submittedControls = new Set();
const maskClass = "mengsan-hand-controls-hidden-shuying";
function maskControls(hidden) {
const wanted = hidden ? new Set([ui.control, ui.confirm, gesture?.event.endButton]) : new Set();
for (const node of maskedControls) {
if (wanted.has(node)) continue;
node.classList.remove(maskClass); maskedControls.delete(node);
}
// ui.control 是不同 UI 共同的控件入口;独立确认/结束按钮也可能挂在外部。
for (const node of wanted) {
if (!node?.classList || node.classList.contains(maskClass)) continue;
node.classList.add(maskClass); maskedControls.add(node);
}
}
function closeSubmittedControls(state) {
for (const node of new Set([ui.confirm, state.event.endButton])) {
if (typeof node?.close !== "function") continue;
if (node.classList && !node.classList.contains("mengsan-hand-submitted-control-shuying")) {
node.classList.add("mengsan-hand-submitted-control-shuying"); submittedControls.add(node);
}
// 与本体 windowmouseup 的提交路径一致:关闭旧确认后再调用 ui.click.ok。
node.close();
}
}
let gesture = null, suppressed = null, aim = null, frame = null, lastPoint = null;
const listen = (node, type, handler, options = true) => {
const safeHandler = event => {
try { handler(event); }
catch (error) {
log("手牌局部输入异常,取消本次拖动", error);
try { cleanup(true); }
catch (cause) { log("取消输入时引擎异常", cause); }
}
};
node.addEventListener(type, safeHandler, options);
listeners.push(() => node.removeEventListener(type, safeHandler, options));
};
const swallow = event => { event.preventDefault(); event.stopImmediatePropagation(); };
const live = state => session.active && adapter.supported() && state?.card.isConnected &&
adapter.zones.includes(state.card.parentNode) && status.event === state.event &&
!state.event.skill && state.event.isMine?.() && player.isAlive?.() !== false;
// 引擎 checkEnd 的自动确认必须在选牌/试选目标期间禁用;仅在本次同步调用内改标志。
function engineCall(callback) {
const previous = {};
for (const key of ["touchnocheck", "clicked", "dragged"]) previous[key] = { own: Object.hasOwn(status, key), value: status[key] };
status.touchnocheck = true; status.clicked = false; status.dragged = false;
try { return callback(); }
finally {
for (const [key, old] of Object.entries(previous)) {
if (old.own) status[key] = old.value;
else delete status[key];
}
}
}
function range(event) {
const value = get.select(event.selectTarget);
if (!Array.isArray(value) || value.length < 2 || value.some(n => !Number.isFinite(n))) return null;
return value;
}
function eligible(card) {
const event = status?.event;
if (!event || typeof ui.click?.card !== "function" || typeof ui.click?.ok !== "function" ||
typeof get?.select !== "function" || typeof env.isActiveCardUse !== "function" ||
!session.active || !adapter.supported() || ui.intro || game.online ||
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 ||
!card.classList.contains("selectable") || card.classList.contains("noclick") ||
status.mousedragging || status.dragged || status.paused2) return false;
const cards = get.select(event.selectCard);
return cards?.[0] === 1 && cards?.[1] === 1;
}
function removeAim() { aim?.remove(); aim = null; }
function showAim(card, point) {
if (!aim) {
aim = document.createElementNS("http://www.w3.org/2000/svg", "svg");
aim.classList.add("mengsan-hand-aim-shuying");
aim.setAttribute("aria-hidden", "true");
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);
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}`);
aim.firstElementChild.setAttribute("vector-effect", "non-scaling-stroke");
}
function clearSelection(state) {
if (!live(state) || !state.selected) return;
engineCall(() => { game.uncheck(); game.check(); });
}
function cleanup(cancel = false) {
const state = gesture;
gesture = null;
if (frame != null) window.cancelAnimationFrame(frame);
frame = null; lastPoint = null;
removeAim(); fan.stopDrag();
maskControls(false);
if (cancel && state) clearSelection(state);
if (state) suppressed = { card: state.card, until: Date.now() + 450,
x: state.endX ?? state.x, y: state.endY ?? state.y };
refresh();
}
function start(event) {
if (event.button !== 0 || !event.isPrimary || gesture) return;
const card = event.target.closest?.(".card");
if (!card || !adapter.zones.includes(card.parentNode) || !eligible(card)) return;
// 在 window 捕获,早于十周年的手牌排序和本体的 windowmousedown;不改全局处理器。
gesture = { card, event: status.event, id: event.pointerId,
x: event.clientX, y: event.clientY, moved: false, selected: false, targeted: false, manual: false };
swallow(event);
}
function targetAt(point) {
const node = document.elementFromPoint(point.x, point.y)?.closest?.(".player");
return game.players?.includes(node) ? node : null;
}
function chooseTarget(state, point) {
if (!state.targeted || state.manual) return;
const target = targetAt(point);
for (const previous of ui.selected.targets.slice()) {
if (previous !== target) engineCall(() => ui.click.target.call(previous));
}
if (target?.classList.contains("selectable") && !ui.selected.targets.includes(target)) {
engineCall(() => ui.click.target.call(target));
}
}
function paint() {
frame = null;
const state = gesture, point = lastPoint;
if (!state || !point || !state.moved) return;
if (!live(state) || !env.canPayCard(player, state.card)) { cleanup(true); return; }
fan.drag(state.card, point, state.targeted);
if (state.targeted) showAim(state.card, point);
chooseTarget(state, point);
maskControls(!state.manual);
}
function move(event) {
const state = gesture;
if (!state || event.pointerId !== state.id) return;
swallow(event);
if (!live(state)) { cleanup(true); return; }
const point = { x: event.clientX, y: event.clientY };
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));
state.selected = ui.selected.cards.includes(state.card);
const targets = range(state.event);
if (!state.selected || !live(state) || !targets) { cleanup(true); return; }
state.targeted = targets[1] > 0;
// 多目标保留选牌和原生确认,不擅自把一条线当作已选全部目标。
state.manual = state.targeted && (targets[0] !== 1 || targets[1] !== 1);
}
lastPoint = point;
if (frame == null) frame = window.requestAnimationFrame(() => {
try { paint(); }
catch (error) { log("手牌拖动绘制失败", error); cleanup(true); }
});
}
function end(event) {
const state = gesture;
if (!state || state.id !== event.pointerId) return;
swallow(event);
state.endX = event.clientX; state.endY = event.clientY;
if (!live(state) || event.type === "pointercancel") { cleanup(true); return; }
if (!state.moved) {
cleanup();
// 短点击保持原生选牌/自动确认行为,不强制进入拖动。
status.clicked = false;
ui.click.card.call(state.card);
return;
}
lastPoint = { x: event.clientX, y: event.clientY };
paint();
if (!gesture) return;
const targets = range(state.event);
if (targets && targets[1] > 0 && (targets[0] !== 1 || targets[1] !== 1)) state.manual = true;
const ready = env.canPayCard(player, state.card) && ui.selected.cards.length === 1 &&
ui.selected.cards[0] === state.card && targets &&
(!state.targeted ? state.y - event.clientY >= 60 &&
event.clientY < adapter.root.getBoundingClientRect().top :
!state.manual && targets[0] === 1 && targets[1] === 1 &&
targetAt(lastPoint) === ui.selected.targets[0] && ui.selected.targets.length === 1);
if (ready && engineCall(() => game.check()) && live(state)) {
closeSubmittedControls(state);
cleanup();
ui.click.ok();
} else {
cleanup(!state.manual);
}
}
function compatibility(event) {
if (gesture || (event.type === "click" && suppressed && Date.now() < suppressed.until &&
(suppressed.card.contains(event.target) || Math.hypot(event.clientX - suppressed.x, event.clientY - suppressed.y) < 3))) swallow(event);
}
function hover(event) {
if (gesture || event.pointerType === "touch") return;
const card = event.target.closest?.(".card");
fan.setHover(adapter.zones.includes(card?.parentNode) ? card : null);
}
function cancel(event) {
if (!gesture) return;
if (event?.type === "keydown" && event.key !== "Escape") return;
if (event?.cancelable) swallow(event);
cleanup(true);
}
if (window.PointerEvent && adapter.root && status && get) {
listen(window, "pointerdown", start);
listen(window, "pointermove", move);
listen(window, "pointerup", end);
listen(window, "pointercancel", cancel);
listen(window, "blur", cancel);
listen(window, "keydown", cancel);
listen(window, "contextmenu", cancel);
for (const type of ["mousedown", "mousemove", "mouseup", "click", "touchstart", "touchmove", "touchend"]) {
listen(window, type, compatibility, { capture: true, passive: false });
}
}
listen(adapter.root, "pointerover", hover);
listen(adapter.root, "pointerleave", () => { if (!gesture) fan.setHover(null); });
return {
owns: () => Boolean(gesture),
reconcile() {
if (gesture && (!live(gesture) || !gesture.card.isConnected)) cleanup(true);
else if (gesture?.moved) maskControls(!gesture.manual);
for (const node of submittedControls) {
if (node.isConnected) continue;
node.classList.remove("mengsan-hand-submitted-control-shuying"); submittedControls.delete(node);
}
},
dispose() {
try { cleanup(true); }
catch (error) { log("手牌拖动取消失败", error); }
for (const release of listeners.reverse()) release();
maskControls(false);
for (const node of submittedControls) node.classList.remove("mengsan-hand-submitted-control-shuying");
submittedControls.clear();
suppressed = null;
},
};
}
+2 -1
View File
@@ -21,7 +21,8 @@ export function createHandOverlays(document, cardCost) {
const cost = String(cardCost(card));
setData(entry.cost, "mengsanCost", cost);
entry.cost.setAttribute("aria-label", `费用 ${cost}`);
if (entry.cost.parentNode !== card) card.appendChild(entry.cost);
if (card.name === "mengsan_dazed_shuying") entry.cost.remove();
else if (entry.cost.parentNode !== card) card.appendChild(entry.cost);
const level = cardUpgradeLevel(card);
if (level) {
setData(entry.upgrade, "mengsanUpgrade", String(level));
+184 -22
View File
@@ -4,12 +4,21 @@
--mengsan-hand-ink: #fff1cb;
}
.mengsan-hand-scroll-shuying {
overflow-x: auto !important;
overflow-y: hidden !important;
overflow: visible !important;
left: 0 !important;
top: 0 !important;
bottom: 0 !important;
width: 100% !important;
height: 100% !important;
padding: 0 !important;
pointer-events: none;
scrollbar-width: thin;
scrollbar-color: #d8ba83 #234844;
overscroll-behavior-x: contain;
touch-action: pan-x;
touch-action: none;
}
.mengsan-hand-root-shuying {
overflow: visible !important;
}
.mengsan-hand-scroll-shuying:focus-visible {
outline: 2px solid #d8ba83;
@@ -17,37 +26,88 @@
}
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying {
position: relative !important;
display: flex !important;
align-items: flex-end;
gap: 8px;
width: max-content !important;
min-width: 100% !important;
display: block !important;
width: 100% !important;
height: 100% !important;
left: 0 !important;
top: auto !important;
bottom: 0 !important;
margin: 0 !important;
padding: 24px 8px 6px !important;
padding: 0 !important;
box-sizing: border-box;
pointer-events: none;
overflow: visible !important;
}
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card {
position: relative !important;
flex: 0 0 auto;
width: 90px !important;
height: 110px !important;
left: auto !important;
position: absolute !important;
width: 112px !important;
height: 156px !important;
left: 0 !important;
right: auto !important;
top: auto !important;
top: 100% !important;
bottom: auto !important;
margin: 0 !important;
transform: none !important;
transition: transform 120ms ease-out !important;
transform-origin: 50% 100% !important;
transform: translate(var(--mengsan-fan-x, 0px), var(--mengsan-fan-y, -128px)) rotate(var(--mengsan-fan-angle, 0deg)) !important;
z-index: var(--mengsan-fan-z, 1) !important;
transition: transform 160ms cubic-bezier(.2,.7,.3,1), box-shadow 160ms ease-out !important;
pointer-events: auto;
touch-action: none;
}
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.selected {
transform: translateY(-18px) !important;
transform: translate(var(--mengsan-fan-x, 0px), calc(var(--mengsan-fan-y, -128px) - 20px)) rotate(var(--mengsan-fan-angle, 0deg)) !important;
outline: 2px solid var(--mengsan-hand-gold);
outline-offset: -2px;
}
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.mengsan-hand-hover-shuying {
transform: translate(var(--mengsan-fan-x, 0px), calc(var(--mengsan-fan-y, -128px) - 62px)) rotate(0deg) scale(1.35) !important;
z-index: 220 !important;
box-shadow: 0 0 0 2px #b6e8de, 0 0 14px #68b6a58c, 0 12px 22px #0009;
}
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.mengsan-hand-drag-shuying {
top: 0 !important;
transform: translate(var(--mengsan-drag-x), var(--mengsan-drag-y)) rotate(0deg) !important;
z-index: 240 !important;
pointer-events: none !important;
transition: none !important;
box-shadow: 0 0 0 2px #b6e8de, 0 0 14px #68b6a58c, 0 12px 22px #0009;
}
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.mengsan-hand-free-drag-shuying {
transform: translate(var(--mengsan-drag-x), var(--mengsan-drag-y)) rotate(0deg) scale(1.22) !important;
transform-origin: 50% 50% !important;
}
.mengsan-hand-aim-shuying {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
z-index: 1000;
pointer-events: none;
overflow: visible;
}
.mengsan-hand-aim-shuying > path {
fill: none;
stroke: rgb(250, 220, 140);
stroke-width: 2.6;
stroke-linecap: round;
filter: drop-shadow(0 0 2px #f5d25b);
}
/* 只在梦三手牌挂载期间隐藏十周年阶段、等待响应和对方思考图片。 */
body.mengsan-hand-ui-shuying [style*="extension/十周年UI/ui/assets/lbtn/tips/"]:is(
[style*="/tips/hhks."], [style*="/tips/zbjd."], [style*="/tips/pdjd."],
[style*="/tips/mpjd."], [style*="/tips/cpjd."], [style*="/tips/qpjd."],
[style*="/tips/jsjd."], [style*="/tips/hhjs."],
[style*="/tips/ddxy."], [style*="/tips/dfsk."]
) {
display: none !important;
}
body.mengsan-hand-ui-shuying :is(
.mengsan-hand-controls-hidden-shuying,
.mengsan-hand-submitted-control-shuying
) {
visibility: hidden !important;
pointer-events: none !important;
}
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.removing,
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.display-none {
display: none !important;
@@ -86,8 +146,11 @@
content: attr(data-mengsan-cost);
}
.mengsan-hand-zone-shuying > .card > .mengsan-hand-upgrade-shuying {
top: 4px !important;
right: 4px !important;
/* 扇形叠牌露出左侧:角标排在花色下、费用上,不抬高整张牌的层级。 */
top: calc(max(48%, 64px) - 25px) !important;
left: 3px !important;
right: auto !important;
white-space: nowrap;
width: 26px;
height: 20px;
border: 1px solid #86bd85;
@@ -95,13 +158,27 @@
background: #214f36;
color: #f5ffea;
}
/* 只为带强化角标的真实手牌预留左侧信息栏,避免窄屏挤压。 */
.mengsan-hand-zone-shuying > .card:has(> .mengsan-hand-upgrade-shuying) > .mengsan-hand-cost-shuying {
top: max(48%, 64px) !important;
}
.mengsan-hand-zone-shuying > .card:has(> .mengsan-hand-upgrade-shuying) > .suit-num {
top: 6px !important;
}
@media (max-width: 640px), (max-height: 500px) {
.mengsan-hand-zone-shuying > .card:has(> .mengsan-hand-upgrade-shuying) > .suit-num {
top: 4px !important;
font-size: 20px !important;
line-height: 16px !important;
}
}
.mengsan-hand-zone-shuying > .card > .mengsan-hand-upgrade-shuying::before {
content: "+" attr(data-mengsan-upgrade);
}
@media (max-width: 640px), (max-height: 500px) {
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card {
width: 80px !important;
height: 100px !important;
width: 82px !important;
height: 114px !important;
}
}
@media (prefers-reduced-motion: reduce) {
@@ -109,3 +186,88 @@
transition: none !important;
}
}
/* 梦三本地竖幅立绘使用完整卡面;只协调梦三手牌里的原生显示节点。 */
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) {
background-size: cover !important;
background-position: center !important;
}
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) {
display: block !important;
top: auto !important;
bottom: 8px !important;
left: auto !important;
right: 8px !important;
width: auto !important;
height: auto !important;
padding: 3px 6px !important;
margin: 0 !important;
writing-mode: horizontal-tb !important;
-webkit-writing-mode: horizontal-tb !important;
font-size: 16px !important;
line-height: 20px !important;
letter-spacing: 1px !important;
color: #fff1cb !important;
text-shadow: 0 1px 2px #000 !important;
background: #231b17e6 !important;
border-radius: 3px;
z-index: 7 !important;
pointer-events: none;
}
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) br {
display: none;
}
/* 十周年同时保留竖排名称与 top-name 时,只显示后者。 */
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip):has(> .top-name) > .name {
display: none !important;
}
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) {
top: 6px !important;
left: 6px !important;
right: auto !important;
bottom: auto !important;
width: auto !important;
height: auto !important;
margin: 0 !important;
padding: 2px 4px !important;
font-size: 18px !important;
line-height: 18px !important;
letter-spacing: 0 !important;
color: #231b17 !important;
text-shadow: none !important;
background: #fff1cbe6 !important;
border-radius: 3px;
z-index: 7 !important;
pointer-events: none;
}
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]) > .image,
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]) > .info:empty {
display: none !important;
}
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):is([data-suit="heart"], [data-suit="diamond"]):not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) {
color: #a52219 !important;
}
/* 强化卡牌的完整卡面样式优先级更高,窄屏同样为角标留出空隙。 */
@media (max-width: 640px), (max-height: 500px) {
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip):has(> .mengsan-hand-upgrade-shuying) > .suit-num {
top: 4px !important;
padding: 0 2px !important;
font-size: 18px !important;
line-height: 16px !important;
}
}
/* 长牌名完整显示,不挤出卡面;只作用于梦三手牌。 */
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) {
font-size: 12px !important;
letter-spacing: 0 !important;
white-space: nowrap !important;
}
@media (max-width: 640px), (max-height: 500px) {
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) {
font-size: 10px !important;
padding: 2px 4px !important;
}
}