mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-18 13:20:02 +08:00
fixed # 新增羁绊系统
fixed # 新增遗物系统 fixed # 梦三模式下移除护甲上限限制
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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}。另可选择一项战利品。`
|
||||
: "此役已定,可选择一项战利品,也可放弃本次选择。",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user