fixed # 更改个人HUD

This commit is contained in:
2026-10-23 00:38:05 +08:00
parent fe3ebcc0e8
commit 727f469e93
89 changed files with 1692 additions and 59 deletions
+279
View File
@@ -0,0 +1,279 @@
import { collectCombatStatuses } from './status-model.js';
import { createOverlayLayout } from './overlays.js';
// Native cards, players and controls stay under their original parents.
export function mountBattleHUD(player, battle, env) {
const { ui, get, _status, document, window, assetBase } = env;
const arena = ui.arena;
if (!arena || !ui.me || !ui.control || !battle.session.active) throw new Error('梦三 HUD 缺少原生界面容器');
const handRoot = ui.me, controlRoot = ui.control;
const overlays = createOverlayLayout(arena, handRoot, ui, player);
const restores = [], styled = new WeakMap(), hiddenMarks = new Set();
let disposed = false, frozen = false, frame = null, dialog = null, previousFocus = null, resize, observer;
const ownClass = (node, name) => {
if (!node || node.classList.contains(name)) return;
node.classList.add(name); restores.push(() => node.classList.remove(name));
};
const style = (node, key, value) => {
let values = styled.get(node);
if (!values) styled.set(node, values = new Map());
if (!values.has(key)) {
const old = node.style.getPropertyValue(key), priority = node.style.getPropertyPriority(key);
const record = { last: null }; values.set(key, record);
restores.push(() => {
if (node.style.getPropertyValue(key) !== record.last) return;
if (old) node.style.setProperty(key, old, priority); else node.style.removeProperty(key);
});
}
const record = values.get(key);
if (record.requested !== value || node.style.getPropertyValue(key) !== record.last) {
node.style.setProperty(key, value, 'important');
record.requested = value;
// CSSOM rounds long decimal pixel values; compare the actual stored value on release.
record.last = node.style.getPropertyValue(key);
}
};
const text = (node, value) => { if (node.textContent !== String(value)) node.textContent = String(value); };
const create = (tag, className, parent, label) => {
const node = document.createElement(tag); node.className = className;
if (label != null) node.textContent = label;
if (tag === 'button') node.type = 'button';
parent?.appendChild(node); return node;
};
const plain = value => String(value || '').replace(/<[^>]*>/g, '');
const nameOf = target => plain(get.translation(target.name || target.playerid || '角色'));
const root = create('section', 'mengsan-hud-shuying', arena);
root.setAttribute('aria-label', '梦三战斗操作区');
const info = create('div', 'mengsan-hud-info-shuying', root);
const name = create('strong', 'mengsan-hud-name-shuying', info, nameOf(player));
const hp = create('span', 'mengsan-hud-hp-shuying', info);
const healthTrack = create('div', 'mengsan-hud-health-track-shuying', info);
healthTrack.setAttribute('aria-hidden', 'true');
const healthFill = create('span', '', healthTrack);
const block = create('span', 'mengsan-hud-block-shuying', info), energy = create('strong', 'mengsan-hud-energy-shuying', info);
const center = create('div', 'mengsan-hud-center-shuying', root);
const statuses = create('div', 'mengsan-hud-statuses-shuying', center);
statuses.setAttribute('aria-label', '玩家当前状态');
const link = create('link', '', null); link.rel = 'stylesheet'; link.href = `${assetBase}/ui/hud/style.css`;
const closeDialog = () => {
if (!dialog) return;
const current = dialog; dialog = null;
if (current.open) current.close(); current.remove();
if (previousFocus?.isConnected) previousFocus.focus();
};
const openDetails = target => {
if (disposed || !battle.session.active) return;
closeDialog(); previousFocus = document.activeElement;
dialog = create('dialog', 'mengsan-hud-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 entries = collectCombatStatuses(target, battle);
for (const entry of entries) {
const row = create('section', 'mengsan-hud-detail-row-shuying', dialog);
create('strong', '', row, `${entry.name}${entry.value == null ? '' : ` ${entry.value}${entry.unit ? ` ${entry.unit}` : ''}`}`);
create('p', '', row, entry.detail);
}
if (!entries.length) create('p', '', dialog, '暂无已登记状态。');
// Unknown/native marks retain their engine intro and are never hidden by the HUD.
dialog.addEventListener('cancel', event => { event.preventDefault(); closeDialog(); });
dialog.addEventListener('close', () => { if (dialog === current) closeDialog(); });
for (const type of ['pointerdown', 'pointerup', 'click', 'touchstart', 'touchend', 'keydown']) dialog.addEventListener(type, event => event.stopPropagation());
dialog.showModal(); close.focus();
};
let statusSignature = null;
function renderStatuses() {
const entries = collectCombatStatuses(player, battle);
const columns = layout === 'narrow' ? 1 : 2;
const rows = Math.max(1, Math.floor((statuses.clientHeight || 80) / 38));
const maximum = Math.max(0, columns * rows - (entries.length > columns * rows ? 1 : 0));
const signature = JSON.stringify([maximum, entries]);
if (signature !== statusSignature) {
statusSignature = signature; statuses.replaceChildren();
center.hidden = entries.length === 0;
for (const entry of entries.slice(0, maximum)) {
const button = create('button', 'mengsan-hud-status-shuying', statuses);
button.setAttribute('aria-label', `${entry.name} ${entry.value ?? ''} ${entry.unit},查看状态详情`);
if (entry.icon) {
const image = create('img', '', button); image.src = `${assetBase}/assets/status/${entry.icon}`; image.alt = '';
image.addEventListener('error', () => image.remove(), { once: true });
}
create('span', '', button, entry.name);
if (entry.value != null) create('b', '', button, entry.value);
button.addEventListener('click', () => openDetails(player));
}
if (entries.length > maximum) {
const more = create('button', 'mengsan-hud-status-shuying', statuses, `+${entries.length - maximum}`);
more.setAttribute('aria-label', '查看全部状态'); more.addEventListener('click', () => openDetails(player));
}
}
const wanted = new Set();
for (const target of [player]) {
for (const entry of collectCombatStatuses(target, battle)) {
for (const skill of entry.markSkills) {
const mark = target.marks?.[skill]; if (mark?.classList) wanted.add(mark);
}
}
}
for (const mark of hiddenMarks) if (!wanted.has(mark)) { mark.classList.remove('mengsan-hud-represented-mark-shuying'); hiddenMarks.delete(mark); }
for (const mark of wanted) if (!mark.classList.contains('mengsan-hud-represented-mark-shuying')) { mark.classList.add('mengsan-hud-represented-mark-shuying'); hiddenMarks.add(mark); }
}
let layout = null;
const hudHeightFor = () => layout === 'short' ? 188 : layout === 'narrow' ? 288 : 274;
const oldLayout = arena.getAttribute('data-mengsan-hud-layout');
restores.push(() => {
if (arena.getAttribute('data-mengsan-hud-layout') !== layout) return;
if (oldLayout === null) arena.removeAttribute('data-mengsan-hud-layout'); else arena.setAttribute('data-mengsan-hud-layout', oldLayout);
});
function skinActor(target) {
// Decade nests these in camp/hp wraps; direct-child .hp/.name selectors miss them.
for (const key of ['campWrap', 'hpWrap', 'hp', 'identity', 'framebg', 'count', 'name', 'name2', 'nameol']) {
ownClass(target.node?.[key], 'mengsan-hud-replaced-node-shuying');
}
ownClass(target.node?.avatar, 'mengsan-hud-portrait-shuying');
ownClass(target.node?.equips, 'mengsan-hud-equips-shuying');
if (target === player) ownClass(target.node?.equips, 'mengsan-hud-self-equips-shuying');
}
function syncActors() {
const width = arena.clientWidth, height = arena.clientHeight;
const narrow = width <= 600 && height >= 480;
const short = height <= 500 && !narrow;
layout = short ? 'short' : narrow ? 'narrow' : 'wide';
if (arena.getAttribute('data-mengsan-hud-layout') !== layout) arena.setAttribute('data-mengsan-hud-layout', layout);
const hudHeight = hudHeightFor();
style(arena, '--mengsan-hud-height', `${hudHeight}px`);
const large = width >= 1000;
style(arena, '--mengsan-hud-left', short ? '216px' : narrow ? '116px' : large ? '352px' : '292px');
style(arena, '--mengsan-hud-right', short ? '132px' : narrow ? '116px' : '184px');
style(arena, '--mengsan-profile-info-left', short ? '108px' : narrow ? '22px' : large ? '200px' : '160px');
style(arena, '--mengsan-profile-width', short ? '172px' : narrow ? '76px' : large ? '200px' : '130px');
skinActor(player);
// Inline important dimensions also beat skins with #arena > .player !important rules.
const selfBottom = narrow ? 180 : 24;
const selfHeight = short || narrow ? 104 : large ? 196 : 148;
const infoTop = narrow ? hudHeight - selfBottom + 16 : hudHeight - selfBottom - selfHeight + (short ? 0 : 20);
style(arena, '--mengsan-profile-info-top', `${infoTop}px`);
for (const [key, value] of Object.entries({left: short ? '24px' : narrow ? '22px' : '40px', right:'auto', top:'auto',
bottom:`${selfBottom}px`, width:short ? '76px' : narrow ? '76px' : large ? '144px' : '104px',
height:`${selfHeight}px`, transform:'none'})) style(player, key, value);
style(arena, '--mengsan-side-dock-bottom', `${selfBottom + selfHeight + 12}px`);
style(arena, '--mengsan-relic-dock-bottom', `${selfBottom + selfHeight + (narrow ? -4 : 12)}px`);
style(arena, '--mengsan-skill-ceiling', `${Math.min(hudHeight - 36, selfBottom + selfHeight)}px`);
style(arena, '--mengsan-status-height', short ? '52px' : narrow ? '84px' : '160px');
}
function syncExternalEquipment() {
const equips = player.node?.equips;
if (!equips?.isConnected || equips.parentNode === player) return;
// An optional skin can keep the same real equipment nodes in a separate arena wrapper.
ownClass(ui.equipSolts, 'mengsan-hud-equip-host-shuying');
const host = equips.offsetParent || arena, h = host.getBoundingClientRect(), a = arena.getBoundingClientRect(), p = player.getBoundingClientRect();
const sx = h.width / host.offsetWidth || 1, sy = h.height / host.offsetHeight || sx;
const arenaScale = a.width / arena.clientWidth || 1;
const width = Math.max(0, p.width - 12 * arenaScale);
const equipHeight = Math.min(100 * arenaScale, p.height * .58);
for (const [key, value] of Object.entries({position:'absolute',left:`${(p.left + 6 * arenaScale - h.left) / sx}px`,right:'auto',bottom:'auto',
top:`${(p.bottom - 6 * arenaScale - equipHeight - h.top) / sy}px`,width:`${width / sx}px`,height:`${equipHeight / sy}px`,'max-height':`${equipHeight / sy}px`})) style(equips, key, value);
}
function placeControl(node, bottom, maxHeight) {
ownClass(node, 'mengsan-hud-external-control-shuying');
const host = node.offsetParent || arena, a = arena.getBoundingClientRect(), h = host.getBoundingClientRect();
const scale = h.width / host.offsetWidth || 1, arenaScale = a.width / arena.clientWidth || 1;
const rail = layout === 'wide' ? 142 : layout === 'short' ? 112 : 96;
for (const [key, value] of Object.entries({position:'absolute',left:'auto',top:'auto',transform:'none',
right:`${(h.right - a.right + (layout === 'wide' ? 24 : 16) * arenaScale) / scale}px`, bottom:`${(h.bottom - a.bottom + bottom * arenaScale) / scale}px`,
width:`${rail * arenaScale / scale}px`, 'max-height':`${maxHeight * arenaScale / scale}px`})) style(node, key, value);
}
function syncControls() {
// The actual action controls stay horizontal above the hand; skill references get their own rail.
const candidates = [...new Set([ui.skills, ui.skills2, ui.skills3, ui.skillControl, ui.gskillControl])]
.filter(node => node?.isConnected && node !== ui.control);
const external = candidates.filter(node => !candidates.some(other => other !== node && other.contains(node)));
const hudHeight = hudHeightFor();
const rail = layout === 'wide' ? 142 : layout === 'short' ? 112 : 96;
const actionBottom = layout === 'narrow' ? hudHeight - 48 : hudHeight + 8;
const compactActions = arena.clientWidth < 1000;
for (const [key,value] of Object.entries({position:'absolute',left:compactActions?'16px':'var(--mengsan-hud-left)',right:compactActions?'16px':'var(--mengsan-hud-right)',top:'auto',bottom:`${actionBottom}px`,width:'auto','max-height':'none',transform:'none'})) style(ui.control,key,value);
for (const group of ui.control.children) if (group.classList.contains('control')) {
if (external.includes(group)) {
ownClass(group,'mengsan-hud-native-skills-shuying');
for (const button of group.children) if (button.tagName === 'DIV') style(button, 'margin', '0px');
continue;
}
for (const [key, value] of Object.entries({position:'relative',left:'auto',right:'auto',top:'auto',bottom:'auto',transform:'none'})) style(group, key, value);
}
let bottom = 20;
const ceiling = parseFloat(arena.style.getPropertyValue('--mengsan-skill-ceiling'));
external.forEach((node, index) => {
if (node === ui.skillControl || node === ui.gskillControl) ownClass(node, 'mengsan-hud-skin-skills-shuying');
placeControl(node, bottom, Math.max(24, (ceiling - bottom - 6 * (external.length - index - 1)) / (external.length - index)));
bottom += node.offsetHeight + 6;
});
for (const node of [ui.confirm,_status.event?.endButton]) {
if (!node?.isConnected || ui.control.contains(node)) continue;
ownClass(node,'mengsan-hud-external-action-shuying');
const host=node.offsetParent||arena,a=arena.getBoundingClientRect(),h=host.getBoundingClientRect();
const scale=h.width/host.offsetWidth||1,zoom=a.width/arena.clientWidth||1;
const center=(parseFloat(arena.style.getPropertyValue('--mengsan-hud-left'))+arena.clientWidth-parseFloat(arena.style.getPropertyValue('--mengsan-hud-right')))/2;
const side=node===ui.confirm?-1:1;
for(const [key,value] of Object.entries({position:'absolute',left:`${(a.left+center*zoom-h.left)/scale+side*80}px`,right:'auto',top:'auto',bottom:`${(h.bottom-a.bottom+actionBottom*zoom)/scale}px`,width:`${Math.min(rail,140)}px`,transform:'translateX(-50%)'}))style(node,key,value);
}
}
function syncNotifications() {
const maxTop = arena.clientHeight - hudHeightFor() - 48;
// Decade positions string popups at player.top + 80, which now intersects our footer.
for (const popup of arena.querySelectorAll(':scope > .damage.skill-popup')) {
if(!popup.classList.contains('mengsan-hud-notification-shuying') &&
(Math.abs(popup.offsetLeft-player.offsetLeft)>1 || Math.abs(popup.offsetTop-player.offsetTop-80)>1))continue;
ownClass(popup, 'mengsan-hud-notification-shuying');
if (popup.offsetTop > maxTop) style(popup, 'top', `${Math.max(0, maxTop)}px`);
const maxLeft = Math.max(0, arena.clientWidth - 160);
style(popup, 'left', `${Math.max(12, Math.min(maxLeft, popup.offsetLeft))}px`);
}
}
function refresh() {
if (disposed || frozen) return;
if (!battle.session.active || !arena.isConnected || !handRoot.isConnected || ui.me !== handRoot || ui.control !== controlRoot) { dispose(); return; }
text(name, nameOf(player)); text(hp, `生命 ${player.hp} / ${player.maxHp}`); text(block, `格挡 ${player.hujia || 0}`);
healthFill.style.width = `${Math.max(0, Math.min(100, player.maxHp > 0 ? player.hp / player.maxHp * 100 : 0))}%`;
text(energy, `费用 ${player.storage.mengsanEnergy_shuying ?? 0} / ${player.storage.mengsanMaxEnergy_shuying ?? 0}`);
syncActors(); syncExternalEquipment(); renderStatuses(); syncControls(); syncNotifications();
overlays.refresh({ footerHeight:hudHeightFor(), status:_status });
}
function schedule() {
if (disposed || frozen || document.hidden || frame !== null) return;
frame = window.requestAnimationFrame(() => { frame = null; refresh(); });
}
const timer = window.setInterval(schedule, 200);
function freeze() {
if(disposed || frozen)return;
frozen=true;window.clearInterval(timer);
if(frame!==null){window.cancelAnimationFrame(frame);frame=null;}
observer?.disconnect();resize?.disconnect();closeDialog();overlays.dispose();
ownClass(arena,'mengsan-hud-frozen-shuying');
}
function dispose() {
if (disposed) return; disposed = true;
window.clearInterval(timer); if (frame !== null) window.cancelAnimationFrame(frame);
observer?.disconnect(); resize?.disconnect(); window.removeEventListener('resize', schedule);
document.removeEventListener('visibilitychange', schedule);
overlays.dispose();
closeDialog();
for (const mark of hiddenMarks) mark.classList.remove('mengsan-hud-represented-mark-shuying'); hiddenMarks.clear();
for (const restore of restores.reverse()) restore();
root.remove(); link.remove();
}
ownClass(document.body, 'mengsan-hud-active-shuying'); ownClass(arena, 'mengsan-hud-arena-shuying');
ownClass(ui.me, 'mengsan-hud-hand-shuying'); ownClass(ui.control, 'mengsan-hud-controls-shuying');
ownClass(ui.mebg, 'mengsan-hud-mebg-shuying'); ownClass(player, 'mengsan-hud-self-shuying');
// Panel events must not reach native selection/drag handlers.
for (const type of ['pointerdown', 'pointerup', 'click', 'touchstart', 'touchend', 'contextmenu']) root.addEventListener(type, event => event.stopPropagation());
link.addEventListener('load', schedule);
link.addEventListener('error', () => { env.log?.('梦三 HUD 样式加载失败,保留原生布局'); dispose(); });
document.head.appendChild(link);
observer = new window.MutationObserver(schedule); observer.observe(ui.control, { childList: true, subtree: true });
observer.observe(arena, { childList: true });
if(ui.window && ui.window!==arena) observer.observe(ui.window,{childList:true});
if (window.ResizeObserver) { resize = new window.ResizeObserver(schedule); resize.observe(arena); }
window.addEventListener('resize', schedule); document.addEventListener('visibilitychange', schedule);
const api = { refresh, freeze, dispose }; battle.session.ownResource(api, dispose); refresh(); return api;
}
+100
View File
@@ -0,0 +1,100 @@
// Position existing native prompts and popped descriptions; never recreate their content or handlers.
export function createOverlayLayout(arena, hand, ui, player) {
const records = new Map();
const roots = new Set([ui.window, arena].filter(Boolean));
const popped = () => new Set([...roots].flatMap(root => [...root.querySelectorAll(':scope > .dialog.popped')]));
const markPopups = new WeakSet();
let activeMarkPopup = null, openingMark = false, disposed = false, options = null;
// Native mark intros do not reject repeated clicks themselves. Observe their real
// popup after the native handler, and block duplicate opens without replacing it.
const guardMark = event => {
const target = event.target.closest?.('.mark');
if (!target || !player.contains(target)) return;
if (openingMark || activeMarkPopup?.isConnected) {
event.preventDefault(); event.stopImmediatePropagation(); return;
}
const before = popped();
openingMark = true;
arena.ownerDocument.defaultView.queueMicrotask(() => {
openingMark = false;
if (disposed) return;
activeMarkPopup = [...popped()].find(node => !before.has(node)) || null;
if (activeMarkPopup) { markPopups.add(activeMarkPopup); if (options) api.refresh(options); }
});
};
const markEvents = ['click', 'touchend', 'contextmenu'];
for (const type of markEvents) player.addEventListener(type, guardMark, true);
function release(node, record) {
for (const [key, field] of record.styles) {
if (node.style.getPropertyValue(key) !== field.last) continue;
if (field.before) node.style.setProperty(key,field.before,field.priority); else node.style.removeProperty(key);
}
for (const [name, had] of record.classes) if (!had) node.classList.remove(name);
records.delete(node);
}
function mark(node, name) {
let record=records.get(node);
if(!record) {record={styles:new Map(),classes:new Map()};records.set(node,record);}
if(!record.classes.has(name))record.classes.set(name,node.classList.contains(name));
node.classList.add(name);return record;
}
function style(node,key,value) {
const record=records.get(node);
if(!record.styles.has(key))record.styles.set(key,{before:node.style.getPropertyValue(key),priority:node.style.getPropertyPriority(key)});
const field=record.styles.get(key);
if(field.requested!==value||node.style.getPropertyValue(key)!==field.last)node.style.setProperty(key,value,'important');
field.requested=value;field.last=node.style.getPropertyValue(key);
}
function position(node,x,y,width,maxHeight) {
for(const [key,value] of Object.entries({position:'absolute',right:'auto',bottom:'auto',transform:'none',margin:'0px',width:`${width}px`,'max-height':`${maxHeight}px`}))style(node,key,value);
const host=node.offsetParent||arena.ownerDocument.documentElement,h=host.getBoundingClientRect();
const sx=h.width/host.offsetWidth||1,sy=h.height/host.offsetHeight||sx;
style(node,'left',`${(x-h.left)/sx-host.clientLeft+host.scrollLeft}px`);
style(node,'top',`${(y-h.top)/sy-host.clientTop+host.scrollTop}px`);
}
const api = {
refresh(nextOptions) {
options = nextOptions;
const {footerHeight,status} = nextOptions;
const used=new Set(),a=arena.getBoundingClientRect(),zoom=a.width/arena.clientWidth||1;
const left=parseFloat(arena.style.getPropertyValue('--mengsan-hud-left'))||0;
const right=parseFloat(arena.style.getPropertyValue('--mengsan-hud-right'))||0;
const x=a.left+left*zoom,width=Math.max(100,arena.clientWidth-left-right);
const controlTop=ui.control.getBoundingClientRect().top;
const prompts=new Set(arena.querySelectorAll(':scope > .hand-tip,:scope > .hand-tip-info'));
const dialog=ui.dialog;
if(dialog?.isConnected && !dialog.classList.contains('popped') && !dialog.classList.contains('fullheight') && !dialog.forcebutton && !dialog.buttons?.length && !dialog.querySelector('.button,.card,.player,input,textarea,select') && status.event?.player===player)prompts.add(dialog);
// The event owns simple native prompt dialogs too, without a global game dependency.
const eventDialog=status.event?.dialog;
if(eventDialog?.isConnected && eventDialog===dialog && !eventDialog.buttons?.length && !eventDialog.forcebutton && !eventDialog.querySelector('.button,.card,.player,input,textarea,select'))prompts.add(eventDialog);
let promptBottom=Math.min(controlTop-10*zoom,a.bottom-(footerHeight+54)*zoom);
for(const node of prompts) {
if(node.hidden || node.style.display==='none')continue;
mark(node,'mengsan-hud-prompt-shuying');used.add(node);
position(node,x,0,width,arena.clientHeight<=500?34:64);
const height=node.getBoundingClientRect().height;
position(node,x,Math.max(a.top+50*zoom,promptBottom-height),width,arena.clientHeight<=500?34:64);
promptBottom=node.getBoundingClientRect().top-6*zoom;
}
const handBox=hand.getBoundingClientRect();
const handTop=Math.min(handBox.top,...[...hand.querySelectorAll('.handcards>.card')].map(node=>node.getBoundingClientRect().top));
const popups=popped();
let hasPopup=false;
for(const node of popups) {
const rect=node.getBoundingClientRect();
if(!markPopups.has(node) && !records.has(node) && !(rect.bottom>handTop && rect.left<handBox.right && rect.right>handBox.left))continue;
mark(node,'mengsan-hud-card-intro-shuying');used.add(node);hasPopup=true;
const popupWidth=Math.min(320,Math.max(180,rect.width/zoom),arena.clientWidth-24);
const maxHeight=Math.max(64,(promptBottom-a.top)/zoom-12);
const center=Math.max(a.left+12*zoom,Math.min(a.right-(popupWidth+12)*zoom,markPopups.has(node)?x:rect.left));
position(node,center,a.top+12*zoom,popupWidth,maxHeight);
const height=node.getBoundingClientRect().height;
position(node,center,Math.max(a.top+12*zoom,promptBottom-height-8*zoom),popupWidth,maxHeight);
}
if(hasPopup)for(const root of roots)for(const node of root.querySelectorAll(':scope > .poplayer')) {mark(node,'mengsan-hud-intro-layer-shuying');style(node,'z-index','89');used.add(node);}
for(const [node,record] of records)if(!node.isConnected||!used.has(node))release(node,record);
},
dispose(){disposed=true;for(const type of markEvents)player.removeEventListener(type,guardMark,true);for(const [node,record]of records)release(node,record);},
};
return api;
}
+119
View File
@@ -0,0 +1,119 @@
import { readCombatStatusSources } from '../../battle/status-sources.js';
import { slowPercent } from '../../monsters/effigy-slow.js';
// The descriptions describe Mengsan's current rules, not an imported Wiki engine.
const storageStatuses = [
['vulnerable', '易伤', 'mengsanVulnerable_shuying', '回合', '受到攻击伤害增加;剩余自身回合数,倍率按攻击来源及当前效果结算。', 'mengsan_vulnerable_shuying'],
['weak', '虚弱', 'mengsanWeak_shuying', '回合', '攻击伤害减少25%;纸鹤对敌人采用40%减伤。自身回合结束减少。', 'mengsan_weak_shuying'],
['frail', '脆弱', 'mengsanFrail_shuying', '回合', '获得格挡减少25%,向下取整;自身回合结束减少。', 'mengsan_frail_shuying'],
['artifact', '人工制品', 'mengsanArtifact_shuying', '次', '每次抵消一次符合规则的负面状态施加,不抵消伤害。', 'mengsan_artifact_shuying'],
['constrict', '紧缠', 'mengsanConstrict_shuying', '伤害', '自身回合结束受到等量非攻击伤害;来源死亡解除。', 'mengsan_constrict_shuying'],
['tangled', '缠结', 'mengsanTangled_shuying', '回合', '攻击牌费用增加1,持续时间可叠加,加费不叠加。', 'mengsan_tangled_shuying'],
['shrink', '缩小', 'mengsanShrink_shuying', '', '攻击伤害减少30%,不重复叠加;来源死亡解除。', 'mengsan_shrink_shuying'],
['plow', '横冲直撞', 'mengsanPlow_shuying', '生命阈值', '受到伤害后生命不高于该阈值时击晕,清零力量并进入第二阶段。', 'mengsan_plow_shuying'],
['slippery', '滑溜', 'mengsanSlippery_shuying', '次', '下一次实际生命损失限制为1,完全格挡不消耗。', 'mengsan_slippery_shuying'],
['territorial', '领地意识', 'mengsanTerritorial_shuying', '力量', '自身回合结束获得等量力量。', 'mengsan_territorial_shuying'],
['infested', '寄生物', 'mengsanInfested_shuying', '召唤数', '死亡后立即召唤扭动虫,必须消灭召唤单位才能获胜。', 'mengsan_infested_shuying'],
];
// [display name, icon, value meaning, description]; only actual effect fields are read.
const effects = {
turnStrength: ['恶魔形态', 'demon_form', '力量', '自身回合开始获得力量。'],
turnEnergy: ['每回合费用', 'demesne', '费用', '自身回合开始额外获得费用。'],
turnBlock: ['绯红披风', 'crimson_mantle', '格挡', '自身回合开始获得格挡;失血部分另列。'],
turnLoseHp: ['披风失血', 'crimson_mantle', '生命', '自身回合开始失去生命。'],
infernoLoseHp: ['狱火失血', 'inferno', '生命', '自身回合开始失去生命。'],
lossDamage: ['狱火', 'inferno', '伤害', '自身回合失去生命时,对所有敌人造成伤害。'],
lossStrength: ['撕裂', 'rupture', '力量', '自身回合失去生命时获得力量。'],
vulnerableBonus: ['易伤增幅', 'vicious', '倍率加成', '对易伤敌人的伤害倍率额外加成。'],
attackBlock: ['狂怒', 'rage', '格挡', '本回合每打出一张攻击牌获得格挡。'],
retaliation: ['火焰屏障', 'flame_barrier', '伤害', '本回合受到攻击后反击。'],
colossus: ['巨像', 'colossus', '', '本回合受到易伤敌人的攻击伤害减少50%。'],
freeAttack: ['免费攻击', 'free_attack', '', '下一张符合规则的攻击牌免费。'],
noDraw: ['不可抽牌', 'no_draw', '', '本回合不能继续抽牌。'],
noEnergy: ['无法获得费用', 'no_energy_gain', '', '本回合不能再获得费用。'],
doubleTap: ['连环拳', 'one_two_punch', '张', '本回合接下来的攻击牌额外打出一次。'],
plating: ['覆甲', 'plating', '格挡', '回合末获得等量格挡;自身回合开始减少1层。'],
exhaustBlock: ['无惧疼痛', 'feel_no_pain', '格挡', '卡牌消耗后获得格挡。'],
exhaustDraw: ['黑暗之拥', 'dark_embrace', '张', '卡牌消耗后抽牌。'],
vulnerableDraw: ['凶恶', 'vicious', '张', '每次成功施加易伤后抽牌。'],
juggle: ['杂耍', 'juggling', '', '自身回合第三张攻击牌复制至手牌。'],
endAttack: ['惊逃', 'stampede', '张', '回合结束随机免费打出攻击手牌。'],
retrieveAttack: ['好勇斗狠', 'aggression', '张', '自身回合开始从弃牌堆获得攻击牌并强化。'],
bodyguard: ['肉盾', 'tank', '', '自身承受敌人双倍伤害,其他友方受到敌人的伤害减半。'],
autoStrike: ['地狱狂徒', 'hellraiser', '', '抽到杀或名称含打击的攻击牌时自动打出。'],
firstBlock: ['不动', 'unmovable', '张', '每个自身回合首批获得格挡的卡牌,格挡翻倍。'],
blockDamage: ['势不可当', 'juggernaut', '伤害', '获得格挡时对随机敌人造成伤害。'],
barricade: ['壁垒', 'barricade', '', '自身回合开始保留格挡。'],
corruption: ['腐化', 'corruption', '', '技能牌费用变为0,打出后消耗。'],
dexterity: ['敏捷', 'dexterity', '格挡', '影响从卡牌中获得的格挡。'],
noCardBlock: ['不可格挡', 'no_block', '回合', '无法从卡牌中获得格挡。'],
intangible: ['无实体', 'intangible', '回合', '按当前规则限制伤害和生命损失。'],
gambit: ['孤注一掷', 'the_gambit', '', '受到未被格挡的攻击伤害时死亡。'],
retain: ['保留手牌', 'retain_hand', '', '本回合保留手牌。'],
panache: ['神气制胜', 'panache', '伤害', '自身回合每打出5张牌,对所有敌人造成伤害。'],
defendBonus: ['勒紧', 'fasten', '格挡', '防御牌额外获得格挡。'],
turnVigor: ['准备时间', 'prep_time', '活力', '自身回合开始获得活力。'],
automation: ['自动化', 'automation', '费用', '抽牌达到计数要求后获得费用。'],
powerDiscount: ['好奇', 'curious', '费用', '能力牌费用降低。'],
boulder: ['滚石', 'rolling_boulder', '伤害', '回合开始对所有敌人造成伤害,随后伤害增加5。'],
nostalgia: ['怀旧', 'nostalgia', '', '每回合首张攻击或技能牌放回抽牌堆顶。'],
entropy: ['熵', 'entropy', '', '自身回合开始变化手牌。'],
beacon_of_hope: ['希望灯塔', 'beacon_of_hope', '', '获得格挡时向其他友方提供格挡。'],
mayhem: ['乱战', 'mayhem', '', '自身回合开始自动打出抽牌堆顶的牌。'],
calamity: ['劫难', 'calamity', '', '打出攻击牌时生成随机攻击牌。'],
};
const activeValue = value => value === true || typeof value === 'number' && Number.isFinite(value) && value !== 0;
const number = value => typeof value === 'number' && Number.isFinite(value) ? value : 0;
const make = (id, name, icon, value, unit, detail, markSkills = []) => ({ id, name,
icon: icon ? `${icon}_power.png` : null, value: value === true ? null : value, unit, detail, markSkills });
export const statusIconNames = [...new Set(['strength', 'ringing', 'slow', 'the_bomb', 'vigor', 'draw_cards_next_turn', 'energy_next_turn', 'block_next_turn',
...storageStatuses.map(row => row[0]), ...Object.values(effects).map(row => row[1])])].map(id => `${id}_power.png`);
export function collectCombatStatuses(player, battle) {
if (!battle?.session?.active) return [];
const storage = player.storage || {}, result = [], sources = readCombatStatusSources(player, battle.session);
const penalty = sources.reduce((n, entry) => n + number(entry.state.negativeStrength), 0);
const permanent = number(storage.mengsanStrength_shuying), temporary = number(storage.mengsanSetupStrength_shuying);
if (permanent || temporary || penalty) result.push(make('strength', '力量', 'strength', permanent + temporary - penalty, '伤害',
`常驻力量 ${permanent};临时力量 ${temporary};本回合减益 ${penalty}。不同临时来源按各自原规则到期。`,
['mengsan_raider_strength_shuying', 'mengsan_setup_strength_shuying']));
for (const [id, name, key, unit, detail, skill] of storageStatuses) {
if (activeValue(storage[key])) result.push(make(id, name, id, storage[key], unit, detail, [skill]));
}
if (storage.mengsanRinging_shuying || player.hasSkill?.('mengsan_ringing_shuying')) result.push(make('ringing', '昏眩', 'ringing',
`${Math.min(1, number(storage.mengsanRingingUsed_shuying))}/1`, '主动出牌', '本次自身回合至多主动使用1张牌,不限制响应;回合结束解除。', ['mengsan_ringing_shuying']));
if (player.hasSkill?.('mengsan_slow_shuying')) result.push(make('slow', '缓慢', 'slow', slowPercent(player), '%',
`缓慢 ${number(storage.mengsanSlow_shuying)} 层;友方回合每打出一张牌,受到的攻击牌伤害增加层数×10%;换回合计数重置。`, ['mengsan_slow_shuying']));
for (const { id, state } of sources) {
for (const zone of ['powers', 'turn', 'defense']) {
for (const [key, value] of Object.entries(state[zone] || {})) {
const definition = effects[key];
if (!definition || !activeValue(value)) continue;
const [name, icon, unit, detail] = definition;
result.push(make(`${id}:${zone}:${key}`, name, icon, key === 'vulnerableBonus' ? `${Math.round(value * 100)}%` : value, unit, detail,
id === 'ironclad' ? ['mengsan_ic_powers_shuying'] : []));
}
}
if (activeValue(state.vigor)) result.push(make(`${id}:vigor`, '活力', 'vigor', state.vigor, '伤害', '下一张攻击牌获得额外伤害。'));
(state.bombs || []).forEach((bomb, index) => result.push(make(`${id}:bomb:${index}`, '炸弹', 'the_bomb', bomb.turns, '回合',
`此实例倒计时结束后对所有敌人造成${bomb.damage}点伤害;各炸弹独立结算。`)));
(state.nextTurn || []).forEach((next, index) => {
for (const [key, name, icon, unit] of [['draw', '下回合抽牌', 'draw_cards_next_turn', '张'], ['energy', '下回合费用', 'energy_next_turn', '费用'], ['block', '下回合格挡', 'block_next_turn', '格挡']]) {
if (activeValue(next[key])) result.push(make(`${id}:next:${index}:${key}`, name, icon, next[key], unit,
`自身回合开始获得;${next.remaining ? `剩余${next.remaining}次。` : '此实例结算一次。'}`));
}
});
}
if (battle.relics?.displayState && battle.owner === player) {
const relic = battle.relics.displayState();
for (const [key, name, icon, unit, detail] of [
['dexterity', '遗物敏捷', 'dexterity', '格挡', '遗物提供的卡牌格挡修正。'],
['plating', '遗物覆甲', 'plating', '格挡', '遗物提供的回合末格挡。'],
['nextEnergy', '下回合费用', 'energy_next_turn', '费用', '遗物将在下个自身回合提供费用。'],
['nextBlock', '下回合格挡', 'block_next_turn', '格挡', '遗物将在下个自身回合提供格挡。']]) {
if (activeValue(relic[key])) result.push(make(`relic:${key}`, name, icon, relic[key], unit, detail));
}
}
return result;
}
+128
View File
@@ -0,0 +1,128 @@
/* Presentation only: native players, cards and controls keep their handlers. */
.mengsan-hud-arena-shuying { --mengsan-hud-paper:#edf2f7;--mengsan-hud-gold:#dcc18e;--mengsan-hud-raised:transparent;--mengsan-hud-stroke:rgba(240,240,240,.35);overflow:clip!important; }
.mengsan-hud-hand-shuying,.mengsan-hud-controls-shuying,.mengsan-hud-external-control-shuying,.mengsan-hud-self-shuying,.mengsan-hud-actor-shuying { transition:none!important; }
.mengsan-hud-native-skills-shuying,.mengsan-hud-native-skills-shuying>div,.mengsan-hud-skin-skills-shuying>.enable,.mengsan-hud-skin-skills-shuying>.trigger,.mengsan-hud-skin-skills-shuying .skillitem { transition:none!important; }
.mengsan-hud-shuying { position:absolute;left:0;right:0;bottom:0;height:var(--mengsan-hud-height,274px);z-index:20;box-sizing:border-box;pointer-events:none;color:var(--mengsan-hud-paper);background:transparent;border:0;border-radius:0;backdrop-filter:none;box-shadow:none;text-shadow:0 1px 3px #000,0 0 6px #000c;font:13px/1.4 "Microsoft YaHei",sans-serif; }
/* Transparent layout only: no footer slab, tint, outline or blur. */
.mengsan-hud-shuying::before,.mengsan-hud-shuying::after { display:none; }
.mengsan-hud-shuying *,.mengsan-hud-dialog-shuying * { box-sizing:border-box; }
.mengsan-hud-shuying div,.mengsan-hud-shuying span,.mengsan-hud-shuying strong,.mengsan-hud-shuying button,.mengsan-hud-dialog-shuying section,.mengsan-hud-dialog-shuying p { position:relative;display:block;float:none; }
.mengsan-hud-shuying [hidden],.mengsan-unit-status-shuying[hidden] { display:none!important; }
.mengsan-hud-shuying .mengsan-hud-info-shuying,.mengsan-hud-shuying .mengsan-hud-center-shuying { position:absolute; }
.mengsan-hud-shuying button,.mengsan-unit-status-shuying,.mengsan-hud-dialog-shuying button { color:var(--mengsan-hud-paper,#edf2f7);background:var(--mengsan-hud-raised,#28374ad6);border:1px solid var(--mengsan-hud-stroke,#dce8f42e);border-radius:12px;font:inherit;cursor:pointer;touch-action:manipulation;pointer-events:auto; }
.mengsan-hud-shuying button:hover,.mengsan-unit-status-shuying:hover { border-color:#dcc18e;background:#ffffff12; }
.mengsan-hud-shuying button:focus-visible,.mengsan-unit-status-shuying:focus-visible,.mengsan-hud-dialog-shuying button:focus-visible { outline:2px solid #f4d894;outline-offset:2px; }
.mengsan-hud-shuying .mengsan-hud-info-shuying { left:var(--mengsan-profile-info-left);top:var(--mengsan-profile-info-top);bottom:auto;width:100px;display:flex;flex-direction:column;gap:12px; }
.mengsan-hud-name-shuying { font:600 18px/1.2 "Microsoft YaHei",sans-serif;letter-spacing:.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.mengsan-hud-hp-shuying,.mengsan-hud-block-shuying { font-size:12px;color:#e2dfd4;font-variant-numeric:tabular-nums;white-space:nowrap; }
.mengsan-hud-health-track-shuying { width:100%;height:4px;margin:0 0 2px;background:#ffffff1a;border-radius:4px;overflow:hidden; }
.mengsan-hud-health-track-shuying>span { height:100%;background:#9bb78d; }
.mengsan-hud-energy-shuying { color:#eacb87;font-size:16px;font-variant-numeric:tabular-nums;white-space:nowrap; }
.mengsan-hud-center-shuying { left:40px;right:auto;top:auto;bottom:var(--mengsan-side-dock-bottom);width:var(--mengsan-profile-width);height:var(--mengsan-status-height); }
.mengsan-hud-shuying .mengsan-hud-statuses-shuying { display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-auto-rows:32px;align-content:end;gap:6px;overflow:hidden;white-space:nowrap;height:100%; }
.mengsan-hud-shuying .mengsan-hud-status-shuying { display:flex;align-items:center;gap:4px;max-width:100%;min-width:0;min-height:32px;padding:3px 6px;background:transparent;border-radius:10px; }
.mengsan-hud-status-shuying span { overflow:hidden;text-overflow:ellipsis;font-size:12px; }
.mengsan-hud-status-shuying b { font-variant-numeric:tabular-nums;font-size:13px; }
.mengsan-hud-status-shuying img { width:22px;height:22px;object-fit:contain; }
.mengsan-hud-hand-shuying { position:absolute!important;left:var(--mengsan-hud-left)!important;right:var(--mengsan-hud-right)!important;bottom:68px!important;top:auto!important;width:calc(100% - var(--mengsan-hud-left) - var(--mengsan-hud-right))!important;height:calc(var(--mengsan-hud-height,274px) - 126px)!important;z-index:24!important;background:none!important;border:0!important;transform:none!important;pointer-events:none; }
.mengsan-hud-hand-shuying .handcards>.card { pointer-events:auto; }
.mengsan-hud-mebg-shuying,.mengsan-hud-hand-shuying>.fakeme.avatar { display:none!important; }
/* These explicit references cover Decade's nested camp-wrap and hp-wrap as well. */
.mengsan-hud-active-shuying .mengsan-hud-replaced-node-shuying { display:none!important; }
#arena.mengsan-hud-arena-shuying>.player.mengsan-hud-self-shuying { box-sizing:border-box!important;background:#172131!important;border:1px solid #dce8f44d!important;border-radius:16px!important;box-shadow:0 6px 20px #0005!important;min-width:0!important;min-height:0!important;max-width:none!important;max-height:none!important; }
#arena.mengsan-hud-arena-shuying>.player.mengsan-hud-self-shuying { z-index:25!important; }
#arena.mengsan-hud-arena-shuying>.player.mengsan-hud-self-shuying::before,#arena.mengsan-hud-arena-shuying>.player.mengsan-hud-self-shuying::after { display:none!important; }
#arena.mengsan-hud-arena-shuying>.player.selectable.mengsan-hud-self-shuying { outline:2px solid #e8d198;outline-offset:3px; }
#arena.mengsan-hud-arena-shuying>.player.selected.mengsan-hud-self-shuying,#arena.mengsan-hud-arena-shuying>.player.target.mengsan-hud-self-shuying { outline:3px solid #f29b73;outline-offset:3px; }
#arena.mengsan-hud-arena-shuying .player>.mengsan-hud-portrait-shuying,#arena.mengsan-hud-arena-shuying .player.mengsan-hud-self-shuying>.dynamic-wrap { position:absolute!important;inset:0!important;width:100%!important;height:100%!important;transform:none!important;clip-path:inset(0 round 14px)!important;-webkit-clip-path:inset(0 round 14px)!important;border-radius:14px!important;background-size:cover!important;background-position:center 15%!important; }
.mengsan-hud-portrait-shuying>.action { display:none!important; }
.mengsan-hud-self-shuying>.mengsan-energy-shuying,.mengsan-hud-actor-shuying>.mengsan-energy-shuying { display:none!important; }
.mengsan-hud-represented-mark-shuying { display:none!important; }
/* Container IDs and combo-control differ. Bind to owned classes and keep all callbacks. */
/* Only arrange the real native control groups; their theme owns all button paint. */
body.mengsan-hud-active-shuying #arena>.mengsan-hud-controls-shuying { height:auto!important;display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;align-items:center!important;align-content:flex-end!important;justify-content:center!important;gap:8px;overflow:visible!important;margin:0!important;padding:0!important;z-index:30!important;pointer-events:none; }
body.mengsan-hud-active-shuying #arena>.mengsan-hud-controls-shuying>.control { position:relative!important;left:auto!important;right:auto!important;top:auto!important;bottom:auto!important;transform:none!important;display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;flex:none!important;align-items:center!important;justify-content:center!important;width:auto!important;max-width:100%!important;min-width:0!important;height:auto!important;margin:0!important;overflow:visible!important;pointer-events:none; }
body.mengsan-hud-active-shuying #arena>.mengsan-hud-controls-shuying>.control>div { pointer-events:auto; }
.mengsan-hud-external-control-shuying { z-index:31!important;overflow:auto!important;box-sizing:border-box; }
.mengsan-hud-skin-skills-shuying { height:auto!important;max-width:none!important;display:flex!important;flex-direction:column!important;gap:4px; }
.mengsan-hud-skin-skills-shuying>.enable,.mengsan-hud-skin-skills-shuying>.trigger { position:relative!important;display:flex!important;flex-wrap:nowrap!important;flex-direction:column!important;align-items:stretch!important;justify-content:center!important;gap:8px;width:100%!important;height:auto!important;margin:0!important;padding:0!important; }
.mengsan-hud-skin-skills-shuying .skillitem { position:relative!important;float:none!important;box-sizing:border-box!important;width:auto!important;min-width:40px!important;max-width:100%!important;height:auto!important;min-height:28px!important;margin:0!important;padding:4px 8px!important;background-size:100% 100%!important;border:0!important;border-radius:10px!important;font:13px/20px "Microsoft YaHei",sans-serif!important;white-space:normal!important; }
.mengsan-hud-skin-skills-shuying>.enable>.skillitem { width:100%!important;min-height:34px!important;text-align:center; }
.mengsan-hud-skin-skills-shuying .skillitem { -webkit-text-fill-color:currentColor!important;-webkit-text-stroke:0!important;background-clip:border-box!important;-webkit-background-clip:border-box!important; }
.mengsan-hud-skin-skills-shuying .skillitem::before,.mengsan-hud-skin-skills-shuying .skillitem-child { display:none!important; }
.mengsan-hud-skin-skills-shuying .skillitem:is(.select,.selected) { outline:1px solid #e6cb93;outline-offset:-2px; }
.mengsan-hud-skin-skills-shuying>.enable>.skillitem:not(.usable):not(.select),.mengsan-hud-skin-skills-shuying .skillitem.shixiao { opacity:.55; }
.mengsan-hud-skin-skills-shuying>.trigger>.skillitem { color:#c2c7c8!important;font-size:12px!important; }
.mengsan-hud-active-shuying .mengsan-hand-relics-shuying { position:absolute!important;left:auto!important;right:24px!important;transform:none!important;top:auto!important;bottom:var(--mengsan-relic-dock-bottom)!important;width:128px!important;max-width:128px!important;height:auto;max-height:var(--mengsan-status-height);padding:2px;box-sizing:border-box;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-end;gap:6px;overflow-x:hidden;overflow-y:auto;z-index:22;touch-action:pan-y;background:transparent!important;border:0;border-radius:14px;scrollbar-width:thin;scrollbar-color:#dce8f440 transparent; }
.mengsan-hud-active-shuying .mengsan-hand-relics-shuying[hidden] { display:none!important; }
.mengsan-hud-active-shuying .mengsan-hand-relics-shuying>button { flex:none;width:32px;min-height:32px;height:32px;border-color:var(--mengsan-hud-stroke);border-radius:10px;background:transparent; }
.mengsan-hud-active-shuying .mengsan-hand-relics-shuying img { width:28px;height:28px; }
.mengsan-hud-active-shuying .mengsan-hud-hand-shuying .mengsan-hand-scroll-shuying { left:0!important;width:100%!important; }
.mengsan-hud-dialog-shuying { position:fixed;inset:0;margin:auto;width:min(620px,calc(100vw - 24px));max-height:82vh;padding:24px;color:#edf2f7;background:#172131ed;border:1px solid #dce8f42e;border-radius:20px;backdrop-filter:blur(10px);overflow:auto;z-index:150;font:15px/1.6 "Microsoft YaHei",sans-serif; }
.mengsan-hud-dialog-shuying::backdrop { background:#0009; }
.mengsan-hud-dialog-shuying h2 { margin:0 70px 14px 0;font-size:19px; }
.mengsan-hud-dialog-close-shuying { position:absolute;right:14px;top:14px;padding:5px 10px;min-height:36px; }
.mengsan-hud-detail-row-shuying { border-top:1px solid #cbb58a40;padding:10px 0; }
.mengsan-hud-detail-row-shuying p { margin:4px 0; }
[data-mengsan-hud-layout="short"] .mengsan-hud-shuying .mengsan-hud-info-shuying { left:108px;width:90px;gap:4px; }
[data-mengsan-hud-layout="short"] .mengsan-hud-hp-shuying,[data-mengsan-hud-layout="short"] .mengsan-hud-block-shuying { font-size:11px; }
[data-mengsan-hud-layout="short"] .mengsan-hud-name-shuying { font-size:15px; }
[data-mengsan-hud-layout="short"] .mengsan-hud-energy-shuying { font-size:14px; }
[data-mengsan-hud-layout="short"] .mengsan-hud-hand-shuying { bottom:64px!important;height:calc(var(--mengsan-hud-height) - 122px)!important; }
[data-mengsan-hud-layout="short"] .mengsan-hud-center-shuying { top:auto;left:24px; }
[data-mengsan-hud-layout="short"] .mengsan-hud-shuying .mengsan-hud-status-shuying { min-height:26px;padding:2px 4px; }
[data-mengsan-hud-layout="short"] .mengsan-hud-status-shuying img { width:18px;height:18px; }
[data-mengsan-hud-layout="narrow"] .mengsan-hud-shuying .mengsan-hud-info-shuying { left:22px;width:76px;font-size:12px;gap:5px; }
[data-mengsan-hud-layout="narrow"] .mengsan-hud-hand-shuying { bottom:68px!important; }
[data-mengsan-hud-layout="narrow"] .mengsan-hud-name-shuying { font-size:16px; }
[data-mengsan-hud-layout="narrow"] .mengsan-hud-status-shuying span { display:none; }
#arena.mengsan-hud-arena-shuying>.mengsan-hud-notification-shuying { width:148px!important;max-width:148px!important;min-height:30px;height:30px!important;transition:none!important;font:600 16px/30px "Microsoft YaHei",sans-serif!important;white-space:nowrap; }
#arena.mengsan-hud-arena-shuying>.mengsan-hud-notification-shuying>span { font-size:16px!important; }
/* Stack real equipment nodes over the lower portrait, like Decade's player equips. */
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying {
position:absolute!important;left:6px!important;right:auto!important;top:auto!important;bottom:6px!important;
width:calc(100% - 12px)!important;min-width:0!important;height:auto!important;max-height:min(100px,58%)!important;box-sizing:border-box!important;
display:flex!important;flex-direction:column!important;align-items:stretch!important;justify-content:flex-start!important;gap:2px;overflow-x:hidden!important;overflow-y:auto!important;
padding:0!important;margin:0!important;transform:none!important;transition:none!important;background:none!important;border:0!important;box-shadow:none!important;
pointer-events:auto!important;touch-action:pan-y;scrollbar-width:thin;scrollbar-color:#dce8f440 transparent;z-index:72!important;
}
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying:empty { display:none!important; }
body.mengsan-hud-active-shuying #arena .mengsan-hud-equip-host-shuying { opacity:1!important;visibility:visible!important;overflow:visible!important;pointer-events:none!important;z-index:26!important; }
body.mengsan-hud-active-shuying #arena .mengsan-hud-equip-host-shuying>.equips-back { display:none!important; }
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying>.card {
position:relative!important;inset:auto!important;display:flex!important;flex:none!important;align-items:center!important;justify-content:center!important;
width:100%!important;min-width:0!important;max-width:100%!important;height:18px!important;margin:0!important;padding:0 2px!important;box-sizing:border-box!important;
transform:none!important;transition:none!important;overflow:hidden!important;border:1px solid #5f372d!important;border-radius:3px!important;
background:rgba(228,207,166,.88)!important;box-shadow:none!important;color:#181818!important;pointer-events:auto!important;
}
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying>.card>.name2 {
position:relative!important;inset:auto!important;display:block!important;max-width:100%!important;width:auto!important;height:auto!important;
overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;color:#181818!important;font:11px/16px "Microsoft YaHei",sans-serif!important;
text-shadow:none!important;pointer-events:none!important;
}
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying>.card>.name2>span { position:static!important;display:inline!important;font:inherit!important; }
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying>.card>:not(.name2) { display:none!important; }
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying>.card.selectable { border-color:#e4c98e!important;box-shadow:inset 0 0 0 1px #e4c98e!important; }
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying>.card.selected { background:#49624b!important;border-color:#e4c98e!important; }
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying>.card:is([data-suit="heart"],[data-suit="diamond"])>.name2 { color:#8b0000!important; }
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying::-webkit-scrollbar { width:3px; }
body.mengsan-hud-active-shuying #arena .mengsan-hud-equips-shuying::-webkit-scrollbar-thumb { background:#dce8f440;border-radius:3px; }
[data-mengsan-hud-layout="short"] .mengsan-hand-relics-shuying { right:16px!important;width:112px!important;max-width:112px!important; }
[data-mengsan-hud-layout="narrow"] .mengsan-hand-relics-shuying { right:16px!important;width:96px!important;max-width:96px!important;max-height:36px;flex-wrap:nowrap;justify-content:flex-start;overflow-x:auto;overflow-y:hidden;touch-action:pan-x; }
body.mengsan-hud-active-shuying #arena>.mengsan-hud-controls-shuying>.mengsan-hud-native-skills-shuying { flex-direction:column!important;flex-wrap:nowrap!important;align-items:stretch!important;max-width:none!important;overflow:auto!important; }
body.mengsan-hud-active-shuying #arena .mengsan-hud-native-skills-shuying>div { width:100%!important;min-width:0!important;box-sizing:border-box!important;padding:8px 10px!important;min-height:40px!important;font:14px/20px "Microsoft YaHei",sans-serif!important;white-space:normal!important; }
[data-mengsan-hud-layout="narrow"] .mengsan-hud-center-shuying { left:22px; }
[data-mengsan-hud-layout="short"] .mengsan-hud-shuying .mengsan-hud-statuses-shuying { height:100%; }
[data-mengsan-hud-layout="narrow"] .mengsan-hud-shuying .mengsan-hud-statuses-shuying { grid-template-columns:1fr; }
.mengsan-hud-prompt-shuying { box-sizing:border-box!important;height:auto!important;min-height:0!important;overflow:auto!important;padding:2px 6px!important;background:none!important;border:0!important;box-shadow:none!important;text-align:center!important;z-index:32!important;color:#f6edcb!important;text-shadow:0 1px 3px #000,0 0 6px #000!important; }
body.mengsan-hud-active-shuying .mengsan-hud-card-intro-shuying { box-sizing:border-box!important;z-index:90!important;height:auto!important;min-height:0!important;overflow:auto!important;border-radius:14px!important;background:#202020f5!important;box-shadow:0 8px 24px #0007!important;color:#f4f4f4!important;transition:none!important; }
body.mengsan-hud-active-shuying #window>.mengsan-hud-card-intro-shuying { z-index:90!important; }
body.mengsan-hud-active-shuying #window>.mengsan-hud-intro-layer-shuying { z-index:89!important; }
.mengsan-hud-card-intro-shuying>.content-container,.mengsan-hud-prompt-shuying>.content-container { position:relative!important;inset:auto!important;height:auto!important;max-height:none!important;overflow:visible!important; }
.mengsan-hud-card-intro-shuying>.content-container>.content,.mengsan-hud-prompt-shuying>.content-container>.content { position:relative!important;inset:auto!important;height:auto!important; }
.mengsan-hud-card-intro-shuying .content>div,.mengsan-hud-prompt-shuying .content>div { position:relative!important; }
body.mengsan-hud-active-shuying .mengsan-hud-intro-layer-shuying { z-index:89!important;transition:none!important; }
.mengsan-hud-frozen-shuying .mengsan-hud-hand-shuying,.mengsan-hud-frozen-shuying .mengsan-hud-self-shuying,.mengsan-hud-frozen-shuying .mengsan-hud-shuying { pointer-events:none!important; }
.mengsan-hud-frozen-shuying .mengsan-hud-hand-shuying *,.mengsan-hud-frozen-shuying .mengsan-hud-self-shuying *,.mengsan-hud-frozen-shuying .mengsan-hud-shuying * { pointer-events:none!important; }
.mengsan-hud-frozen-shuying .mengsan-hud-controls-shuying,.mengsan-hud-frozen-shuying .mengsan-hud-external-control-shuying,.mengsan-hud-frozen-shuying .hand-tip,.mengsan-hud-frozen-shuying .hand-tip-info { visibility:hidden!important; }