From 205a0174ae13c48086c3f2d0af43f9d3a8c84ce9 Mon Sep 17 00:00:00 2001 From: Shu-Ying <1754798088@qq.com> Date: Mon, 5 Oct 2026 17:41:18 +0800 Subject: [PATCH] =?UTF-8?q?fixed=20#=20=E4=BF=AE=E6=AD=A3=E5=B7=B2?= =?UTF-8?q?=E7=9F=A5BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension.css | 103 ++++++++++++++++++ extension.js | 2 +- function.js | 48 +++++++- mengsan/assets/cards/README.md | 2 +- mengsan/ui/hand/index.js | 4 +- mengsan/ui/hand/overlays.js | 21 +++- mengsan/ui/hand/style.css | 193 ++++++++++----------------------- mengsan/ui/map/renderer.js | 13 ++- mengsan/ui/navigation.js | 71 +++++++++++- mengsan/ui/style.css | 4 + menu.js | 131 ++++++++++++++++++---- update.js | 25 +++++ 12 files changed, 443 insertions(+), 174 deletions(-) diff --git a/extension.css b/extension.css index 9a974b2..2aa7125 100644 --- a/extension.css +++ b/extension.css @@ -1096,6 +1096,109 @@ } } +/* 更新设置:沿用选择窗口材质,两个选项与取消/确认按钮适配窄屏。 */ +.shuYing-option-dialog-mask[data-variant="settings"] { + --setting-muted: #d1c9bc; + --setting-focus: #ffe2a0; + padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) + max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left)) !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog { + width: min(600px, 100%) !important; + max-height: calc(100vh - 32px) !important; + max-height: calc(100dvh - max(12px, env(safe-area-inset-top)) - max(12px, env(safe-area-inset-bottom))) !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-title, +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-intro, +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-controls { + flex: none !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-intro, +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-tag, +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-description { + color: var(--setting-muted) !important; + font-size: 14px !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-options { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + min-height: 0 !important; + gap: 12px !important; + padding: 18px !important; + overflow-x: hidden !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-card { + min-height: 170px !important; + padding: 38px 18px 18px !important; + transform: none !important; + touch-action: manipulation; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-order { + display: none !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-name { + margin-top: 0 !important; + font-size: 25px !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-description { + margin-top: 12px !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-controls { + gap: 12px !important; + padding: 0 18px 18px !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-confirm { + flex: 1 !important; + min-width: 0 !important; + min-height: 48px !important; + height: auto !important; + padding: 10px 8px !important; + font-size: 16px !important; + line-height: 1.4 !important; + touch-action: manipulation; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-cancel { + background: rgba(255, 255, 255, 0.04) !important; + border-color: rgba(232, 190, 99, 0.4) !important; + color: var(--setting-muted) !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] button:focus-visible { + outline: 2px solid var(--setting-focus) !important; + outline-offset: 2px !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] button:active:not(:disabled) { + opacity: 0.85 !important; +} + +.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-confirm:disabled { + opacity: 0.5 !important; + cursor: default !important; + box-shadow: none !important; +} + +@media screen and (max-width: 520px) { + .shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-options { + grid-template-columns: minmax(0, 1fr) !important; + padding: 12px !important; + } + + .shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-card { + min-height: 140px !important; + } +} + /* 天书对局羁绊入口与概览:不依赖十周年 UI 节点,原版和扩展 UI 共用。 */ .shuYing-bond-overview-button { all: unset !important; diff --git a/extension.js b/extension.js index f3f699d..ddff93a 100644 --- a/extension.js +++ b/extension.js @@ -5,7 +5,7 @@ import initShuYingMenu from "./menu.js"; export const type = "extension"; export let shuYing = new Object(); -const shuYingLocalVersion = "2.0.2.3"; +const shuYingLocalVersion = "2.0.2.3-rc.1"; shuYing.name = "术樱包"; shuYing.editable = false; diff --git a/function.js b/function.js index 09d922b..913ae71 100644 --- a/function.js +++ b/function.js @@ -225,7 +225,8 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing = }; // 创建可复用的对局单选界面;单击选中卡片,确认后返回对应键值。 - shuYing.chooseSingleOptionDialog = ({ title = "请选择", intro = "", options = [], defaultKey = null } = {}) => { + shuYing.chooseSingleOptionDialog = ({ title = "请选择", intro = "", options = [], defaultKey = null, + currentKey = null, cancelable = false, variant = "default", confirmText = "确定" } = {}) => { return new Promise(resolve => { const items = options.map(option => typeof option == "string" ? { key: option, name: option } : option) .filter(option => option?.key); @@ -240,18 +241,26 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing = if (parent) parent.appendChild(node); return node; }; + const previousFocus = document.activeElement; const mask = createNode("div", "shuYing-option-dialog-mask", null, document.body); + mask.dataset.variant = variant; const dialog = createNode("div", "shuYing-option-dialog", null, mask); + dialog.setAttribute("role", "dialog"); + dialog.setAttribute("aria-modal", "true"); + dialog.setAttribute("aria-label", title); createNode("div", "shuYing-option-dialog-title", title, dialog); if (intro) createNode("div", "shuYing-option-dialog-intro", intro, dialog); const optionArea = createNode("div", "shuYing-option-dialog-options", null, dialog); const controls = createNode("div", "shuYing-option-dialog-controls", null, dialog); let closed = false; + let onKeydown = null; let selectedKey = defaultKey && items.some(item => item.key == defaultKey) ? defaultKey : items[0].key; const finish = key => { if (closed) return; closed = true; + if (onKeydown) document.removeEventListener("keydown", onKeydown, true); mask.remove(); + if (cancelable && previousFocus?.isConnected) previousFocus.focus?.({ preventScroll: true }); resolve(key); }; const refreshSelection = () => { @@ -260,6 +269,7 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing = card.classList.toggle("selected", selected); card.setAttribute("aria-pressed", String(selected)); }); + confirm.disabled = currentKey != null && selectedKey == currentKey; }; items.forEach((option, index) => { const card = createNode("button", "shuYing-option-dialog-card", null, optionArea); @@ -276,14 +286,44 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing = refreshSelection(); }); }); - const confirm = createNode("button", "shuYing-option-dialog-confirm", "确定", controls); + if (cancelable) { + const cancel = createNode("button", "shuYing-option-dialog-confirm shuYing-option-dialog-cancel", "取消", controls); + cancel.type = "button"; + cancel.addEventListener("click", event => { event.stopPropagation(); finish(null); }); + } + const confirm = createNode("button", "shuYing-option-dialog-confirm", confirmText, controls); confirm.type = "button"; confirm.addEventListener("click", event => { event.stopPropagation(); - finish(selectedKey); + if (!confirm.disabled) finish(selectedKey); + }); + mask.addEventListener("click", event => { + event.stopPropagation(); + if (cancelable && event.target == mask) finish(null); }); - mask.addEventListener("click", event => event.stopPropagation()); dialog.addEventListener("click", event => event.stopPropagation()); + if (cancelable) { + dialog.addEventListener("keydown", event => event.stopPropagation()); + onKeydown = event => { + if (event.key == "Escape") { + event.preventDefault(); + event.stopPropagation(); + finish(null); + } + else if (event.key == "Tab") { + const buttons = Array.from(dialog.querySelectorAll("button:not(:disabled)")); + const first = buttons[0], last = buttons[buttons.length - 1]; + if (!dialog.contains(document.activeElement) + || event.shiftKey && document.activeElement == first + || !event.shiftKey && document.activeElement == last) { + event.preventDefault(); + (event.shiftKey ? last : first)?.focus(); + } + event.stopPropagation(); + } + }; + document.addEventListener("keydown", onKeydown, true); + } refreshSelection(); const initial = Array.from(optionArea.querySelectorAll(".shuYing-option-dialog-card")).find(node => node.dataset.key == selectedKey) || optionArea.querySelector(".shuYing-option-dialog-card"); diff --git a/mengsan/assets/cards/README.md b/mengsan/assets/cards/README.md index ece5809..b9a0e61 100644 --- a/mengsan/assets/cards/README.md +++ b/mengsan/assets/cards/README.md @@ -9,7 +9,7 @@ - 批量优化只处理 `mengsan_*.png` 运行时卡牌,参考原件如 `wound-original.png` 不参与。 - 画风:泛黄宣纸、黑褐水墨、飞白与淡赭色、中国古代人物或兵器;根据卡牌效果独立安排竖幅构图。拆包图只作题材参考,不修改拆包原件。 - 立绘不绘制名称、费用、花色、强化角标或边框。左上花色点数、左侧中段费用、右下名称由游戏原生节点和梦三样式叠加;左侧信息栏和右下名称需留空间。 -- 卡牌定义使用 `fullimage: true` 与明确的 `image` 路径,不使用横图 `fullskin`。`mengsan_ic_`、`mengsan_colorless_`、`mengsan_event_`、`mengsan_status_`、`mengsan_curse_` 已按前缀接入 `ui/hand/style.css` 的本地完整卡面选择器;其他命名的新增卡牌须单独接入,长牌名同时加入长名称窄屏规则。保持 `cover`,不使用 `100% 100%` 拉伸。 +- 卡牌定义使用 `fullimage: true` 与明确的 `image` 路径,不使用横图 `fullskin`。手牌叠层按实际注册定义的 `fullimage: true` 及 `ext:术樱包/mengsan/assets/cards/牌名.png` 自动设置 `data-mengsan-local-art="full"`;四字及以上的翻译名称自动标记为长名,常规和窄屏样式统一,不再维护逐牌 CSS 名单。保持 `cover`,不使用 `100% 100%` 拉伸。 - 费用和强化角标继续使用现有 `data-*` 与伪元素,避免向卡牌子节点写入额外文字而触发十周年 UI 扫描。 - 不改变卡牌费用、效果、强化、奖励与存档数据;不修改全局标准/军争牌或十周年 UI。 - 接入后检查完整立绘、花色/费用/名称、扇形叠牌、悬停放大和窄屏。文件检查、隔离行为测试及预览不代表实机验收。 diff --git a/mengsan/ui/hand/index.js b/mengsan/ui/hand/index.js index 7deaf68..8d56066 100644 --- a/mengsan/ui/hand/index.js +++ b/mengsan/ui/hand/index.js @@ -7,7 +7,9 @@ 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, card => cardCost(card, player)); + const overlays = createHandOverlays(document, card => cardCost(card, player), { + definitions: env.lib?.card, translations: env.lib?.translate, + }); const fan = createHandFan(adapter, env); let input = null; const restores = []; diff --git a/mengsan/ui/hand/overlays.js b/mengsan/ui/hand/overlays.js index 19866bd..4427417 100644 --- a/mengsan/ui/hand/overlays.js +++ b/mengsan/ui/hand/overlays.js @@ -2,13 +2,30 @@ import { cardUpgradeLevel } from "../../cards/upgrades.js"; import { isXCostCard } from "../../battle/combat-rules.js"; import { applyCardRarity } from "../../cards/rarity.js"; -export function createHandOverlays(document, cardCost) { +export function createHandOverlays(document, cardCost, { definitions, translations } = {}) { const entries = new Map(); const setData = (node, key, value) => { if (node.dataset[key] !== value) node.dataset[key] = value; }; function refresh(card) { if (!card.name) return; + // Opt in only the mode's registered complete illustrations; native card skins stay native. + // Re-evaluate after card.init/transformation, without adding child text for Decade UI to scan. + const definition = definitions?.[card.name]; + const localArt = definition?.fullimage === true && + definition.image === `ext:术樱包/mengsan/assets/cards/${card.name}.png`; + if (localArt) { + setData(card, "mengsanLocalArt", "full"); + const title = translations?.[card.name]; + if (typeof title === "string" && Array.from(title).length >= 4) { + setData(card, "mengsanLongName", "true"); + } else { + delete card.dataset.mengsanLongName; + } + } else { + delete card.dataset.mengsanLocalArt; + delete card.dataset.mengsanLongName; + } let entry = entries.get(card); if (!entry) { const cost = document.createElement("span"); @@ -58,6 +75,8 @@ export function createHandOverlays(document, cardCost) { entry.upgrade.remove(); entry.border?.remove(); entry.rarity.remove(); + delete card.dataset.mengsanLocalArt; + delete card.dataset.mengsanLongName; delete card.dataset.mengsanRarity; delete card.dataset.mengsanRarityLabel; for (const key of ["accent", "dark", "ink", "metal"]) card.style.removeProperty(`--ms-rarity-${key}`); diff --git a/mengsan/ui/hand/style.css b/mengsan/ui/hand/style.css index 13bb9dd..2dd8a45 100644 --- a/mengsan/ui/hand/style.css +++ b/mengsan/ui/hand/style.css @@ -187,12 +187,12 @@ body.mengsan-hand-ui-shuying :is( } } -/* 梦三本地竖幅立绘:刘备及四类共享牌统一使用完整卡面;只协调梦三手牌里的原生显示节点。 */ -.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [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_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip) { +/* 按实际卡牌定义标记本地完整立绘,不再维护牌名清单;只协调梦三手牌原生节点。 */ +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:not(.infohidden):not(.infoflip) { background-size: cover !important; background-position: center !important; } -.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [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_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:not(.infohidden):not(.infoflip) > :is(.name, .top-name) { display: block !important; top: auto !important; bottom: 8px !important; @@ -214,14 +214,14 @@ body.mengsan-hand-ui-shuying :is( z-index: 7 !important; pointer-events: none; } -.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [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_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) br { +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:not(.infohidden):not(.infoflip) > :is(.name, .top-name) br { display: none; } -/* 十周年同时保留竖排名称与 top-name 时,只显示后者。 */ -.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [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_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip):has(> .top-name) > .name { +/* 十周年名称有文字时优先使用;空节点不能遮掉原生名称。 */ +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:not(.infohidden):not(.infoflip):has(> .top-name:not(:empty)) > .name { display: none !important; } -.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [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_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) { +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) { top: 6px !important; left: 6px !important; right: auto !important; @@ -241,17 +241,17 @@ body.mengsan-hand-ui-shuying :is( pointer-events: none; } -.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [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_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]) > .image, -.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [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_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]) > .info:empty { +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"] > .image, +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"] > .info:empty { display: none !important; } -.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [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_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):is([data-suit="heart"], [data-suit="diamond"]):not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) { +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]: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_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [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_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip):has(> .mengsan-hand-upgrade-shuying) > .suit-num { + .mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:not(.infohidden):not(.infoflip):has(> .mengsan-hand-upgrade-shuying) > .suit-num { top: 4px !important; padding: 0 2px !important; font-size: 18px !important; @@ -259,17 +259,58 @@ body.mengsan-hand-ui-shuying :is( } } -/* 长牌名完整显示,不挤出卡面;只作用于梦三手牌。 */ -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_ic_ashen_strike"], [data-card-name="mengsan_ic_fight_me"], [data-card-name="mengsan_ic_demonic_shield"], [data-card-name="mengsan_ic_forgotten_ritual"], [data-card-name="mengsan_ic_burning_pact"], [data-card-name="mengsan_ic_second_wind"], [data-card-name="mengsan_ic_pacts_end"], [data-card-name="mengsan_ic_fiend_fire"], [data-card-name="mengsan_ic_impervious"], [data-card-name="mengsan_ic_not_yet"], [data-card-name="mengsan_ic_demon_form"], [data-card-name="mengsan_ic_pyre"], [data-card-name="mengsan_ic_crimson_mantle"], [data-card-name="mengsan_ic_flame_barrier"], [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) { +/* 本地完整卡面不绘制空标题或重复的花色信息条。 */ +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:not(.infohidden):not(.infoflip) > :is(.name, .top-name):empty { + display: none !important; +} +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:has(> .suit-num:not(:empty)) > .info:not(.gaintag) { + display: none !important; +} +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:not(.infohidden):not(.infoflip) > .suit-num > .suit { + font-size: inherit !important; + line-height: inherit; +} +/* 保留原生词缀内容,覆盖十周年 fullimage 的字号归零,避开费用和名称。 */ +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:not(.infohidden):not(.infoflip) > .gaintag.info:not(:empty) { + display: block !important; + top: auto !important; + bottom: 36px !important; + left: 31px !important; + right: 5px !important; + width: auto !important; + height: auto !important; + margin: 0 !important; + padding: 2px 3px !important; + box-sizing: border-box; + font-size: 12px !important; + line-height: 14px !important; + letter-spacing: 0 !important; + white-space: normal !important; + overflow-wrap: anywhere; + overflow: visible !important; + background: #231b17e6 !important; + color: #fff1cb !important; + text-shadow: none !important; + border: 1px solid #d8ba83 !important; + border-radius: 3px; + z-index: 6 !important; + pointer-events: none; +} +/* 长牌名由翻译长度自动标记,新增牌不需要再抄写 CSS 名单。 */ +.mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"][data-mengsan-long-name="true"]: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_ic_ashen_strike"], [data-card-name="mengsan_ic_fight_me"], [data-card-name="mengsan_ic_demonic_shield"], [data-card-name="mengsan_ic_forgotten_ritual"], [data-card-name="mengsan_ic_burning_pact"], [data-card-name="mengsan_ic_second_wind"], [data-card-name="mengsan_ic_pacts_end"], [data-card-name="mengsan_ic_fiend_fire"], [data-card-name="mengsan_ic_impervious"], [data-card-name="mengsan_ic_not_yet"], [data-card-name="mengsan_ic_demon_form"], [data-card-name="mengsan_ic_pyre"], [data-card-name="mengsan_ic_crimson_mantle"], [data-card-name="mengsan_ic_flame_barrier"], [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) { + .mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"][data-mengsan-long-name="true"]:not(.infohidden):not(.infoflip) > :is(.name, .top-name) { font-size: 10px !important; padding: 2px 4px !important; } + .mengsan-hand-zone-shuying > .card[data-mengsan-local-art="full"]:not(.infohidden):not(.infoflip) > .gaintag.info:not(:empty) { + font-size: 10px !important; + line-height: 12px !important; + } } /* Infection only; keep cost/name above it and leave native card::after alone. */ @@ -332,130 +373,6 @@ body.mengsan-hand-ui-shuying :is( } } -/* 第二批刘备牌组长牌名,沿用现有手牌字号。 */ -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_ic_relentless_assault"], [data-card-name="mengsan_ic_howl_from_beyond"], [data-card-name="mengsan_ic_battle_trance"], [data-card-name="mengsan_ic_infernal_blade"], [data-card-name="mengsan_ic_eager"], [data-card-name="mengsan_ic_feel_no_pain"], [data-card-name="mengsan_ic_stone_armor"], [data-card-name="mengsan_ic_primal_force"], [data-card-name="mengsan_ic_battle_ready"], [data-card-name="mengsan_ic_hellraiser"], [data-card-name="mengsan_ic_dark_embrace"], [data-card-name="mengsan_ic_unshakable"], [data-card-name="mengsan_ic_juggernaut"], [data-card-name="mengsan_ic_rally"]):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_ic_relentless_assault"], [data-card-name="mengsan_ic_howl_from_beyond"], [data-card-name="mengsan_ic_battle_trance"], [data-card-name="mengsan_ic_infernal_blade"], [data-card-name="mengsan_ic_eager"], [data-card-name="mengsan_ic_feel_no_pain"], [data-card-name="mengsan_ic_stone_armor"], [data-card-name="mengsan_ic_primal_force"], [data-card-name="mengsan_ic_battle_ready"], [data-card-name="mengsan_ic_hellraiser"], [data-card-name="mengsan_ic_dark_embrace"], [data-card-name="mengsan_ic_unshakable"], [data-card-name="mengsan_ic_juggernaut"], [data-card-name="mengsan_ic_rally"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { - font-size: 10px !important; padding: 2px 4px !important; - } -} - - -/* 四类共享牌的长名称与刘备牌组采用相同字号,窄屏仍完整显示。 */ -.mengsan-hand-zone-shuying > .card:is( - [data-card-name="mengsan_colorless_dramatic_entrance"], - [data-card-name="mengsan_colorless_ultimate_strike"], - [data-card-name="mengsan_colorless_gang_up"], - [data-card-name="mengsan_colorless_seeker_strike"], - [data-card-name="mengsan_colorless_thrumming_hatchet"], - [data-card-name="mengsan_colorless_mind_blast"], - [data-card-name="mengsan_colorless_tag_team"], - [data-card-name="mengsan_colorless_dark_shackles"], - [data-card-name="mengsan_colorless_jack_of_all_trades"], - [data-card-name="mengsan_colorless_thinking_ahead"], - [data-card-name="mengsan_colorless_production"], - [data-card-name="mengsan_colorless_believe_in_you"], - [data-card-name="mengsan_colorless_restlessness"], - [data-card-name="mengsan_colorless_panic_button"], - [data-card-name="mengsan_colorless_ultimate_defend"], - [data-card-name="mengsan_colorless_coordinate"], - [data-card-name="mengsan_colorless_panache"], - [data-card-name="mengsan_colorless_prowess"], - [data-card-name="mengsan_colorless_prep_time"], - [data-card-name="mengsan_colorless_hand_of_greed"], - [data-card-name="mengsan_colorless_the_gambit"], - [data-card-name="mengsan_colorless_secret_technique"], - [data-card-name="mengsan_colorless_secret_weapon"], - [data-card-name="mengsan_colorless_master_of_strategy"], - [data-card-name="mengsan_colorless_alchemize"], - [data-card-name="mengsan_colorless_scrawl"], - [data-card-name="mengsan_colorless_hidden_gem"], - [data-card-name="mengsan_colorless_beacon_of_hope"], - [data-card-name="mengsan_colorless_eternal_armor"], - [data-card-name="mengsan_event_byrd_swoop"], - [data-card-name="mengsan_event_mad_science_sapping"], - [data-card-name="mengsan_event_mad_science_violence"], - [data-card-name="mengsan_event_mad_science_choking"], - [data-card-name="mengsan_event_feeding_frenzy"], - [data-card-name="mengsan_event_mad_science_energized"], - [data-card-name="mengsan_event_mad_science_wisdom"], - [data-card-name="mengsan_event_mad_science_chaos"], - [data-card-name="mengsan_event_toric_toughness"], - [data-card-name="mengsan_event_mad_science_expertise"], - [data-card-name="mengsan_event_mad_science_curious"], - [data-card-name="mengsan_event_mad_science_improvement"], - [data-card-name="mengsan_event_neows_fury"], - [data-card-name="mengsan_event_brightest_flame"], - [data-card-name="mengsan_status_frantic_escape"], - [data-card-name="mengsan_curse_spore_mind"], - [data-card-name="mengsan_curse_ascenders_bane"], - [data-card-name="mengsan_curse_curse_of_the_bell"], - [data-card-name="mengsan_curse_poor_sleep"], - [data-card-name="mengsan_status_deprecated_card"] -):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_colorless_dramatic_entrance"], - [data-card-name="mengsan_colorless_ultimate_strike"], - [data-card-name="mengsan_colorless_gang_up"], - [data-card-name="mengsan_colorless_seeker_strike"], - [data-card-name="mengsan_colorless_thrumming_hatchet"], - [data-card-name="mengsan_colorless_mind_blast"], - [data-card-name="mengsan_colorless_tag_team"], - [data-card-name="mengsan_colorless_dark_shackles"], - [data-card-name="mengsan_colorless_jack_of_all_trades"], - [data-card-name="mengsan_colorless_thinking_ahead"], - [data-card-name="mengsan_colorless_production"], - [data-card-name="mengsan_colorless_believe_in_you"], - [data-card-name="mengsan_colorless_restlessness"], - [data-card-name="mengsan_colorless_panic_button"], - [data-card-name="mengsan_colorless_ultimate_defend"], - [data-card-name="mengsan_colorless_coordinate"], - [data-card-name="mengsan_colorless_panache"], - [data-card-name="mengsan_colorless_prowess"], - [data-card-name="mengsan_colorless_prep_time"], - [data-card-name="mengsan_colorless_hand_of_greed"], - [data-card-name="mengsan_colorless_the_gambit"], - [data-card-name="mengsan_colorless_secret_technique"], - [data-card-name="mengsan_colorless_secret_weapon"], - [data-card-name="mengsan_colorless_master_of_strategy"], - [data-card-name="mengsan_colorless_alchemize"], - [data-card-name="mengsan_colorless_scrawl"], - [data-card-name="mengsan_colorless_hidden_gem"], - [data-card-name="mengsan_colorless_beacon_of_hope"], - [data-card-name="mengsan_colorless_eternal_armor"], - [data-card-name="mengsan_event_byrd_swoop"], - [data-card-name="mengsan_event_mad_science_sapping"], - [data-card-name="mengsan_event_mad_science_violence"], - [data-card-name="mengsan_event_mad_science_choking"], - [data-card-name="mengsan_event_feeding_frenzy"], - [data-card-name="mengsan_event_mad_science_energized"], - [data-card-name="mengsan_event_mad_science_wisdom"], - [data-card-name="mengsan_event_mad_science_chaos"], - [data-card-name="mengsan_event_toric_toughness"], - [data-card-name="mengsan_event_mad_science_expertise"], - [data-card-name="mengsan_event_mad_science_curious"], - [data-card-name="mengsan_event_mad_science_improvement"], - [data-card-name="mengsan_event_neows_fury"], - [data-card-name="mengsan_event_brightest_flame"], - [data-card-name="mengsan_status_frantic_escape"], - [data-card-name="mengsan_curse_spore_mind"], - [data-card-name="mengsan_curse_ascenders_bane"], - [data-card-name="mengsan_curse_curse_of_the_bell"], - [data-card-name="mengsan_curse_poor_sleep"], - [data-card-name="mengsan_status_deprecated_card"] - ):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { - font-size: 10px !important; - padding: 2px 4px !important; - } -} - /* 稀有度是独立装饰层,不占用原生 card::after,也不改变尺寸、命中区或立绘。 */ .mengsan-hand-rarity-shuying { display: none; pointer-events: none; } .mengsan-hand-zone-shuying > .card[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]):not(.infohidden):not(.infoflip) > .mengsan-hand-rarity-shuying { diff --git a/mengsan/ui/map/renderer.js b/mengsan/ui/map/renderer.js index 3aadbad..a7ae0b9 100644 --- a/mengsan/ui/map/renderer.js +++ b/mengsan/ui/map/renderer.js @@ -1,5 +1,5 @@ import { openCardLibrary } from "../card-library.js"; -import { askMenu, openModeSelection } from "../navigation.js"; +import { askMenu, openModeSelection, openNonameMenu } from "../navigation.js"; import { ui, get } from "../../../../../noname.js"; import config from "../../config.js"; import { getRelic } from "../../relics/definitions.js"; @@ -317,6 +317,17 @@ export const showMap = (run, { saveRun } = {}) => new Promise(resolve => { }); }) }); createMenuAction({ icon: "settings", name: "设置与说明", description: "显示、音效与玩法说明 · 开发中", onClick: () => showMenuPage("设置与说明", "个性化与帮助", parent => renderPlaceholder(parent, "后续将在这里提供显示、音效和操作设置。")) }); + createMenuAction({ icon: "settings", name: "显示 Noname 菜单", description: "打开无名杀原生菜单,管理扩展与游戏设置", + onClick: async event => { + event.stopPropagation(); + setMenuOpen(false); + try { await openNonameMenu(); } + catch (error) { + console.error("梦三打开 Noname 菜单失败:", error); + await askMenu("暂时无法显示菜单", error.message || "请稍后重试。", [{ id: "close", name: "回到行军图" }]); + } + }, + }).setAttribute("aria-haspopup", "dialog"); } function handleEscape(event) { diff --git a/mengsan/ui/navigation.js b/mengsan/ui/navigation.js index c423f48..dceff32 100644 --- a/mengsan/ui/navigation.js +++ b/mengsan/ui/navigation.js @@ -1,7 +1,65 @@ -import { lib, game, ui } from "../../../../noname.js"; +import { lib, game, ui, _status } from "../../../../noname.js"; import { save } from "../../../../noname/util/config.js"; let selectingMode = false; +let nonameMenuTask = null; + +// Open the engine's real settings menu without ending the current expedition flow. +export const openNonameMenu = async () => { + if (nonameMenuTask) return nonameMenuTask; + const container = ui.menuContainer; + if (!container?.isConnected || typeof ui.click?.config !== "function" + || typeof ui.click?.configMenu !== "function") { + throw new Error("Noname 菜单尚未加载,请稍后重试。"); + } + nonameMenuTask = new Promise((resolve, reject) => { + const previousFocus = document.activeElement; + const initiallyHidden = container.classList.contains("hidden"); + const wasPaused = _status.paused2; + const layerClass = "mengsan-noname-menu-open-shuying"; + const hadLayerClass = ui.window.classList.contains(layerClass); + const previousTabindex = container.getAttribute("tabindex"); + const overlays = Array.from(ui.window.querySelectorAll(".mengsan-overlay-shuying")) + .map(node => ({ node, inert: node.inert })); + let closed = false; + const finish = error => { + if (closed) return; + closed = true; + observer.disconnect(); + if (!hadLayerClass) ui.window.classList.remove(layerClass); + for (const { node, inert } of overlays) node.inert = inert; + if (previousTabindex == null) container.removeAttribute("tabindex"); + else container.setAttribute("tabindex", previousTabindex); + if (previousFocus?.isConnected) previousFocus.focus?.({ preventScroll: true }); + if (error) reject(error); + else resolve(); + }; + const observer = new MutationObserver(() => { + if (!container.isConnected || container.classList.contains("hidden")) finish(); + }); + try { + observer.observe(container, { attributes: true, attributeFilter: ["class"] }); + observer.observe(ui.window, { childList: true }); + ui.window.classList.add(layerClass); + for (const { node } of overlays) node.inert = true; + if (initiallyHidden) ui.click.config(); + if (container.classList.contains("hidden")) throw new Error("Noname 菜单未能打开,请稍后重试。"); + container.setAttribute("tabindex", "-1"); + container.focus({ preventScroll: true }); + } catch (error) { + if (initiallyHidden) { + try { + if (!container.classList.contains("hidden")) ui.click.configMenu(); + } catch (closeError) { console.error("Noname 菜单关闭失败:", closeError); } + if (wasPaused) game.pause2(); + else game.resume2(); + } + finish(error); + } + }); + try { await nonameMenuTask; } + finally { nonameMenuTask = null; } +}; // Reuse the registered engine splash, not reload-to-the-current-mode or a fake mode list. export const openModeSelection = async () => { @@ -96,11 +154,14 @@ export const mountMenu = (overlay, options = {}) => { if (opened) return; opened = true; options.onOpen?.(); + let action = null; try { - const action = await askMenu("征程菜单", "退出不删除存档。未完成的节点与对话将在下次进入时重新开始。", [ + action = await askMenu("征程菜单", "退出不删除存档。未完成的节点与对话将在下次进入时重新开始。", [ { id: "resume", name: "继续当前界面" }, + { id: "noname", name: "显示 Noname 菜单" }, { id: "exit", name: "返回模式选择" }, ]); + if (action === "noname") { await openNonameMenu(); return; } if (action !== "exit") return; const confirmed = await askMenu("返回模式选择?", "保留上次已完成节点的存档。本次尚未完成的内容不会提交。", [ { id: "cancel", name: "留在梦三" }, @@ -108,8 +169,10 @@ export const mountMenu = (overlay, options = {}) => { ]); if (confirmed === "confirm") await openModeSelection(); } catch (error) { - console.error("梦三返回模式选择失败:", error); - await askMenu("暂时无法返回", "没有清除征程。请关闭提示后重试。", [{ id: "close", name: "回到当前界面" }]); + console.error("梦三菜单操作失败:", error); + await askMenu(action === "noname" ? "暂时无法显示菜单" : "暂时无法返回", + action === "noname" ? error.message || "请关闭提示后重试。" : "没有清除征程。请关闭提示后重试。", + [{ id: "close", name: "回到当前界面" }]); } finally { opened = false; options.onClose?.(); diff --git a/mengsan/ui/style.css b/mengsan/ui/style.css index c4baa14..c59ca66 100644 --- a/mengsan/ui/style.css +++ b/mengsan/ui/style.css @@ -1,3 +1,7 @@ +/* Only elevate the engine menu and its dropdowns while opened from Mengsan. */ +.mengsan-noname-menu-open-shuying > .menu-container { z-index: 120 !important; } +.mengsan-noname-menu-open-shuying > .popup-container { z-index: 121 !important; } + .mengsan-overlay-shuying { position: fixed; inset: 0; diff --git a/menu.js b/menu.js index 166047b..62396a5 100644 --- a/menu.js +++ b/menu.js @@ -206,22 +206,38 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) { nopointer: true, }; - const channelText = channel => `更新通道:${channel == "preview" ? "测试版" : "正式版"}(点击切换)`; + const channelText = channel => `更新通道:${channel == "preview" ? "测试版" : "正式版"}(点击选择)`; menu.update_channel = { name: channelText(updater.getUpdateChannel()), clear: true, - intro: "切换后立即从当前下载源的对应标签下载并校验文件,完成后需重启游戏。测试版可能回退到较早的内容。", + intro: "打开选择窗口,确认后下载所选通道的更新,完成后需重启游戏。", async onclick() { if (!shuYing.m_bIsDownload) { - alert("有其他文件正在下载,请稍后再试吧。"); + alert("正在处理其他操作,请稍后再试。"); return; } const current = updater.getUpdateChannel(); - const next = current == "preview" ? "stable" : "preview"; shuYing.m_bIsDownload = false; - this.innerHTML = `正在切换到${next == "preview" ? "测试版" : "正式版"}…`; try { + const next = await shuYing.chooseSingleOptionDialog({ + title: "选择更新通道", + intro: "确认后会下载所选通道的更新,完成后需重启游戏。测试版的版本可能早于当前正式版。", + defaultKey: current, currentKey: current, + cancelable: true, variant: "settings", confirmText: "切换并更新", + options: [ + { + key: "stable", name: "正式版", tag: current == "stable" ? "当前使用" : "日常游玩", + description: "接收正式发布的版本,适合日常游玩。" + }, + { + key: "preview", name: "测试版", tag: current == "preview" ? "当前使用" : "提前体验", + description: "提前体验新内容,可能包含尚未完善的功能。" + }, + ], + }); + if (!next || next == current) return; + this.innerHTML = `正在切换到${next == "preview" ? "测试版" : "正式版"}…`; const version = await updater.checkVersion(shuYing, { channel: next, switchChannel: true, @@ -235,29 +251,53 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) { const text = channelText(updater.getUpdateChannel()); menu.update_channel.name = text; this.innerHTML = text; + shuYing.m_bIsDownload = true; } }, }; - const sourceText = source => `下载源:${source == "github" ? "GitHub" : "Gitea"}(点击切换)`; + const sourceText = source => `下载源:${source == "github" ? "GitHub" : "Gitea"}(点击选择)`; menu.update_source = { name: sourceText(updater.getUpdateSource()), clear: true, - intro: "选择从 GitHub 或 Gitea 获取标签与文件;切换后刷新在线版本,实际下载由版本检测触发。", - onclick() { + intro: "打开下载源选择窗口;确认后刷新在线版本,不会立即下载文件。", + async onclick() { if (!shuYing.m_bIsDownload) { - alert("有其他文件正在下载,请稍后再试吧。"); + alert("正在处理其他操作,请稍后再试。"); return; } - const next = updater.getUpdateSource() == "github" ? "gitea" : "github"; - game.saveConfig("shuYing_update_source", next); - const text = sourceText(next); - menu.update_source.name = text; - this.innerHTML = text; - game.saveConfig("shuYing_online_version", null); - shuYing.updateOnlineVersionMenu(null, "检测中..."); - shuYing.getVersion(); + const current = updater.getUpdateSource(); + shuYing.m_bIsDownload = false; + try { + const next = await shuYing.chooseSingleOptionDialog({ + title: "选择下载源", + intro: "确认后保存下载源并刷新在线版本,不会立即下载文件。需要更新时,请点击“版本检测”。", + defaultKey: current, currentKey: current, + cancelable: true, variant: "settings", confirmText: "保存下载源", + options: [ + { + key: "gitea", name: "Gitea", tag: current == "gitea" ? "当前使用" : "默认下载源", + description: "从 Gitea 获取版本信息和更新文件(国内源,下载速度上限1.4M)" + }, + { + key: "github", name: "GitHub", tag: current == "github" ? "当前使用" : "可选下载源", + description: "从 GitHub 获取版本信息和更新文件。" + }, + ], + }); + if (!next || next == current) return; + game.saveConfig("shuYing_update_source", next); + game.saveConfig("shuYing_online_version", null); + shuYing.updateOnlineVersionMenu(null, "检测中..."); + void shuYing.getVersion(); + } + finally { + const text = sourceText(updater.getUpdateSource()); + menu.update_source.name = text; + this.innerHTML = text; + shuYing.m_bIsDownload = true; + } }, }; @@ -343,18 +383,63 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) { }, }; + const mengsanLabelId = "shuying_mengsan_module_status"; + const mengsanStates = { + checking: ["检测中", "请稍候"], + installed: ["已安装", "点击卸载"], + disabled: ["未安装", "点击安装"], + installing: ["安装未完成", "点击修复"], + repair: ["待修复", "点击修复"], + uninstalling: ["卸载未完成", "继续卸载"], + unknown: ["状态未确认", "点击检查"], + working: ["处理中", "请稍候"], + }; + const mengsanLabelHTML = status => { + const [label, action] = mengsanStates[status] || mengsanStates.unknown; + return `梦三模式:${label}(${action})`; + }; + let mengsanLabelNode = null; + let mengsanStatusRequest = 0; + let mengsanModuleOperating = false; + const renderMengsanStatus = status => { + const content = mengsanLabelHTML(status); + menu.mengsanModule.name = `${content}`; + // Keep the existing node so closing the menu during a download still refreshes it. + mengsanLabelNode = document.getElementById(mengsanLabelId) || mengsanLabelNode; + if (mengsanLabelNode) mengsanLabelNode.innerHTML = content; + }; + shuYing.updateMengsanModuleMenu = async () => { + const request = ++mengsanStatusRequest; + mengsanLabelNode = document.getElementById(mengsanLabelId) || mengsanLabelNode; + if (mengsanModuleOperating) { renderMengsanStatus("working"); return; } + let status; + try { status = await updater.getMengsanModuleStatus(); } + catch (error) { + console.error("读取梦三安装状态失败:", error); + status = "unknown"; + } + if (request == mengsanStatusRequest) renderMengsanStatus(status); + }; menu.mengsanModule = { - name: "梦三模式(按需安装/卸载)", + name: `${mengsanLabelHTML("checking")}`, clear: true, - onclick() { - if (shuYing.m_bIsDownload) { - shuYing.m_bIsDownload = false; - updater.manageMengsanModule(shuYing); - } else { + async onclick() { + if (!shuYing.m_bIsDownload) { alert("有其他文件正在下载,请稍后再试吧。"); + return; + } + shuYing.m_bIsDownload = false; + mengsanModuleOperating = true; + ++mengsanStatusRequest; // Discard any pre-operation status lookup still in flight. + renderMengsanStatus("working"); + try { await updater.manageMengsanModule(shuYing); } + finally { + mengsanModuleOperating = false; + await shuYing.updateMengsanModuleMenu(); } }, }; + void shuYing.updateMengsanModuleMenu(); menu.repairBug = { name: "本地资源修复", diff --git a/update.js b/update.js index 9e0e33e..b0604f7 100644 --- a/update.js +++ b/update.js @@ -40,6 +40,7 @@ function beginUpdate(shuYing, full = false) { shuYing._progressRendered = 0; shuYing._updateStarted = Date.now(); shuYing._updateStats = { cached: 0, hashed: 0 }; + void shuYing.updateMengsanModuleMenu?.(); } function relativeTarget(target) { @@ -154,6 +155,7 @@ async function finishUpdate(shuYing) { addLog(shuYing, `本次耗时 ${((Date.now() - shuYing._updateStarted) / 1000).toFixed(1)} 秒;缓存通过 ${stats?.cached || 0} 次,完整校验通过 ${stats?.hashed || 0} 次`); } await flushLog(shuYing); + await shuYing.updateMengsanModuleMenu?.(); } function parseVersion(version) { @@ -1097,6 +1099,28 @@ async function saveMengsanRecord(record) { await saveUpdaterConfig({ [mengsanRecordKey]: record, [mengsanVersionKey]: version }); } +async function getMengsanModuleStatus() { + const record = getMengsanRecord(); + if (record) return record.status; + const version = lib.config[mengsanVersionKey]; + if (version) return version == "disabled" ? "disabled" : "installed"; + + // Legacy ZIP installations may have no receipt; inspect only the local entry. + const entry = "extension/术樱包/mengsan/register.js"; + if (lib.node?.fs || hasCordovaFiles() || typeof game.checkFile == "function") { + const exists = await checkFile(entry); + return exists == 1 ? "installed" : exists == -1 ? "disabled" : "unknown"; + } + if (typeof game.readFile == "function") { + try { await readFile(entry); return "installed"; } + catch (error) { + if (error?.code == "ENOENT" || error?.code == 1) return "disabled"; + throw error; + } + } + return "unknown"; +} + async function saveUpdaterConfig(values) { // Publish only after both the IndexedDB transaction and its requests commit. lib.status.reload++; @@ -1712,4 +1736,5 @@ export default { repairMissingFiles, repairCoreFiles, manageMengsanModule, + getMengsanModuleStatus, };