fixed # 新增羁绊系统

fixed # 新增遗物系统
fixed # 梦三模式下移除护甲上限限制
This commit is contained in:
2026-09-30 14:33:17 +08:00
parent f2da9bce65
commit 82d80397b6
45 changed files with 2403 additions and 130 deletions
+91
View File
@@ -0,0 +1,91 @@
import { bondDefinitions, getBondCombat, getBondDeck }
from "../bonds/definitions.js";
import { getBondIntents } from "../bonds/intents.js";
import { bondProbability, ensureBonds, selectBond } from "../bonds/state.js";
export function renderBonds(parent, run, saveRun, onBusy = () => {}) {
const bonds = ensureBonds(run);
const root = document.createElement("div");
root.className = "mengsan-bonds-shuying";
parent.appendChild(root);
const text = (tag, content, target = root) => {
const node = document.createElement(tag);
node.textContent = content;
target.appendChild(node);
return node;
};
const status = text("p", "");
status.setAttribute("role", "status");
const buttons = [];
let saving = false;
const choose = async id => {
if (saving) return;
saving = true;
onBusy(true);
const previous = bonds.selected;
for (const button of buttons) button.disabled = true;
status.textContent = "正在保存助战选择…";
try {
selectBond(run, id);
await saveRun(run);
const updated = renderBonds(parent, run, saveRun, onBusy);
root.replaceWith(updated);
updated.querySelector('button[aria-pressed="true"]')
?.focus({ preventScroll: true });
} catch (error) {
selectBond(run, previous);
status.textContent = "保存失败,请重试。";
console.error("梦三助战选择保存失败", error);
for (const button of buttons) button.disabled = false;
} finally {
saving = false;
onBusy(false);
}
};
const clear = text("button", "不指定助战");
clear.type = "button";
clear.setAttribute("aria-pressed", String(bonds.selected === null));
clear.addEventListener("click", () => choose(null));
buttons.push(clear);
if (!Object.keys(bonds.npcs).length) text("p", "尚未结识羁绊 NPC。");
for (const [id, npc] of Object.entries(bonds.npcs)) {
const definition = bondDefinitions[id];
if (!definition) continue;
const card = document.createElement("section");
card.className = "mengsan-bond-card-shuying";
root.appendChild(card);
text("h3", definition.name, card);
const ready = Boolean(getBondCombat(id, npc.level));
const state = npc.down ? "濒死 · 等待救助" :
ready ? "可助战" : "战斗配置待补";
text("p", `${state}${npc.down && !ready ?
" · 战斗配置待补" : ""}`, card);
text("p", `羁绊 ${npc.level} 级 · 助战概率 ${
(bondProbability(npc.level) * 100).toFixed(2)}%`, card);
const progress = text("progress", "", card);
progress.max = 100;
progress.value = npc.level === 10 ? 100 : npc.progress;
progress.setAttribute("aria-label", `${definition.name}升级进度`);
text("p", npc.level === 10 ? "羁绊已满级" :
`升级进度 ${npc.progress}% / 100%`, card);
const button = text("button", bonds.selected === id ?
"已指定助战" : "指定助战", card);
button.type = "button";
button.setAttribute("aria-pressed", String(bonds.selected === id));
button.addEventListener("click", () => choose(id));
buttons.push(button);
const combat = getBondCombat(id, npc.level);
text("p", combat ? `生命 ${combat.maxHp} · 初始手牌 ${combat.hand}` :
"生命、初始手牌及技能等待配置。", card);
text("p", `独立牌库 ${getBondDeck(definition).length} 张 · 意图 ${
getBondIntents(definition).map(move => move.name).join(" → ")}`,
card);
if (npc.down) {
const rescue = text("button", "救助道具待补充", card);
rescue.type = "button";
rescue.disabled = true;
}
}
text("p", "指定 NPC 后,每场战斗结束:到场 +50%,未到场 +25%。");
return root;
}
+14 -4
View File
@@ -1,5 +1,6 @@
import { lib, get, ui } from "../../../../noname.js";
import { AFFIX_INFO } from "../cards/affixes.js";
import { cardUpgradeLevel, cardUpgradeRule } from "../cards/upgrades.js";
const suits = { spade: "♠ 黑桃", heart: "♥ 红桃", club: "♣ 梅花", diamond: "♦ 方块" };
const types = { basic: "基本牌", trick: "锦囊牌", delay: "延时锦囊", equip: "装备牌" };
@@ -20,9 +21,12 @@ export function describeLibraryCard(card) {
number: ({ 1: "A", 11: "J", 12: "Q", 13: "K" })[card.number] || String(card.number ?? "—"),
nature: nature || "无属性", type: types[info?.type] || "未知类别",
red: card.suit === "heart" || card.suit === "diamond",
description: text(lib.translate[`${card.name}_info`] || "暂无卡牌介绍。"),
description: text(cardUpgradeRule(card.name) && info?.cardPrompt
? info.cardPrompt(card)
: lib.translate[`${card.name}_info`] || "暂无卡牌介绍。"),
affixes: (Array.isArray(card.affixes) ? card.affixes : []).filter(key => key !== "annihilate").map(key => affixes[key] || `词缀:${translate(key)}`),
upgrade: Number.isFinite(card.upgrade) ? card.upgrade : 0,
upgrade: cardUpgradeLevel(card),
upgradeLimit: cardUpgradeRule(card.name)?.maxLevel || 0,
available: Boolean(info),
};
}
@@ -49,13 +53,17 @@ function preview(card, details, parent) {
const art = element("div", "ms-deck-art", null, face);
element("span", "ms-deck-glyph", details.name, art);
const imageName = lib.card[card.name]?.cardimage || card.name;
const modeImage = lib.card[card.name]?.image;
const localArt = typeof modeImage === "string" &&
/^ext:术樱包\/mengsan\/assets\/cards\/[a-z0-9_]+\.png$/.test(modeImage)
? modeImage.replace(/^ext:/, "extension/") : null;
// Only mode-standard local art paths; unknown or missing art keeps the text fallback.
if (/^[a-zA-Z0-9_]+$/.test(imageName)) {
const image = element("img", "ms-deck-image", null, art);
image.alt = "";
image.loading = "lazy";
image.addEventListener("error", () => image.remove(), { once: true });
image.src = `${lib.assetURL || ""}image/card/${imageName}.png`;
image.src = `${lib.assetURL || ""}${localArt || `image/card/${imageName}.png`}`;
}
element("strong", "ms-deck-name", details.name, face);
element("span", "ms-deck-meta", `${details.type} · ${details.nature}`, face);
@@ -110,7 +118,9 @@ export function openCardLibrary(run, options = {}) {
element("h4", "", "卡牌介绍", copy);
element("p", "ms-deck-rules", details.description, copy);
if (!details.available) element("p", "ms-deck-warning", "该卡牌定义未加载;保留存档记录,不删除此牌。", copy);
if (details.upgrade) element("p", "", `强化等级:${details.upgrade}(实际效果以玩法规则为准)`, copy);
element("p", "", details.upgradeLimit
? `强化:${details.upgrade}/${details.upgradeLimit}`
: "此牌不可强化", copy);
for (const affix of details.affixes) element("p", "ms-deck-rules", affix, copy);
body.scrollTop = 0; backButton.focus();
}
+38
View File
@@ -0,0 +1,38 @@
import { cardUpgradeLevel } from "../cards/upgrades.js";
export function createHandUpgradeBadges(document) {
const badges = new Map();
return {
refresh(card) {
if (!card?.name) return;
const level = cardUpgradeLevel(card);
let badge = badges.get(card) || Array.from(card.children || [])
.find(child => child.classList?.contains(
"mengsan-hand-upgrade-shuying"));
if (!level) {
badge?.remove();
badges.delete(card);
delete card.dataset.mengsanUpgrade;
return;
}
if (!badge) {
badge = document.createElement("span");
badge.className = "mengsan-hand-upgrade-shuying";
badge.setAttribute("role", "img");
}
if (badge.parentNode !== card) card.appendChild(badge);
badge.textContent = "";
badge.dataset.mengsanUpgrade = String(level);
badge.setAttribute("aria-label", `已强化${level}次`);
card.dataset.mengsanUpgrade = String(level);
badges.set(card, badge);
},
dispose() {
for (const [card, badge] of badges) {
badge.remove();
delete card.dataset.mengsanUpgrade;
}
badges.clear();
},
};
}
+11 -4
View File
@@ -114,11 +114,17 @@ export function mountEnemyIntent(player, session, assetBase, document,
if (deathBlow && hits !== 1) {
throw new RangeError("濒死一击只能造成一次伤害");
}
const damage = previewAttackDamage(intent, player, getTarget());
const target = getTarget();
const targets = Array.isArray(target) ? target : [target];
const damages = (targets.length ? targets : [null]).map(
current => previewAttackDamage(intent, player, current));
const damage = Math.max(...damages);
const minimum = Math.min(...damages);
const range = minimum === damage ? String(damage) :
`${minimum}~${damage}`;
const tier = deathBlow ? null :
attackIconForDamage(damage * hits);
const damageText = hits > 1 ? `${damage}×${hits}` :
String(damage);
const damageText = hits > 1 ? `${range}×${hits}` : range;
if (amount.textContent !== damageText) {
amount.textContent = damageText;
}
@@ -126,7 +132,7 @@ export function mountEnemyIntent(player, session, assetBase, document,
`${assetBase}/assets/intent/Intent_death_blow.png` :
`${assetBase}/assets/intent/Intent_attack_${tier}.png`;
if (icon.getAttribute("src") !== src) icon.src = src;
effects.push(`${deathBlow ? "Death Blow" : "Attack"}: ${damage} damage${hits > 1 ? ` × ${hits} hits` : ""}`);
effects.push(`${deathBlow ? "Death Blow" : "Attack"}: ${range} damage${hits > 1 ? ` × ${hits} hits` : ""}`);
}
if (hasDebuff) {
const label = intent.debuff === "脆弱" ? "Frail" :
@@ -143,6 +149,7 @@ export function mountEnemyIntent(player, session, assetBase, document,
effects.push("Unknown");
}
const label = effects.join(", ");
badge.title = "攻击与负面效果作用于所有存活敌对角色;伤害按各目标状态计算,护甲另行抵扣。";
if (badge.getAttribute("aria-label") !== label) badge.setAttribute("aria-label", label);
badge.hidden = false;
};
+24 -8
View File
@@ -2,6 +2,8 @@ import { openCardLibrary } from "../card-library.js";
import { askMenu, openModeSelection } from "../navigation.js";
import { ui, get } from "../../../../../noname.js";
import config from "../../config.js";
import { getRelic } from "../../relics/definitions.js";
import { renderBonds } from "../bonds.js";
import { getSelectableNodes } from "../../progression/state.js";
// Original vector pictograms; no external image/font requests.
@@ -88,15 +90,16 @@ const getTraversedEdgeIds = map => {
return traversed;
};
export const showMap = run => new Promise(resolve => {
export const showMap = (run, { saveRun } = {}) => new Promise(resolve => {
const act = config.acts[run.actIndex];
const selectable = getSelectableNodes(run);
const selectableIds = new Set(selectable.map(node => node.id));
const traversedEdgeIds = getTraversedEdgeIds(run.map);
const { overlay, panel, header } = createMapOverlay(act.name);
let closeCardLibrary = null;
let savingBonds = false;
const finish = result => {
if (overlay.dataset.resolved) return;
if (overlay.dataset.resolved || savingBonds) return;
overlay.dataset.resolved = "true";
document.removeEventListener("keydown", handleEscape);
closeCardLibrary?.();
@@ -173,7 +176,7 @@ export const showMap = run => new Promise(resolve => {
exitCopy.append(exitName, exitDescription);
exitButton.appendChild(exitCopy);
exitButton.addEventListener("click", async () => {
if (exitButton.disabled) return;
if (exitButton.disabled || savingBonds) return;
exitButton.disabled = true;
try {
const result = await askMenu("返回模式选择?", "保留上次完成节点的存档,未完成内容不会提交。", [
@@ -189,6 +192,7 @@ export const showMap = run => new Promise(resolve => {
menuFooter.appendChild(exitButton);
const setMenuOpen = open => {
if (savingBonds) return;
if (open) renderMainMenu();
menuLayer.hidden = !open;
menuTrigger.setAttribute("aria-expanded", String(open));
@@ -214,7 +218,7 @@ export const showMap = run => new Promise(resolve => {
return button;
};
const showMenuPage = (title, subtitle, render) => {
const showMenuPage = (title, subtitle, render, focusSelector = null) => {
menuTitle.textContent = title;
menuContent.replaceChildren();
const intro = ui.create.div(".mengsan-map-menu-page-intro-shuying", menuContent);
@@ -229,7 +233,11 @@ export const showMap = run => new Promise(resolve => {
back.appendChild(label);
back.addEventListener("click", renderMainMenu);
menuContent.appendChild(back);
requestAnimationFrame(() => back.focus());
requestAnimationFrame(() => {
const target = focusSelector ?
menuContent.querySelector(focusSelector) : back;
target?.focus({ preventScroll: Boolean(focusSelector) });
});
};
const renderBackpack = parent => {
@@ -260,8 +268,9 @@ export const showMap = run => new Promise(resolve => {
}
values.forEach(value => {
const row = ui.create.div(".mengsan-map-inventory-row-shuying", list);
createText("", get.translation(value) || value, row);
createText("", "已获得", row);
const relic = getRelic(value);
createText("", relic?.name || get.translation(value) || value, row);
createText("", relic?.description || "已获得", row);
});
};
const supports = run.player.supports || [];
@@ -273,7 +282,7 @@ export const showMap = run => new Promise(resolve => {
createText("", config.rewards[support.rewardId]?.name || get.translation(support.unit.character), row);
createText("", support.battles === -1 ? "本次征程持续生效" : `剩余 ${support.battles} 场`, row);
}
appendNames("永久道具", run.player.items, "尚未获得永久道具。");
appendNames("遗物(道具)", run.player.items, "尚未获得遗物。");
appendNames("永久技能", run.player.permanentSkills, "尚未获得永久技能。");
};
@@ -289,6 +298,13 @@ export const showMap = run => new Promise(resolve => {
menuContent.replaceChildren();
createMenuAction({ icon: "map", name: "继续行军", description: "关闭菜单并返回当前路线", onClick: () => setMenuOpen(false) });
createMenuAction({ icon: "bag", name: "行囊", description: `查看牌组、道具与永久技能 · ${run.player.deck.length}张牌`, onClick: () => showMenuPage("行囊", "本次征程携带的资源", renderBackpack) });
createMenuAction({ icon: "book", name: "羁绊", description: "查看羁绊等级、成长进度与助战角色", onClick: () => showMenuPage("羁绊", "本次征程的同行之谊 · 仅可指定一名助战", parent => renderBonds(parent, run, saveRun, busy => {
savingBonds = busy;
closeMenu.disabled = busy;
exitButton.disabled = busy;
const back = menuContent.querySelector(".mengsan-map-menu-back-shuying");
if (back) back.disabled = busy;
}), ".mengsan-bonds-shuying button") });
createMenuAction({ icon: "book", name: "图鉴", description: "查看已发现的敌人、事件与奖励 · 开发中", onClick: () => showMenuPage("图鉴", "征程见闻与收集记录", parent => renderPlaceholder(parent, "后续将收录已发现的节点、敌人和奖励。")) });
createMenuAction({ icon: "record", name: "征程记录", description: `已通过 ${run.statistics?.completedNodes || 0} 个节点`, onClick: () => showMenuPage("征程记录", "本次征程的阶段统计", parent => {
const list = ui.create.div(".mengsan-map-record-list-shuying", parent);
+81
View File
@@ -0,0 +1,81 @@
import { ui } from "../../../../noname.js";
import { heldRelics } from "../relics/definitions.js";
export function mountRelics(session, run, battleRelics) {
let dialog = null, list = null, previousFocus = null, disposed = false;
const relics = heldRelics(run);
const button = document.createElement("button");
button.type = "button";
button.className = "mengsan-battle-piles-button-shuying " +
"mengsan-battle-relics-button-shuying";
button.textContent = `遗物 ${relics.length}`;
button.setAttribute("aria-haspopup", "dialog");
ui.window.appendChild(button);
const close = () => {
if (!dialog) return;
const current = dialog;
dialog = null; list = null;
if (current.open) current.close();
current.remove();
if (previousFocus?.isConnected) previousFocus.focus();
};
const refresh = () => {
if (!list) return;
list.replaceChildren();
if (!relics.length) {
const empty = document.createElement("p");
empty.textContent = "本次征程尚未获得遗物。";
list.appendChild(empty);
}
for (const relic of relics) {
const row = document.createElement("section");
const name = document.createElement("h3");
name.textContent = relic.name;
const description = document.createElement("p");
description.textContent = relic.description;
const status = document.createElement("small");
status.textContent = battleRelics.status(relic);
row.append(name, description, status);
list.appendChild(row);
}
};
button.addEventListener("click", () => {
if (disposed || !session.active || dialog) return;
previousFocus = document.activeElement;
dialog = document.createElement("dialog");
const currentDialog = dialog;
dialog.className = "mengsan-deck-dialog-shuying " +
"mengsan-relic-dialog-shuying";
dialog.setAttribute("aria-label", "遗物(道具)");
const header = document.createElement("header");
header.className = "ms-deck-header";
const title = document.createElement("h2");
title.textContent = "遗物(道具)";
const dismiss = document.createElement("button");
dismiss.type = "button";
dismiss.className = "ms-deck-button";
dismiss.textContent = "关闭";
dismiss.addEventListener("click", close);
header.append(title, dismiss);
list = document.createElement("div");
list.className = "mengsan-relic-list-shuying";
dialog.append(header, list);
dialog.addEventListener("cancel", event => {
event.preventDefault(); close();
});
dialog.addEventListener("close", () => {
if (dialog === currentDialog) close();
});
dialog.addEventListener("keydown", event => event.stopPropagation());
refresh();
document.body.appendChild(dialog);
try { dialog.showModal(); dismiss.focus(); }
catch (error) { close(); throw error; }
});
const dispose = () => {
if (disposed) return;
disposed = true; close(); button.remove();
};
session.ownResource(button, dispose);
return { dispose, refresh };
}
+45 -8
View File
@@ -1,5 +1,6 @@
import { lib, get } from "../../../../noname.js";
import { cardCost } from "../battle/combat-rules.js";
import { cardUpgradeRule } from "../cards/upgrades.js";
import { chooseButtons } from "./flow-ui.js";
const element = (tag, className, text, parent) => {
@@ -23,11 +24,20 @@ const viewCard = choice => {
const name = cardNameOf(choice);
const info = lib.card[name];
const imageName = info?.cardimage || name;
const image = /^[a-zA-Z0-9_]+$/.test(imageName) ? `${lib.assetURL || ""}image/card/${imageName}.png` : null;
const modeImage = info?.image;
const localArt = typeof modeImage === "string" &&
/^ext:术樱包\/mengsan\/assets\/cards\/[a-z0-9_]+\.png$/.test(modeImage)
? modeImage.replace(/^ext:/, "extension/") : null;
const image = localArt ? `${lib.assetURL || ""}${localArt}` :
/^[a-zA-Z0-9_]+$/.test(imageName)
? `${lib.assetURL || ""}image/card/${imageName}.png` : null;
const type = { basic: "基本牌", trick: "锦囊牌", delay: "延时锦囊", equip: "装备牌" }[info?.type] || "卡牌";
return {
name: get.translation(name), type, image, cost: cardCost({ name }),
description: plainText(lib.translate[`${name}_info`] || choice.description || "暂无卡牌介绍。"),
description: plainText(cardUpgradeRule(name) && info?.cardPrompt
? info.cardPrompt(choice.card || { name })
: lib.translate[`${name}_info`] || choice.description ||
"暂无卡牌介绍。"),
};
};
@@ -94,7 +104,11 @@ export function chooseCardDialog(choices, { title = "选择一张牌", allowSkip
// Reusable post-battle choice surface. Each option owns its follow-up; the surface
// only resolves a stable candidate ID (or null) and never awards a reward itself.
export function chooseVictoryOptions(options, { title = "战后抉择", allowSkip = true } = {}) {
export function chooseVictoryOptions(options, {
title = "战后抉择", allowSkip = true,
description = "此役已定,选择下一段征途的收获。",
rewardLabel = "可选战利品",
} = {}) {
if (!Array.isArray(options) || !options.length || options.some(option => !option?.id || !option?.label)) {
return Promise.reject(new TypeError("chooseVictoryOptions requires nonempty options with IDs and labels"));
}
@@ -107,9 +121,9 @@ export function chooseVictoryOptions(options, { title = "战后抉择", allowSki
element("span", "mengsan-reward-overline-shuying", "梦三 · 战后", intro);
element("span", "mengsan-victory-seal-shuying", "胜", intro);
element("h2", "", title, intro);
element("p", "", "此役已定,选择下一段征途的收获。", intro);
element("p", "", description, intro);
const board = element("div", "mengsan-victory-options-shuying", null, stage);
element("span", "mengsan-victory-eyebrow-shuying", "可选战利品", board);
element("span", "mengsan-victory-eyebrow-shuying", rewardLabel, board);
let settled = false, choosing = false;
const finish = id => {
if (settled) return;
@@ -162,8 +176,26 @@ export function chooseVictoryOptions(options, { title = "战后抉择", allowSki
// Gold is a persisted candidate, not an automatic reward. The card pool remains
// a separate Dialog; skipping that Dialog returns to the victory choices.
export function chooseBattleReward(choices) {
if (!Array.isArray(choices) || !choices.length) return Promise.resolve(null);
export function chooseBattleReward(choices, { fixedRewards = [] } = {}) {
if (!Array.isArray(choices)) return Promise.resolve(null);
const fixedDescription = fixedRewards.map(reward =>
`${reward.name || reward.id}${reward.description
? `:${plainText(reward.description)}` : ""}`
).join(";");
if (!choices.length) {
if (!fixedRewards.length) return Promise.resolve(null);
return chooseVictoryOptions([{
id: "claim-fixed-rewards",
label: "领取固定奖励",
description: fixedDescription,
choose: () => null,
}], {
title: "过关奖励",
description: "此役已定,以下固定奖励将全部获得。",
rewardLabel: "固定战利品",
allowSkip: false,
});
}
const gold = choices.find(choice => choice.kind === "gold");
const rewards = choices.filter(choice => choice.kind !== "gold");
const options = [];
@@ -186,5 +218,10 @@ export function chooseBattleReward(choices) {
},
});
}
return chooseVictoryOptions(options);
return chooseVictoryOptions(options, {
title: "过关奖励",
description: fixedRewards.length
? `固定奖励:${fixedDescription}。另可选择一项战利品。`
: "此役已定,可选择一项战利品,也可放弃本次选择。",
});
}
+76
View File
@@ -1387,3 +1387,79 @@ body.mengsan-battle-ui-shuying :is(
.handcards > .card[data-mengsan-cost] > .mengsan-hand-cost-shuying::before {
content: attr(data-mengsan-cost);
}
.handcards > .card[data-mengsan-upgrade] > .mengsan-hand-upgrade-shuying {
position: absolute !important;
top: 4px !important;
right: 4px !important;
z-index: 8 !important;
display: grid !important;
place-items: center;
width: 26px;
height: 20px;
border: 1px solid #86bd85;
border-radius: 4px;
background: #214f36;
box-shadow: 0 1px 3px #0008;
color: #f5ffea;
font: 700 13px/1 "Microsoft YaHei", sans-serif;
pointer-events: none;
}
.handcards > .card[data-mengsan-upgrade] > .mengsan-hand-upgrade-shuying::before {
content: "+" attr(data-mengsan-upgrade);
}
.mengsan-battle-relics-button-shuying { top: 162px; }
.mengsan-relic-dialog-shuying { max-width: 620px; }
.mengsan-relic-list-shuying { overflow-y: auto; padding: 12px 0; }
.mengsan-relic-list-shuying section {
padding: 14px 0;
border-bottom: 1px solid #b9975d55;
}
.mengsan-relic-list-shuying h3 { margin: 0 0 8px; color: #ffe0a1; }
.mengsan-relic-list-shuying p { margin: 0 0 6px; line-height: 1.7; }
.mengsan-relic-list-shuying small { color: #d1bd9c; }
@media (max-height: 500px) {
.mengsan-battle-relics-button-shuying { top: 122px; }
}
.mengsan-bonds-shuying {
display: grid;
gap: 12px;
}
.mengsan-bond-card-shuying {
border: 1px solid rgba(116, 89, 56, .35);
border-radius: 10px;
padding: 16px;
overflow-wrap: anywhere;
}
.mengsan-bonds-shuying p {
margin: 8px 0;
line-height: 1.6;
}
.mengsan-bonds-shuying h3 {
margin: 0 0 8px;
}
.mengsan-bonds-shuying progress {
width: 100%;
accent-color: #8c6a34;
}
.mengsan-bonds-shuying button {
min-height: 44px;
padding: 8px 16px;
border: 1px solid currentColor;
border-radius: 6px;
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
}
.mengsan-bonds-shuying button[aria-pressed="true"] {
background: rgba(140, 106, 52, .18);
font-weight: bold;
}
.mengsan-bonds-shuying button:disabled {
opacity: .5;
cursor: not-allowed;
}
.mengsan-bonds-shuying button:focus-visible {
outline: 2px solid #8c6a34;
outline-offset: 3px;
}