mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-18 13:20:02 +08:00
fixed # 补全梦三怪物、卡牌
fixed # 修复天书乱斗中,过关技能不会重置次数的问题
This commit is contained in:
+101
-19
@@ -1,9 +1,13 @@
|
||||
import { lib, get, ui } from "../../../../noname.js";
|
||||
import { cardDefinitions } from "../cards/card-definitions.js";
|
||||
import { DAZED_NAME } from "../cards/status-cards.js";
|
||||
import { SLIMED_NAME } from "../cards/slimed-card.js";
|
||||
import { cardCost } from "../battle/combat-rules.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: "装备牌" };
|
||||
const types = { basic: "基本牌", trick: "锦囊牌", delay: "延时锦囊", equip: "装备牌", status: "状态牌" };
|
||||
const affixes = Object.fromEntries(Object.entries(AFFIX_INFO).map(([key, info]) => [key, `${info.name}:${info.description}`]));
|
||||
const text = value => {
|
||||
const template = document.createElement("template");
|
||||
@@ -31,6 +35,28 @@ export function describeLibraryCard(card) {
|
||||
};
|
||||
}
|
||||
|
||||
// 只读图鉴数据,不创建实体牌、伪造花色点数或写入永久牌组。
|
||||
export function getCardCatalog() {
|
||||
const states = Object.entries(lib.card).filter(([name, info]) =>
|
||||
/^mengsan_[a-z0-9_]+$/.test(name) && info?.type === "status" &&
|
||||
/^ext:术樱包\/mengsan\/assets\/cards\/[a-z0-9_]+\.png$/.test(info.image)).map(([name]) => name);
|
||||
return [...new Set([...Object.keys(cardDefinitions), DAZED_NAME, SLIMED_NAME, ...states])]
|
||||
.map(name => ({ name, nature: null, affixes: [], upgrade: 0 }));
|
||||
}
|
||||
|
||||
const categories = { damage: "攻击", utility: "辅助", recovery: "恢复", status: "状态" };
|
||||
function describeCatalogCard(card) {
|
||||
const details = describeLibraryCard(card), info = lib.card[card.name];
|
||||
const definition = cardDefinitions[card.name];
|
||||
const category = info?.type === "status" || card.name === DAZED_NAME || card.name === SLIMED_NAME
|
||||
? "status" : definition?.category || "utility";
|
||||
const unplayable = info?.enable === false;
|
||||
const cost = info && !unplayable ? cardCost(card) : null;
|
||||
return { ...details, category, categoryName: categories[category], cost, unplayable,
|
||||
costLabel: !info ? "未加载" : unplayable ? "不可打出" : `${cost}费`,
|
||||
exclusive: definition?.owner ? translate(definition.owner) : "" };
|
||||
}
|
||||
|
||||
function element(tag, className, value, parent) {
|
||||
const node = document.createElement(tag);
|
||||
node.className = className;
|
||||
@@ -47,9 +73,9 @@ function button(label, parent, action, className = "ms-deck-button") {
|
||||
}
|
||||
|
||||
// Read-only DOM previews, never game.createCard or Card.init: no IDs, triggers or pile writes.
|
||||
function preview(card, details, parent) {
|
||||
function preview(card, details, parent, catalog = false) {
|
||||
const face = element("div", "ms-deck-face", null, parent);
|
||||
element("span", `ms-deck-corner${details.red ? " ms-deck-red" : ""}`, `${details.suit} ${details.number}`, face);
|
||||
element("span", `ms-deck-corner${details.red ? " ms-deck-red" : ""}`, catalog ? details.costLabel : `${details.suit} ${details.number}`, face);
|
||||
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;
|
||||
@@ -66,9 +92,9 @@ function preview(card, details, parent) {
|
||||
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);
|
||||
element("span", "ms-deck-meta", catalog ? `${details.categoryName} · ${details.exclusive ? "专属:" + details.exclusive : details.category === "status" ? "战斗生成" : "通用"}` : `${details.type} · ${details.nature}`, face);
|
||||
if (details.upgrade || details.affixes.length) {
|
||||
element("span", "ms-deck-tags", [details.upgrade ? `强化 +${details.upgrade}` : "", ...details.affixes.map(value => value.split(":")[0])].filter(Boolean).join(" · "), face);
|
||||
element("span", "ms-deck-tags", [details.upgrade ? `+${details.upgrade}` : "", ...details.affixes.map(value => value.split(":")[0])].filter(Boolean).join(" · "), face);
|
||||
}
|
||||
return face;
|
||||
}
|
||||
@@ -92,7 +118,39 @@ export function openCardLibrary(run, options = {}) {
|
||||
if (previousFocus?.isConnected) previousFocus.focus();
|
||||
};
|
||||
button("关闭", header, close);
|
||||
let view = !options.sections && options.view === "catalog" ? "catalog" : "deck";
|
||||
const navigation = element("nav", "ms-deck-tabs", null, dialog);
|
||||
navigation.setAttribute("aria-label", "卡牌列表视图");
|
||||
navigation.hidden = Boolean(options.sections);
|
||||
const deckTab = button("持有牌", navigation, () => { view = "deck"; render(); });
|
||||
const catalogTab = button("图鉴", navigation, () => { view = "catalog"; render(); });
|
||||
const filters = element("div", "ms-catalog-filters", null, dialog);
|
||||
function field(label) {
|
||||
const wrapper = element("label", "ms-catalog-field", null, filters);
|
||||
element("span", "", label, wrapper); return wrapper;
|
||||
}
|
||||
const search = element("input", "ms-catalog-input", null, field("名称 / 效果"));
|
||||
search.type = "search"; search.maxLength = 120; search.placeholder = "搜索卡牌";
|
||||
search.setAttribute("aria-label", "搜索图鉴卡牌");
|
||||
const categoryFilter = element("select", "ms-catalog-select", null, field("类型"));
|
||||
categoryFilter.setAttribute("aria-label", "图鉴类型");
|
||||
for (const [key, label] of Object.entries({ "": "全部类型", ...categories })) {
|
||||
const option = element("option", "", label, categoryFilter); option.value = key;
|
||||
}
|
||||
const costFilter = element("select", "ms-catalog-select", null, field("费用"));
|
||||
costFilter.setAttribute("aria-label", "图鉴费用");
|
||||
for (const [key, label] of [["", "全部费用"], ["0", "0费"], ["1", "1费"], ["2", "2费"], ["3+", "3费及以上"], ["unplayable", "不可打出"]]) {
|
||||
const option = element("option", "", label, costFilter); option.value = key;
|
||||
}
|
||||
button("清空筛选", filters, () => {
|
||||
search.value = ""; categoryFilter.value = ""; costFilter.value = "";
|
||||
render(); search.focus();
|
||||
});
|
||||
search.addEventListener("input", render);
|
||||
categoryFilter.addEventListener("change", render);
|
||||
costFilter.addEventListener("change", render);
|
||||
const summary = element("p", "ms-deck-summary", null, dialog);
|
||||
summary.setAttribute("aria-live", "polite");
|
||||
const body = element("div", "ms-deck-body", null, dialog);
|
||||
const footer = element("p", "ms-deck-footer", "点击卡牌查看完整介绍 · 仅供查看,不改变牌组或抽牌顺序", dialog);
|
||||
const current = Array.isArray(run?.player?.deck) ? run.player.deck : [];
|
||||
@@ -105,40 +163,64 @@ export function openCardLibrary(run, options = {}) {
|
||||
detailPanel.replaceChildren(); body.scrollTop = scrollTop;
|
||||
selectedButton?.focus({ preventScroll: true });
|
||||
}
|
||||
function showDetail(card, details, owner) {
|
||||
function showDetail(card, details, owner, catalog = false) {
|
||||
selectedButton = owner; scrollTop = body.scrollTop;
|
||||
detail = true; list.hidden = true; detailPanel.hidden = false;
|
||||
detailPanel.replaceChildren();
|
||||
const backButton = button("返回牌库", detailPanel, back);
|
||||
const backButton = button(catalog ? "返回图鉴" : "返回牌库", detailPanel, back);
|
||||
const content = element("div", "ms-deck-detail-content", null, detailPanel);
|
||||
preview(card, details, content);
|
||||
preview(card, details, content, catalog);
|
||||
const copy = element("div", "ms-deck-detail-copy", null, content);
|
||||
element("h3", "", details.name, copy);
|
||||
element("p", "", `${details.suit} ${details.number} · ${details.type} · ${details.nature}`, copy);
|
||||
element("p", "", catalog ? `${details.costLabel} · ${details.categoryName} · ${details.type}` :
|
||||
`${details.suit} ${details.number} · ${details.type} · ${details.nature}`, copy);
|
||||
if (catalog && details.exclusive) element("p", "ms-deck-rules", `专属角色:${details.exclusive}`, copy);
|
||||
element("h4", "", "卡牌介绍", copy);
|
||||
element("p", "ms-deck-rules", details.description, copy);
|
||||
if (!details.available) element("p", "ms-deck-warning", "该卡牌定义未加载;保留存档记录,不删除此牌。", 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();
|
||||
}
|
||||
function render() {
|
||||
detail = false; list.hidden = false; detailPanel.hidden = true;
|
||||
detailPanel.replaceChildren(); list.replaceChildren(); body.scrollTop = 0;
|
||||
const sections = options.sections ? options.sections() : [{ title: "", cards: current }];
|
||||
summary.textContent = options.sections ? "剩余牌堆按从左到右、从上到下的顺序摸取;第 1 张为下一张。" : `本次征程实际携带 · 共 ${current.length} 张 · 同名不同花色、点数分别展示`;
|
||||
selectedButton = null;
|
||||
const catalog = !options.sections && view === "catalog";
|
||||
filters.hidden = !catalog;
|
||||
deckTab.setAttribute("aria-pressed", String(!catalog));
|
||||
catalogTab.setAttribute("aria-pressed", String(catalog));
|
||||
list.classList.toggle("ms-catalog-grid", catalog);
|
||||
let records = [];
|
||||
if (catalog) {
|
||||
const query = search.value.trim().toLocaleLowerCase();
|
||||
records = getCardCatalog().map(card => ({ card, details: describeCatalogCard(card) }));
|
||||
const total = records.length;
|
||||
records = records.filter(({ card, details }) =>
|
||||
(!query || `${details.name} ${card.name} ${details.description}`.toLocaleLowerCase().includes(query)) &&
|
||||
(!categoryFilter.value || details.category === categoryFilter.value) &&
|
||||
(!costFilter.value || (costFilter.value === "unplayable" ? details.unplayable :
|
||||
costFilter.value === "3+" ? details.cost !== null && details.cost >= 3 :
|
||||
details.cost !== null && details.cost === Number(costFilter.value))));
|
||||
summary.textContent = `梦三卡牌图鉴 · 共 ${total} 种 · 当前显示 ${records.length} 种`;
|
||||
} else {
|
||||
summary.textContent = options.sections ? "剩余牌堆按从左到右、从上到下的顺序摸取;第 1 张为下一张。" :
|
||||
`本次征程实际携带 · 共 ${current.length} 张 · 同名不同花色、点数分别展示`;
|
||||
}
|
||||
footer.textContent = catalog ? "图鉴仅供查阅,不会获得卡牌或改变征程牌组 · 点击卡牌查看完整介绍" :
|
||||
"点击卡牌查看完整介绍 · 仅供查看,不改变牌组或抽牌顺序";
|
||||
const sections = catalog ? [{ title: "", cards: records.map(record => record.card) }] :
|
||||
options.sections ? options.sections() : [{ title: "", cards: current }];
|
||||
for (const section of sections) {
|
||||
const cards = section.cards;
|
||||
if (section.title) element("h3", "ms-deck-section", `${section.title} · ${cards.length} 张`, list);
|
||||
if (!cards.length) element("p", "ms-deck-empty", section.ordered ? "剩余牌堆为空;下一次摸牌需按规则洗切弃牌堆,洗切后的顺序尚未确定。" : "此处暂无卡牌。", list);
|
||||
if (!cards.length) element("p", "ms-deck-empty", catalog ? "没有符合筛选的卡牌,可清空筛选后查看全部图鉴。" : section.ordered ? "剩余牌堆为空;下一次摸牌需按规则洗切弃牌堆,洗切后的顺序尚未确定。" : "此处暂无卡牌。", list);
|
||||
cards.forEach((card, index) => {
|
||||
const details = describeLibraryCard(card);
|
||||
const tile = button("", list, () => showDetail(card, details, tile), "ms-deck-card");
|
||||
tile.setAttribute("aria-label", `${section.ordered ? `第 ${index + 1} 张,` : ""}${details.name},${details.suit}${details.number},${details.nature},查看详情`);
|
||||
const details = catalog ? records[index].details : describeLibraryCard(card);
|
||||
const tile = button("", list, () => showDetail(card, details, tile, catalog), "ms-deck-card");
|
||||
tile.setAttribute("aria-label", catalog ? `${details.name},${details.categoryName},${details.costLabel},查看详情` :
|
||||
`${section.ordered ? `第 ${index + 1} 张,` : ""}${details.name},${details.suit}${details.number},${details.nature},查看详情`);
|
||||
if (section.ordered) element("span", "ms-deck-order", index === 0 ? "1 · 下一张" : `${index + 1}`, tile);
|
||||
preview(card, details, tile);
|
||||
preview(card, details, tile, catalog);
|
||||
element("span", "ms-deck-excerpt", details.description, tile);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// 只覆盖命名空间内的 CSS 变量,不移动或复制真实卡牌。
|
||||
export function fanSlots(count, width, cardWidth = 112, cardHeight = 156) {
|
||||
if (!Number.isSafeInteger(count) || count < 0 || !Number.isFinite(width) || width <= 0) return [];
|
||||
const step = count > 1 ? Math.min(cardWidth * 0.68,
|
||||
Math.max(0, width - cardWidth - 32) / (count - 1)) : 0;
|
||||
const half = (count - 1) / 2;
|
||||
const angleStep = count > 1 ? Math.min(4, 13 / half) : 0;
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const offset = index - half;
|
||||
return { x: width / 2 + offset * step - cardWidth / 2,
|
||||
y: -cardHeight + 28 + (half ? Math.pow(offset / half, 2) * 20 : 0),
|
||||
angle: offset * angleStep, z: index + 1 };
|
||||
});
|
||||
}
|
||||
|
||||
export function createHandFan(adapter, env) {
|
||||
const { window } = env;
|
||||
const owned = new Map();
|
||||
let enabled = false, hover = null, dragging = null;
|
||||
function variable(card, key, value) {
|
||||
let record = owned.get(card);
|
||||
if (!record) { record = new Map(); owned.set(card, record); }
|
||||
if (!record.has(key)) record.set(key, {
|
||||
before: card.style.getPropertyValue(key), priority: card.style.getPropertyPriority(key), last: null,
|
||||
});
|
||||
const entry = record.get(key);
|
||||
entry.last = value;
|
||||
if (card.style.getPropertyValue(key) !== value) card.style.setProperty(key, value);
|
||||
}
|
||||
function release(card) {
|
||||
for (const name of ["mengsan-hand-hover-shuying", "mengsan-hand-drag-shuying", "mengsan-hand-free-drag-shuying"]) card.classList.remove(name);
|
||||
for (const [key, record] of owned.get(card) || []) {
|
||||
if (card.style.getPropertyValue(key) !== record.last) continue;
|
||||
if (record.before) card.style.setProperty(key, record.before, record.priority);
|
||||
else card.style.removeProperty(key);
|
||||
}
|
||||
owned.delete(card);
|
||||
}
|
||||
function setHover(card) {
|
||||
if (hover === card) return;
|
||||
hover?.classList.remove("mengsan-hand-hover-shuying");
|
||||
hover = enabled && !dragging ? card : null;
|
||||
hover?.classList.add("mengsan-hand-hover-shuying");
|
||||
}
|
||||
function sync(active) {
|
||||
enabled = active;
|
||||
const cards = adapter.cards().filter(card => !card.classList.contains("display-none"));
|
||||
const current = new Set(cards);
|
||||
for (const card of owned.keys()) if (!active || !current.has(card)) release(card);
|
||||
if (!active) { setHover(null); return; }
|
||||
const small = window.innerWidth <= 640 || window.innerHeight <= 500;
|
||||
const width = adapter.containers[0]?.clientWidth || adapter.root.clientWidth;
|
||||
const slots = fanSlots(cards.length, width, small ? 82 : 112, small ? 114 : 156);
|
||||
cards.forEach((card, index) => {
|
||||
const slot = slots[index];
|
||||
if (!slot) return;
|
||||
variable(card, "--mengsan-fan-x", `${slot.x}px`);
|
||||
variable(card, "--mengsan-fan-y", `${slot.y}px`);
|
||||
variable(card, "--mengsan-fan-angle", `${slot.angle}deg`);
|
||||
variable(card, "--mengsan-fan-z", `${slot.z}`);
|
||||
});
|
||||
if (hover && !current.has(hover)) setHover(null);
|
||||
}
|
||||
function drag(card, point, targeted) {
|
||||
if (dragging && dragging !== card) stopDrag();
|
||||
setHover(null);
|
||||
dragging = card;
|
||||
card.classList.add("mengsan-hand-drag-shuying");
|
||||
card.classList.toggle("mengsan-hand-free-drag-shuying", !targeted);
|
||||
const zone = card.parentNode;
|
||||
const rect = zone.getBoundingClientRect();
|
||||
// CSS 像素相对于真实父容器:兼容 documentZoom/缩放,不能直接拿屏幕坐标当布局坐标。
|
||||
const scaleX = rect.width / zone.offsetWidth || 1;
|
||||
const scaleY = rect.height / zone.offsetHeight || scaleX;
|
||||
const rootRect = adapter.root.getBoundingClientRect();
|
||||
const small = window.innerWidth <= 640 || window.innerHeight <= 500;
|
||||
const width = small ? 82 : 112, height = small ? 114 : 156;
|
||||
const x = targeted ? rootRect.left + rootRect.width / 2 : point.x;
|
||||
const y = targeted ? rootRect.bottom - height * scaleY / 2 - 6 : point.y;
|
||||
variable(card, "--mengsan-drag-x", `${(x - rect.left) / scaleX - width / 2}px`);
|
||||
variable(card, "--mengsan-drag-y", `${(y - rect.top) / scaleY - height / 2}px`);
|
||||
}
|
||||
function stopDrag() {
|
||||
dragging?.classList.remove("mengsan-hand-drag-shuying", "mengsan-hand-free-drag-shuying");
|
||||
dragging = null;
|
||||
}
|
||||
return { sync, setHover, drag, stopDrag,
|
||||
dispose() { stopDrag(); setHover(null); for (const card of owned.keys()) release(card); } };
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import { createHandAdapter } from "./adapter.js";
|
||||
import { createHandOverlays } from "./overlays.js";
|
||||
import { createHandDiagnostics } from "./diagnostics.js";
|
||||
import { createHandFan } from "./fan.js";
|
||||
import { createHandInput } from "./input.js";
|
||||
|
||||
export async function mountHandUI(player, session, env) {
|
||||
const { ui, document, window, cardCost, styleURL } = env;
|
||||
const adapter = createHandAdapter(player, ui);
|
||||
const overlays = createHandOverlays(document, cardCost);
|
||||
const overlays = createHandOverlays(document, card => cardCost(card, player));
|
||||
const fan = createHandFan(adapter, env);
|
||||
let input = null;
|
||||
const restores = [];
|
||||
const diagnose = createHandDiagnostics(env.game, session.id);
|
||||
const style = document.createElement("link");
|
||||
@@ -58,6 +62,8 @@ export async function mountHandUI(player, session, env) {
|
||||
}
|
||||
const supported = adapter.supported();
|
||||
setLayout(supported && !nativeInput);
|
||||
fan.sync(supported && !nativeInput);
|
||||
input?.reconcile();
|
||||
if (!supported && !warned) {
|
||||
warned = true;
|
||||
log("当前容器不支持独立排列,已保留原排列与费用/强化标记");
|
||||
@@ -68,17 +74,35 @@ export async function mountHandUI(player, session, env) {
|
||||
target.addEventListener(type, listener, options);
|
||||
restores.push(() => target.removeEventListener(type, listener, options));
|
||||
}
|
||||
// 不可使用的真实手牌不能触发旧手势的横排回退;选中牌保留原生交互。
|
||||
function canBeginNativeInput(card) {
|
||||
if (!adapter.zones.includes(card?.parentNode) ||
|
||||
card.classList.contains("noclick") ||
|
||||
(!card.classList.contains("selectable") &&
|
||||
!card.classList.contains("selected"))) return false;
|
||||
const current = env._status?.event;
|
||||
return !(current?.name === "chooseToUse" && current.player === player &&
|
||||
env.isActiveCardUse?.(current, player) &&
|
||||
env.canPayCard?.(player, card) === false);
|
||||
}
|
||||
function beginInput(event) {
|
||||
if (input?.owns()) return;
|
||||
const card = event.target.closest?.(".card");
|
||||
if (event.button !== 0 || pointer ||
|
||||
!adapter.zones.includes(card?.parentNode)) {
|
||||
!canBeginNativeInput(card)) {
|
||||
return;
|
||||
}
|
||||
pointer = { id: event.pointerId, x: event.clientX, y: event.clientY,
|
||||
pointer = { card, id: event.pointerId, x: event.clientX, y: event.clientY,
|
||||
touch: event.pointerType === "touch" };
|
||||
}
|
||||
function moveInput(event) {
|
||||
if (!pointer || pointer.id !== event.pointerId || nativeInput) return;
|
||||
if (input?.owns()) return;
|
||||
if (!pointer || pointer.id !== event.pointerId) return;
|
||||
if (!canBeginNativeInput(pointer.card)) { endInput(event); return; }
|
||||
if (nativeInput) return;
|
||||
// 未接管的上拖(包括响应牌或已选牌)不属于手牌排序,不退回横排。
|
||||
const dx = event.clientX - pointer.x, dy = event.clientY - pointer.y;
|
||||
if (dy < 0 && Math.abs(dy) >= Math.abs(dx)) return;
|
||||
if (Math.hypot(event.clientX - pointer.x, event.clientY - pointer.y) < 8) {
|
||||
return;
|
||||
}
|
||||
@@ -131,6 +155,8 @@ export async function mountHandUI(player, session, env) {
|
||||
restoreTouchScroll();
|
||||
observer?.disconnect();
|
||||
resizeObserver?.disconnect();
|
||||
input?.dispose();
|
||||
fan.dispose();
|
||||
overlays.dispose();
|
||||
setLayout(false);
|
||||
for (const restore of restores.reverse()) restore();
|
||||
@@ -157,6 +183,8 @@ export async function mountHandUI(player, session, env) {
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
if (!session.active || disposed) { dispose(); return { refresh, dispose }; }
|
||||
ownClass(adapter.root, "mengsan-hand-root-shuying");
|
||||
ownClass(document.body, "mengsan-hand-ui-shuying");
|
||||
for (const zone of adapter.zones) {
|
||||
ownClass(zone, "mengsan-hand-zone-shuying");
|
||||
}
|
||||
@@ -164,7 +192,7 @@ export async function mountHandUI(player, session, env) {
|
||||
if (!container.hasAttribute("tabindex")) {
|
||||
ownAttribute(container, "tabindex", "0");
|
||||
}
|
||||
ownAttribute(container, "aria-label", "梦三手牌,可横向滚动");
|
||||
ownAttribute(container, "aria-label", "梦三扇形手牌,悬停查看;上拖出牌,拖向目标后松手确认");
|
||||
listen(container, "wheel", scrollWheel,
|
||||
{ capture: true, passive: false });
|
||||
listen(container, "touchmove", allowTouchScroll, true);
|
||||
@@ -173,8 +201,10 @@ export async function mountHandUI(player, session, env) {
|
||||
}
|
||||
observer = new window.MutationObserver(refresh);
|
||||
for (const zone of adapter.zones) {
|
||||
observer.observe(zone, { childList: true });
|
||||
observer.observe(zone, { childList: true, subtree: true,
|
||||
attributes: true, attributeFilter: ["class"] });
|
||||
}
|
||||
input = createHandInput(player, session, adapter, fan, env, refresh, log);
|
||||
if (adapter.root?.parentNode) {
|
||||
observer.observe(adapter.root.parentNode, { childList: true });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// 梦三局部输入适配:使用真实 ui.click/card/target 和 game.check 提交,不自行 useCard。
|
||||
export function createHandInput(player, session, adapter, fan, env, refresh, log) {
|
||||
const { ui, game, get, _status: status, document, window } = env;
|
||||
const listeners = [];
|
||||
const maskedControls = new Set(), submittedControls = new Set();
|
||||
const maskClass = "mengsan-hand-controls-hidden-shuying";
|
||||
function maskControls(hidden) {
|
||||
const wanted = hidden ? new Set([ui.control, ui.confirm, gesture?.event.endButton]) : new Set();
|
||||
for (const node of maskedControls) {
|
||||
if (wanted.has(node)) continue;
|
||||
node.classList.remove(maskClass); maskedControls.delete(node);
|
||||
}
|
||||
// ui.control 是不同 UI 共同的控件入口;独立确认/结束按钮也可能挂在外部。
|
||||
for (const node of wanted) {
|
||||
if (!node?.classList || node.classList.contains(maskClass)) continue;
|
||||
node.classList.add(maskClass); maskedControls.add(node);
|
||||
}
|
||||
}
|
||||
function closeSubmittedControls(state) {
|
||||
for (const node of new Set([ui.confirm, state.event.endButton])) {
|
||||
if (typeof node?.close !== "function") continue;
|
||||
if (node.classList && !node.classList.contains("mengsan-hand-submitted-control-shuying")) {
|
||||
node.classList.add("mengsan-hand-submitted-control-shuying"); submittedControls.add(node);
|
||||
}
|
||||
// 与本体 windowmouseup 的提交路径一致:关闭旧确认后再调用 ui.click.ok。
|
||||
node.close();
|
||||
}
|
||||
}
|
||||
let gesture = null, suppressed = null, aim = null, frame = null, lastPoint = null;
|
||||
const listen = (node, type, handler, options = true) => {
|
||||
const safeHandler = event => {
|
||||
try { handler(event); }
|
||||
catch (error) {
|
||||
log("手牌局部输入异常,取消本次拖动", error);
|
||||
try { cleanup(true); }
|
||||
catch (cause) { log("取消输入时引擎异常", cause); }
|
||||
}
|
||||
};
|
||||
node.addEventListener(type, safeHandler, options);
|
||||
listeners.push(() => node.removeEventListener(type, safeHandler, options));
|
||||
};
|
||||
const swallow = event => { event.preventDefault(); event.stopImmediatePropagation(); };
|
||||
const live = state => session.active && adapter.supported() && state?.card.isConnected &&
|
||||
adapter.zones.includes(state.card.parentNode) && status.event === state.event &&
|
||||
!state.event.skill && state.event.isMine?.() && player.isAlive?.() !== false;
|
||||
// 引擎 checkEnd 的自动确认必须在选牌/试选目标期间禁用;仅在本次同步调用内改标志。
|
||||
function engineCall(callback) {
|
||||
const previous = {};
|
||||
for (const key of ["touchnocheck", "clicked", "dragged"]) previous[key] = { own: Object.hasOwn(status, key), value: status[key] };
|
||||
status.touchnocheck = true; status.clicked = false; status.dragged = false;
|
||||
try { return callback(); }
|
||||
finally {
|
||||
for (const [key, old] of Object.entries(previous)) {
|
||||
if (old.own) status[key] = old.value;
|
||||
else delete status[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
function range(event) {
|
||||
const value = get.select(event.selectTarget);
|
||||
if (!Array.isArray(value) || value.length < 2 || value.some(n => !Number.isFinite(n))) return null;
|
||||
return value;
|
||||
}
|
||||
function eligible(card) {
|
||||
const event = status?.event;
|
||||
if (!event || typeof ui.click?.card !== "function" || typeof ui.click?.ok !== "function" ||
|
||||
typeof get?.select !== "function" || typeof env.isActiveCardUse !== "function" ||
|
||||
!session.active || !adapter.supported() || ui.intro || game.online ||
|
||||
event.name !== "chooseToUse" || event.player !== player || !event.isMine?.() ||
|
||||
event.skill || event.complexSelect || event.custom?.replace?.card || event.custom?.replace?.target || event.custom?.replace?.confirm ||
|
||||
!env.isActiveCardUse(event, player) || !env.canPayCard?.(player, card) ||
|
||||
ui.selected.cards.length || ui.selected.targets.length || ui.selected.buttons.length ||
|
||||
!card.classList.contains("selectable") || card.classList.contains("noclick") ||
|
||||
status.mousedragging || status.dragged || status.paused2) return false;
|
||||
const cards = get.select(event.selectCard);
|
||||
return cards?.[0] === 1 && cards?.[1] === 1;
|
||||
}
|
||||
function removeAim() { aim?.remove(); aim = null; }
|
||||
function showAim(card, point) {
|
||||
if (!aim) {
|
||||
aim = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
aim.classList.add("mengsan-hand-aim-shuying");
|
||||
aim.setAttribute("aria-hidden", "true");
|
||||
const path = document.createElementNS(aim.namespaceURI, "path");
|
||||
aim.appendChild(path); document.body.appendChild(aim);
|
||||
}
|
||||
const rect = card.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2, y = rect.top + 12;
|
||||
aim.setAttribute("viewBox", `0 0 ${window.innerWidth} ${window.innerHeight}`);
|
||||
// clientX/Y 与卡牌矩形都是视口坐标,SVG 本身却可能受到 body zoom/transform 影响。
|
||||
// 用 SVG 实际屏幕矩阵反变换,不能重复除 documentZoom 或假定 SVG 没有偏移。
|
||||
const matrix = aim.getScreenCTM();
|
||||
if (!matrix) { removeAim(); return; }
|
||||
const inverse = matrix.inverse();
|
||||
const start = new window.DOMPoint(x, y).matrixTransform(inverse);
|
||||
const end = new window.DOMPoint(point.x, point.y).matrixTransform(inverse);
|
||||
if (![start.x, start.y, end.x, end.y].every(Number.isFinite)) { removeAim(); return; }
|
||||
// 十周年金色细线(本地 CONFIG.LINE_COLOR/LINE_WIDTH),不采用尖塔箭头/骨节造型。
|
||||
aim.firstElementChild.setAttribute("d", `M ${start.x} ${start.y} L ${end.x} ${end.y}`);
|
||||
aim.firstElementChild.setAttribute("vector-effect", "non-scaling-stroke");
|
||||
}
|
||||
function clearSelection(state) {
|
||||
if (!live(state) || !state.selected) return;
|
||||
engineCall(() => { game.uncheck(); game.check(); });
|
||||
}
|
||||
function cleanup(cancel = false) {
|
||||
const state = gesture;
|
||||
gesture = null;
|
||||
if (frame != null) window.cancelAnimationFrame(frame);
|
||||
frame = null; lastPoint = null;
|
||||
removeAim(); fan.stopDrag();
|
||||
maskControls(false);
|
||||
if (cancel && state) clearSelection(state);
|
||||
if (state) suppressed = { card: state.card, until: Date.now() + 450,
|
||||
x: state.endX ?? state.x, y: state.endY ?? state.y };
|
||||
refresh();
|
||||
}
|
||||
function start(event) {
|
||||
if (event.button !== 0 || !event.isPrimary || gesture) return;
|
||||
const card = event.target.closest?.(".card");
|
||||
if (!card || !adapter.zones.includes(card.parentNode) || !eligible(card)) return;
|
||||
// 在 window 捕获,早于十周年的手牌排序和本体的 windowmousedown;不改全局处理器。
|
||||
gesture = { card, event: status.event, id: event.pointerId,
|
||||
x: event.clientX, y: event.clientY, moved: false, selected: false, targeted: false, manual: false };
|
||||
swallow(event);
|
||||
}
|
||||
function targetAt(point) {
|
||||
const node = document.elementFromPoint(point.x, point.y)?.closest?.(".player");
|
||||
return game.players?.includes(node) ? node : null;
|
||||
}
|
||||
function chooseTarget(state, point) {
|
||||
if (!state.targeted || state.manual) return;
|
||||
const target = targetAt(point);
|
||||
for (const previous of ui.selected.targets.slice()) {
|
||||
if (previous !== target) engineCall(() => ui.click.target.call(previous));
|
||||
}
|
||||
if (target?.classList.contains("selectable") && !ui.selected.targets.includes(target)) {
|
||||
engineCall(() => ui.click.target.call(target));
|
||||
}
|
||||
}
|
||||
function paint() {
|
||||
frame = null;
|
||||
const state = gesture, point = lastPoint;
|
||||
if (!state || !point || !state.moved) return;
|
||||
if (!live(state) || !env.canPayCard(player, state.card)) { cleanup(true); return; }
|
||||
fan.drag(state.card, point, state.targeted);
|
||||
if (state.targeted) showAim(state.card, point);
|
||||
chooseTarget(state, point);
|
||||
maskControls(!state.manual);
|
||||
}
|
||||
function move(event) {
|
||||
const state = gesture;
|
||||
if (!state || event.pointerId !== state.id) return;
|
||||
swallow(event);
|
||||
if (!live(state)) { cleanup(true); return; }
|
||||
const point = { x: event.clientX, y: event.clientY };
|
||||
if (!state.moved && Math.hypot(point.x - state.x, point.y - state.y) < 9) return;
|
||||
if (!state.moved) {
|
||||
state.moved = true;
|
||||
engineCall(() => ui.click.card.call(state.card));
|
||||
state.selected = ui.selected.cards.includes(state.card);
|
||||
const targets = range(state.event);
|
||||
if (!state.selected || !live(state) || !targets) { cleanup(true); return; }
|
||||
state.targeted = targets[1] > 0;
|
||||
// 多目标保留选牌和原生确认,不擅自把一条线当作已选全部目标。
|
||||
state.manual = state.targeted && (targets[0] !== 1 || targets[1] !== 1);
|
||||
}
|
||||
lastPoint = point;
|
||||
if (frame == null) frame = window.requestAnimationFrame(() => {
|
||||
try { paint(); }
|
||||
catch (error) { log("手牌拖动绘制失败", error); cleanup(true); }
|
||||
});
|
||||
}
|
||||
function end(event) {
|
||||
const state = gesture;
|
||||
if (!state || state.id !== event.pointerId) return;
|
||||
swallow(event);
|
||||
state.endX = event.clientX; state.endY = event.clientY;
|
||||
if (!live(state) || event.type === "pointercancel") { cleanup(true); return; }
|
||||
if (!state.moved) {
|
||||
cleanup();
|
||||
// 短点击保持原生选牌/自动确认行为,不强制进入拖动。
|
||||
status.clicked = false;
|
||||
ui.click.card.call(state.card);
|
||||
return;
|
||||
}
|
||||
lastPoint = { x: event.clientX, y: event.clientY };
|
||||
paint();
|
||||
if (!gesture) return;
|
||||
const targets = range(state.event);
|
||||
if (targets && targets[1] > 0 && (targets[0] !== 1 || targets[1] !== 1)) state.manual = true;
|
||||
const ready = env.canPayCard(player, state.card) && ui.selected.cards.length === 1 &&
|
||||
ui.selected.cards[0] === state.card && targets &&
|
||||
(!state.targeted ? state.y - event.clientY >= 60 &&
|
||||
event.clientY < adapter.root.getBoundingClientRect().top :
|
||||
!state.manual && targets[0] === 1 && targets[1] === 1 &&
|
||||
targetAt(lastPoint) === ui.selected.targets[0] && ui.selected.targets.length === 1);
|
||||
if (ready && engineCall(() => game.check()) && live(state)) {
|
||||
closeSubmittedControls(state);
|
||||
cleanup();
|
||||
ui.click.ok();
|
||||
} else {
|
||||
cleanup(!state.manual);
|
||||
}
|
||||
}
|
||||
function compatibility(event) {
|
||||
if (gesture || (event.type === "click" && suppressed && Date.now() < suppressed.until &&
|
||||
(suppressed.card.contains(event.target) || Math.hypot(event.clientX - suppressed.x, event.clientY - suppressed.y) < 3))) swallow(event);
|
||||
}
|
||||
function hover(event) {
|
||||
if (gesture || event.pointerType === "touch") return;
|
||||
const card = event.target.closest?.(".card");
|
||||
fan.setHover(adapter.zones.includes(card?.parentNode) ? card : null);
|
||||
}
|
||||
function cancel(event) {
|
||||
if (!gesture) return;
|
||||
if (event?.type === "keydown" && event.key !== "Escape") return;
|
||||
if (event?.cancelable) swallow(event);
|
||||
cleanup(true);
|
||||
}
|
||||
if (window.PointerEvent && adapter.root && status && get) {
|
||||
listen(window, "pointerdown", start);
|
||||
listen(window, "pointermove", move);
|
||||
listen(window, "pointerup", end);
|
||||
listen(window, "pointercancel", cancel);
|
||||
listen(window, "blur", cancel);
|
||||
listen(window, "keydown", cancel);
|
||||
listen(window, "contextmenu", cancel);
|
||||
for (const type of ["mousedown", "mousemove", "mouseup", "click", "touchstart", "touchmove", "touchend"]) {
|
||||
listen(window, type, compatibility, { capture: true, passive: false });
|
||||
}
|
||||
}
|
||||
listen(adapter.root, "pointerover", hover);
|
||||
listen(adapter.root, "pointerleave", () => { if (!gesture) fan.setHover(null); });
|
||||
return {
|
||||
owns: () => Boolean(gesture),
|
||||
reconcile() {
|
||||
if (gesture && (!live(gesture) || !gesture.card.isConnected)) cleanup(true);
|
||||
else if (gesture?.moved) maskControls(!gesture.manual);
|
||||
for (const node of submittedControls) {
|
||||
if (node.isConnected) continue;
|
||||
node.classList.remove("mengsan-hand-submitted-control-shuying"); submittedControls.delete(node);
|
||||
}
|
||||
},
|
||||
dispose() {
|
||||
try { cleanup(true); }
|
||||
catch (error) { log("手牌拖动取消失败", error); }
|
||||
for (const release of listeners.reverse()) release();
|
||||
maskControls(false);
|
||||
for (const node of submittedControls) node.classList.remove("mengsan-hand-submitted-control-shuying");
|
||||
submittedControls.clear();
|
||||
suppressed = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,8 @@ export function createHandOverlays(document, cardCost) {
|
||||
const cost = String(cardCost(card));
|
||||
setData(entry.cost, "mengsanCost", cost);
|
||||
entry.cost.setAttribute("aria-label", `费用 ${cost}`);
|
||||
if (entry.cost.parentNode !== card) card.appendChild(entry.cost);
|
||||
if (card.name === "mengsan_dazed_shuying") entry.cost.remove();
|
||||
else if (entry.cost.parentNode !== card) card.appendChild(entry.cost);
|
||||
const level = cardUpgradeLevel(card);
|
||||
if (level) {
|
||||
setData(entry.upgrade, "mengsanUpgrade", String(level));
|
||||
|
||||
+184
-22
@@ -4,12 +4,21 @@
|
||||
--mengsan-hand-ink: #fff1cb;
|
||||
}
|
||||
.mengsan-hand-scroll-shuying {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: hidden !important;
|
||||
overflow: visible !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
padding: 0 !important;
|
||||
pointer-events: none;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d8ba83 #234844;
|
||||
overscroll-behavior-x: contain;
|
||||
touch-action: pan-x;
|
||||
touch-action: none;
|
||||
}
|
||||
.mengsan-hand-root-shuying {
|
||||
overflow: visible !important;
|
||||
}
|
||||
.mengsan-hand-scroll-shuying:focus-visible {
|
||||
outline: 2px solid #d8ba83;
|
||||
@@ -17,37 +26,88 @@
|
||||
}
|
||||
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying {
|
||||
position: relative !important;
|
||||
display: flex !important;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
width: max-content !important;
|
||||
min-width: 100% !important;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
left: 0 !important;
|
||||
top: auto !important;
|
||||
bottom: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 24px 8px 6px !important;
|
||||
padding: 0 !important;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
overflow: visible !important;
|
||||
}
|
||||
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card {
|
||||
position: relative !important;
|
||||
flex: 0 0 auto;
|
||||
width: 90px !important;
|
||||
height: 110px !important;
|
||||
left: auto !important;
|
||||
position: absolute !important;
|
||||
width: 112px !important;
|
||||
height: 156px !important;
|
||||
left: 0 !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
top: 100% !important;
|
||||
bottom: auto !important;
|
||||
margin: 0 !important;
|
||||
transform: none !important;
|
||||
transition: transform 120ms ease-out !important;
|
||||
transform-origin: 50% 100% !important;
|
||||
transform: translate(var(--mengsan-fan-x, 0px), var(--mengsan-fan-y, -128px)) rotate(var(--mengsan-fan-angle, 0deg)) !important;
|
||||
z-index: var(--mengsan-fan-z, 1) !important;
|
||||
transition: transform 160ms cubic-bezier(.2,.7,.3,1), box-shadow 160ms ease-out !important;
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
}
|
||||
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.selected {
|
||||
transform: translateY(-18px) !important;
|
||||
transform: translate(var(--mengsan-fan-x, 0px), calc(var(--mengsan-fan-y, -128px) - 20px)) rotate(var(--mengsan-fan-angle, 0deg)) !important;
|
||||
outline: 2px solid var(--mengsan-hand-gold);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.mengsan-hand-hover-shuying {
|
||||
transform: translate(var(--mengsan-fan-x, 0px), calc(var(--mengsan-fan-y, -128px) - 62px)) rotate(0deg) scale(1.35) !important;
|
||||
z-index: 220 !important;
|
||||
box-shadow: 0 0 0 2px #b6e8de, 0 0 14px #68b6a58c, 0 12px 22px #0009;
|
||||
}
|
||||
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.mengsan-hand-drag-shuying {
|
||||
top: 0 !important;
|
||||
transform: translate(var(--mengsan-drag-x), var(--mengsan-drag-y)) rotate(0deg) !important;
|
||||
z-index: 240 !important;
|
||||
pointer-events: none !important;
|
||||
transition: none !important;
|
||||
box-shadow: 0 0 0 2px #b6e8de, 0 0 14px #68b6a58c, 0 12px 22px #0009;
|
||||
}
|
||||
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.mengsan-hand-free-drag-shuying {
|
||||
transform: translate(var(--mengsan-drag-x), var(--mengsan-drag-y)) rotate(0deg) scale(1.22) !important;
|
||||
transform-origin: 50% 50% !important;
|
||||
}
|
||||
.mengsan-hand-aim-shuying {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.mengsan-hand-aim-shuying > path {
|
||||
fill: none;
|
||||
stroke: rgb(250, 220, 140);
|
||||
stroke-width: 2.6;
|
||||
stroke-linecap: round;
|
||||
filter: drop-shadow(0 0 2px #f5d25b);
|
||||
}
|
||||
/* 只在梦三手牌挂载期间隐藏十周年阶段、等待响应和对方思考图片。 */
|
||||
body.mengsan-hand-ui-shuying [style*="extension/十周年UI/ui/assets/lbtn/tips/"]:is(
|
||||
[style*="/tips/hhks."], [style*="/tips/zbjd."], [style*="/tips/pdjd."],
|
||||
[style*="/tips/mpjd."], [style*="/tips/cpjd."], [style*="/tips/qpjd."],
|
||||
[style*="/tips/jsjd."], [style*="/tips/hhjs."],
|
||||
[style*="/tips/ddxy."], [style*="/tips/dfsk."]
|
||||
) {
|
||||
display: none !important;
|
||||
}
|
||||
body.mengsan-hand-ui-shuying :is(
|
||||
.mengsan-hand-controls-hidden-shuying,
|
||||
.mengsan-hand-submitted-control-shuying
|
||||
) {
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.removing,
|
||||
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card.display-none {
|
||||
display: none !important;
|
||||
@@ -86,8 +146,11 @@
|
||||
content: attr(data-mengsan-cost);
|
||||
}
|
||||
.mengsan-hand-zone-shuying > .card > .mengsan-hand-upgrade-shuying {
|
||||
top: 4px !important;
|
||||
right: 4px !important;
|
||||
/* 扇形叠牌露出左侧:角标排在花色下、费用上,不抬高整张牌的层级。 */
|
||||
top: calc(max(48%, 64px) - 25px) !important;
|
||||
left: 3px !important;
|
||||
right: auto !important;
|
||||
white-space: nowrap;
|
||||
width: 26px;
|
||||
height: 20px;
|
||||
border: 1px solid #86bd85;
|
||||
@@ -95,13 +158,27 @@
|
||||
background: #214f36;
|
||||
color: #f5ffea;
|
||||
}
|
||||
/* 只为带强化角标的真实手牌预留左侧信息栏,避免窄屏挤压。 */
|
||||
.mengsan-hand-zone-shuying > .card:has(> .mengsan-hand-upgrade-shuying) > .mengsan-hand-cost-shuying {
|
||||
top: max(48%, 64px) !important;
|
||||
}
|
||||
.mengsan-hand-zone-shuying > .card:has(> .mengsan-hand-upgrade-shuying) > .suit-num {
|
||||
top: 6px !important;
|
||||
}
|
||||
@media (max-width: 640px), (max-height: 500px) {
|
||||
.mengsan-hand-zone-shuying > .card:has(> .mengsan-hand-upgrade-shuying) > .suit-num {
|
||||
top: 4px !important;
|
||||
font-size: 20px !important;
|
||||
line-height: 16px !important;
|
||||
}
|
||||
}
|
||||
.mengsan-hand-zone-shuying > .card > .mengsan-hand-upgrade-shuying::before {
|
||||
content: "+" attr(data-mengsan-upgrade);
|
||||
}
|
||||
@media (max-width: 640px), (max-height: 500px) {
|
||||
.mengsan-hand-zone-shuying.mengsan-hand-layout-shuying > .card {
|
||||
width: 80px !important;
|
||||
height: 100px !important;
|
||||
width: 82px !important;
|
||||
height: 114px !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@@ -109,3 +186,88 @@
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 梦三本地竖幅立绘使用完整卡面;只协调梦三手牌里的原生显示节点。 */
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) {
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
}
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) {
|
||||
display: block !important;
|
||||
top: auto !important;
|
||||
bottom: 8px !important;
|
||||
left: auto !important;
|
||||
right: 8px !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
padding: 3px 6px !important;
|
||||
margin: 0 !important;
|
||||
writing-mode: horizontal-tb !important;
|
||||
-webkit-writing-mode: horizontal-tb !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 20px !important;
|
||||
letter-spacing: 1px !important;
|
||||
color: #fff1cb !important;
|
||||
text-shadow: 0 1px 2px #000 !important;
|
||||
background: #231b17e6 !important;
|
||||
border-radius: 3px;
|
||||
z-index: 7 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) br {
|
||||
display: none;
|
||||
}
|
||||
/* 十周年同时保留竖排名称与 top-name 时,只显示后者。 */
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip):has(> .top-name) > .name {
|
||||
display: none !important;
|
||||
}
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) {
|
||||
top: 6px !important;
|
||||
left: 6px !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 0 !important;
|
||||
padding: 2px 4px !important;
|
||||
font-size: 18px !important;
|
||||
line-height: 18px !important;
|
||||
letter-spacing: 0 !important;
|
||||
color: #231b17 !important;
|
||||
text-shadow: none !important;
|
||||
background: #fff1cbe6 !important;
|
||||
border-radius: 3px;
|
||||
z-index: 7 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]) > .image,
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]) > .info:empty {
|
||||
display: none !important;
|
||||
}
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):is([data-suit="heart"], [data-suit="diamond"]):not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) {
|
||||
color: #a52219 !important;
|
||||
}
|
||||
|
||||
/* 强化卡牌的完整卡面样式优先级更高,窄屏同样为角标留出空隙。 */
|
||||
@media (max-width: 640px), (max-height: 500px) {
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip):has(> .mengsan-hand-upgrade-shuying) > .suit-num {
|
||||
top: 4px !important;
|
||||
padding: 0 2px !important;
|
||||
font-size: 18px !important;
|
||||
line-height: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 长牌名完整显示,不挤出卡面;只作用于梦三手牌。 */
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) {
|
||||
font-size: 12px !important;
|
||||
letter-spacing: 0 !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
@media (max-width: 640px), (max-height: 500px) {
|
||||
.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) {
|
||||
font-size: 10px !important;
|
||||
padding: 2px 4px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,9 +91,18 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
throw new TypeError("濒死一击必须有伤害数值");
|
||||
}
|
||||
damageLine.hidden = !hasDamage;
|
||||
const hasSummon = typeof intent.summon === "string";
|
||||
const hasSlimed = Number.isInteger(intent.slimed) && intent.slimed > 0;
|
||||
const hasInfection = Number.isInteger(intent.infection) && intent.infection > 0;
|
||||
const hasDazed = Number.isInteger(intent.dazed) && intent.dazed > 0;
|
||||
const hasDebuff = typeof intent.debuff === "string" &&
|
||||
Number.isInteger(intent.stacks) && intent.stacks > 0;
|
||||
debuffLine.hidden = !hasDebuff;
|
||||
debuffLine.hidden = !(hasDebuff || hasDazed || hasSlimed || hasInfection);
|
||||
const debuffSrc = `${assetBase}/assets/intent/${hasDazed || hasSlimed || hasInfection ? "Intent_status_card.png" : "Intent_debuff.png"}`;
|
||||
if (debuffIcon.getAttribute("src") !== debuffSrc) debuffIcon.src = debuffSrc;
|
||||
if (hasInfection) debuffAmount.textContent = `×${intent.infection}`;
|
||||
if (hasSlimed) debuffAmount.textContent = `×${intent.slimed}`;
|
||||
if (hasDazed) debuffAmount.textContent = `×${intent.dazed}`;
|
||||
if (hasDebuff) {
|
||||
const debuffText = `×${intent.stacks}`;
|
||||
if (debuffAmount.textContent !== debuffText) {
|
||||
@@ -102,8 +111,11 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
}
|
||||
const hasBlock = Number.isInteger(intent.block) && intent.block > 0;
|
||||
const hasStrength = Number.isInteger(intent.strength) && intent.strength > 0;
|
||||
const buffSrc = `${assetBase}/assets/intent/${hasSummon ? "Intent_summon.png" : "Intent_buff.png"}`;
|
||||
if (buffIcon.getAttribute("src") !== buffSrc) buffIcon.src = buffSrc;
|
||||
blockLine.hidden = !hasBlock;
|
||||
buffLine.hidden = !hasStrength;
|
||||
buffLine.hidden = !(hasStrength || hasSummon);
|
||||
if (hasSummon) buffAmount.textContent = "×1";
|
||||
if (hasBlock) blockAmount.textContent = String(intent.block);
|
||||
if (hasStrength) {
|
||||
buffAmount.textContent = `+${intent.strength}`;
|
||||
@@ -136,20 +148,26 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
}
|
||||
if (hasDebuff) {
|
||||
const label = intent.debuff === "脆弱" ? "Frail" :
|
||||
intent.debuff === "易伤" ? "Vulnerable" : "Debuff";
|
||||
intent.debuff === "易伤" ? "Vulnerable" : intent.debuff === "缩小" ? "Shrink" : intent.debuff === "缠结" ? "Tangled" : "Debuff";
|
||||
effects.push(`${label} ×${intent.stacks}`);
|
||||
}
|
||||
if (hasSummon) effects.push("Summon: 利齿之眼 ×1");
|
||||
if (hasDazed) effects.push(`Dazed ×${intent.dazed}`);
|
||||
if (hasInfection) effects.push(`Infection ×${intent.infection}`);
|
||||
if (hasSlimed) effects.push(`Slimed ×${intent.slimed}`);
|
||||
if (hasBlock) effects.push(`Block ${intent.block}`);
|
||||
if (hasStrength) effects.push(`Strength +${intent.strength}`);
|
||||
if (!effects.length) {
|
||||
damageLine.hidden = false;
|
||||
amount.textContent = "";
|
||||
const src = `${assetBase}/assets/intent/Intent_unknown.png`;
|
||||
const src = `${assetBase}/assets/intent/${intent.sleep ? "Intent_sleep.png" : "Intent_unknown.png"}`;
|
||||
if (icon.getAttribute("src") !== src) icon.src = src;
|
||||
effects.push("Unknown");
|
||||
effects.push(intent.sleep ? "Sleep" : "Unknown");
|
||||
}
|
||||
const label = effects.join(", ");
|
||||
badge.title = "攻击与负面效果作用于所有存活敌对角色;伤害按各目标状态计算,护甲另行抵扣。";
|
||||
badge.title = hasInfection ? `向敌对角色的个人弃牌堆加入${intent.infection}张感染;不能被打出,回合结束在手牌中每张造成3点非攻击伤害。` : intent.sleep ? "沉睡:本回合不行动,不消耗费用。" : intent.debuff === "缠结" ? "紧绕藤蔓:攻击后施加1回合缠结,攻击牌费用增加1;自身回合结束减少1回合。" : intent.shrink ? "缩小:攻击伤害减少30%,不重复叠加;来源死亡解除。" : hasSlimed ? `黏液:向敌对角色的个人弃牌堆加入${intent.slimed}张黏液;耗1费用、抽1张牌、消耗。` : hasSummon ? "虚幻孢子:召唤1只6生命的利齿之眼。" :
|
||||
hasDazed ? "牵制:向敌对角色的个人弃牌堆加入3张晕眩;晕眩不能被打出,具有虚无。" :
|
||||
"攻击与负面效果作用于所有存活敌对角色;伤害按各目标状态计算,护甲另行抵扣。";
|
||||
if (badge.getAttribute("aria-label") !== label) badge.setAttribute("aria-label", label);
|
||||
badge.hidden = false;
|
||||
};
|
||||
|
||||
@@ -305,7 +305,9 @@ export const showMap = (run, { saveRun } = {}) => new Promise(resolve => {
|
||||
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: "book", name: "图鉴", description: "卡牌图鉴 · 查看全部梦三卡牌、效果与费用",
|
||||
onClick: () => { closeCardLibrary = openCardLibrary(run, { view: "catalog", title: "卡牌图鉴", eyebrow: "梦三 · 图鉴" }); }
|
||||
}).setAttribute("aria-haspopup", "dialog");
|
||||
createMenuAction({ icon: "record", name: "征程记录", description: `已通过 ${run.statistics?.completedNodes || 0} 个节点`, onClick: () => showMenuPage("征程记录", "本次征程的阶段统计", parent => {
|
||||
const list = ui.create.div(".mengsan-map-record-list-shuying", parent);
|
||||
[["已通过节点", run.statistics?.completedNodes || 0], ["击败敌人", run.statistics?.defeatedEnemies || 0], ["累计金币", run.statistics?.goldEarned || 0]].forEach(([label, value]) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { get } from "../../../../noname.js";
|
||||
import { cardUpgradeRule } from "../cards/upgrades.js";
|
||||
import { lib, get } from "../../../../noname.js";
|
||||
import { getRelic } from "../relics/definitions.js";
|
||||
import {
|
||||
chooseCardDialog, chooseRelicDialog, chooseVictoryOptions,
|
||||
@@ -8,12 +7,12 @@ import {
|
||||
|
||||
const upgradeDescription = choice => {
|
||||
const card = choice.card;
|
||||
const rule = cardUpgradeRule(card.name);
|
||||
const damage = card.name === "sha" ? 6 : 8;
|
||||
const info = lib.card[card.name];
|
||||
const suit = get.translation(card.suit);
|
||||
return `${suit}${card.number} · 伤害 ${damage} → ${rule.damage}` +
|
||||
(rule.vulnerable ? `;易伤 2 → ${rule.vulnerable} 层` : "") +
|
||||
"。费用不变,最多强化一次。";
|
||||
// Display this card's current rules, not eligibility or upgrade comparisons.
|
||||
const description = typeof info?.cardPrompt === "function"
|
||||
? info.cardPrompt(card) : lib.translate[`${card.name}_info`] || "暂无卡牌介绍。";
|
||||
return `${suit}${card.number} · ${description}`;
|
||||
};
|
||||
|
||||
export async function chooseRewardPackage(choices, {
|
||||
@@ -23,59 +22,70 @@ export async function chooseRewardPackage(choices, {
|
||||
const upgradeChoices = pack.upgradeChoices.map(card => ({
|
||||
id: card.id, card,
|
||||
}));
|
||||
const ready = () => (!upgradeChoices.length || cardId !== null) &&
|
||||
(!pack.relicChoices.length || relicId !== null);
|
||||
const upgrade = {
|
||||
id: "upgrade", label: "强化一张牌",
|
||||
description: upgradeChoices.length
|
||||
? "从个人牌库中择一磨砺,查看强化前后的变化。"
|
||||
? "从个人牌库中选择一张牌进行磨砺。"
|
||||
: "当前没有可强化卡牌,此项无需选择。",
|
||||
isDisabled: () => !upgradeChoices.length,
|
||||
async choose() {
|
||||
const selected = await chooseCardDialog(upgradeChoices, {
|
||||
title: "磨砺 · 强化一张牌", skipLabel: "返回奖励",
|
||||
title: "磨砺 · 强化一张牌", skipLabel: "跳过强化",
|
||||
description: "择一磨砺,锋芒更盛。只强化你选中的一张牌。",
|
||||
describeChoice: upgradeDescription, actionLabel: "强化此牌",
|
||||
});
|
||||
cardId = selected;
|
||||
if (selected !== null) {
|
||||
cardId = selected;
|
||||
const choice = upgradeChoices.find(card => card.id === selected);
|
||||
upgrade.label = `已选强化 · ${get.translation(choice.card.name)}`;
|
||||
upgrade.description = upgradeDescription(choice);
|
||||
} else {
|
||||
upgrade.label = "已跳过强化";
|
||||
upgrade.description = "不强化卡牌;点击可重新选择。";
|
||||
}
|
||||
return RETURN_TO_VICTORY;
|
||||
},
|
||||
};
|
||||
const relic = {
|
||||
id: "relic", label: "遗物 · 三选一",
|
||||
id: "relic", label: pack.relicChoices.length
|
||||
? `遗物奖励 · ${pack.relicChoices.length}选1` : "遗物奖励 · 暂无候选",
|
||||
description: pack.relicChoices.length
|
||||
? "检视本次随机珍藏,择一伴你踏上下一段征途。"
|
||||
: "已持有奖励池中的全部遗物,此项无需选择。",
|
||||
isDisabled: () => !pack.relicChoices.length,
|
||||
async choose() {
|
||||
const selected = await chooseRelicDialog(pack.relicChoices, {
|
||||
skipLabel: "返回奖励", actionLabel: "携此遗物",
|
||||
skipLabel: "跳过遗物", actionLabel: "携此遗物",
|
||||
});
|
||||
relicId = selected;
|
||||
if (selected !== null) {
|
||||
relicId = selected;
|
||||
const choice = pack.relicChoices.find(item => item.id === selected);
|
||||
const definition = getRelic(choice.relic);
|
||||
relic.label = `已选遗物 · ${definition.name}`;
|
||||
relic.description = definition.description;
|
||||
} else {
|
||||
relic.label = "已跳过遗物";
|
||||
relic.description = "不领取遗物;点击可重新选择。";
|
||||
}
|
||||
return RETURN_TO_VICTORY;
|
||||
},
|
||||
};
|
||||
return chooseVictoryOptions([upgrade, relic, {
|
||||
id: "claim-package", label: "领取全部奖励",
|
||||
description: "完成以上选择后,金币、强化、遗物与固定卡牌一并入囊。",
|
||||
isDisabled: () => !ready(),
|
||||
id: "claim-package", label: "领取奖励并继续",
|
||||
description: "领取必得奖励与已选奖励,未选择的项目视为跳过。",
|
||||
choose: () => ({ cardId, relicId }),
|
||||
}, {
|
||||
id: "skip-package", label: "跳过",
|
||||
description: "跳过强化与遗物,金币与固定卡牌仍会获得。",
|
||||
choose: () => ({ cardId: null, relicId: null }),
|
||||
}], {
|
||||
title: "初识的卢", allowSkip: false,
|
||||
rewardLabel: "固定收获 · 完成两项选择后领取",
|
||||
rewardLabel: "搜刮 · 战利品列表",
|
||||
rewardItems: [{ name: `金币 · ${pack.gold}(必得)`,
|
||||
description: "无论选择还是跳过,金币都会结算。" },
|
||||
...fixedRewards.map(reward => ({ ...reward, name: `${reward.name}(必得)` }))],
|
||||
description: `固定获得 ${pack.gold} 金币与${fixedRewards.map(
|
||||
reward => `【${reward.name}】`).join("、")}。` +
|
||||
"另可强化一张牌,并从随机遗物中择一件。",
|
||||
"强化与遗物点击后选择,也可跳过。",
|
||||
});
|
||||
}
|
||||
|
||||
+44
-43
@@ -2,7 +2,6 @@ import { lib, get } from "../../../../noname.js";
|
||||
import { cardCost } from "../battle/combat-rules.js";
|
||||
import { cardUpgradeRule } from "../cards/upgrades.js";
|
||||
import { getRelic } from "../relics/definitions.js";
|
||||
import { chooseButtons } from "./flow-ui.js";
|
||||
|
||||
const element = (tag, className, text, parent) => {
|
||||
const node = document.createElement(tag);
|
||||
@@ -34,7 +33,7 @@ const viewCard = choice => {
|
||||
? `${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 }),
|
||||
name: get.translation(name), type, image, cost: cardCost(choice.card || { name }),
|
||||
description: plainText(cardUpgradeRule(name) && info?.cardPrompt
|
||||
? info.cardPrompt(choice.card || { name })
|
||||
: lib.translate[`${name}_info`] || choice.description ||
|
||||
@@ -71,6 +70,22 @@ export function chooseRelicDialog(choices, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// Mixed reward pools use the same selector, with exactly one candidate awarded.
|
||||
function chooseRewardDialog(choices) {
|
||||
if (choices.every(isCardReward)) return chooseCardDialog(choices);
|
||||
if (choices.every(choice => getRelic(choice.relic))) return chooseRelicDialog(choices);
|
||||
return chooseTilesDialog(choices, {
|
||||
title: "选择一项奖励", actionLabel: "领取此奖励",
|
||||
description: "从本次候选中选择一项,也可以跳过。",
|
||||
}, choice => {
|
||||
if (isCardReward(choice)) return viewCard(choice);
|
||||
const relic = getRelic(choice.relic);
|
||||
return { name: relic?.name || choice.name, type: relic ? "遗物" : "奖励",
|
||||
image: null, cost: null,
|
||||
description: plainText(relic?.description || choice.description) };
|
||||
});
|
||||
}
|
||||
|
||||
function chooseTilesDialog(choices, {
|
||||
title = "选择一张牌", allowSkip = true, skipLabel = "跳过",
|
||||
description = "择一入阵,余者留于身后", describeChoice,
|
||||
@@ -137,7 +152,7 @@ function chooseTilesDialog(choices, {
|
||||
export function chooseVictoryOptions(options, {
|
||||
title = "战后抉择", allowSkip = true,
|
||||
description = "此役已定,选择下一段征途的收获。",
|
||||
rewardLabel = "可选战利品",
|
||||
rewardLabel = "可选战利品", rewardItems = [],
|
||||
} = {}) {
|
||||
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"));
|
||||
@@ -154,6 +169,12 @@ export function chooseVictoryOptions(options, {
|
||||
element("p", "", description, intro);
|
||||
const board = element("div", "mengsan-victory-options-shuying", null, stage);
|
||||
element("span", "mengsan-victory-eyebrow-shuying", rewardLabel, board);
|
||||
// Read-only loot list: inspecting a reward must not select or award it.
|
||||
for (const reward of rewardItems) {
|
||||
const item = element("p", "mengsan-victory-option-copy-shuying", null, board);
|
||||
element("strong", "", reward.name || reward.id, item);
|
||||
if (reward.description) element("small", "", plainText(reward.description), item);
|
||||
}
|
||||
let settled = false, choosing = false;
|
||||
const entries = [];
|
||||
const refresh = () => {
|
||||
@@ -218,54 +239,34 @@ export function chooseVictoryOptions(options, {
|
||||
});
|
||||
}
|
||||
|
||||
// Gold is a persisted candidate, not an automatic reward. The card pool remains
|
||||
// a separate Dialog; skipping that Dialog returns to the victory choices.
|
||||
// Gold and fixed rewards are guaranteed; random loot opens a separate one-of-N selector.
|
||||
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 gold = choices.filter(choice => choice.kind === "gold")
|
||||
.reduce((amount, choice) => amount + choice.amount, 0);
|
||||
const rewards = choices.filter(choice => choice.kind !== "gold");
|
||||
const options = [];
|
||||
if (gold) options.push({
|
||||
id: gold.id,
|
||||
label: `金币 · ${gold.amount}`,
|
||||
description: "将此关金币收入征程钱袋。",
|
||||
});
|
||||
if (rewards.length) {
|
||||
const cardOnly = rewards.every(isCardReward);
|
||||
const kind = rewards.every(isCardReward) ? "卡牌"
|
||||
: rewards.every(choice => getRelic(choice.relic)) ? "遗物" : "战利品";
|
||||
options.push({
|
||||
id: "choose-reward",
|
||||
label: cardOnly ? "择一张牌" : "挑选一项战利品",
|
||||
description: cardOnly ? "从本次候选卡牌中选择一张,加入牌组。" : "检视本次战利品,再决定带走哪一项。",
|
||||
async choose() {
|
||||
const selected = cardOnly
|
||||
? await chooseCardDialog(rewards, {skipLabel:"跳过选卡"})
|
||||
: await chooseButtons("选择一项奖励", rewards);
|
||||
return selected ?? RETURN_TO_VICTORY;
|
||||
},
|
||||
id: "choose-reward", label: `${kind}奖励 · ${rewards.length}选1`,
|
||||
description: `点击查看候选${kind},选择一项领取,或点击跳过。`,
|
||||
choose: () => chooseRewardDialog(rewards),
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
id: "skip-loot", label: "跳过",
|
||||
description: rewards.length ? "跳过本次可选奖励,必得奖励照常领取。"
|
||||
: "本次没有可选奖励,领取必得奖励后继续。",
|
||||
choose: () => null,
|
||||
});
|
||||
return chooseVictoryOptions(options, {
|
||||
title: "过关奖励",
|
||||
description: fixedRewards.length
|
||||
? `固定奖励:${fixedDescription}。另可选择一项战利品。`
|
||||
: "此役已定,可选择一项战利品,也可放弃本次选择。",
|
||||
title: "过关奖励", allowSkip: false,
|
||||
description: `本关必得 ${gold} 金币。点击奖励选项后择一领取,也可跳过。`,
|
||||
rewardLabel: "搜刮 · 战利品列表",
|
||||
rewardItems: [{name: `金币 · ${gold}(必得)`,
|
||||
description: "无论选择还是跳过,金币都会结算。"},
|
||||
...fixedRewards.map(reward => ({ ...reward, name: `${reward.name}(必得)` }))],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1427,3 +1427,21 @@ body.mengsan-battle-ui-shuying :is(
|
||||
outline: 2px solid #8c6a34;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Read-only encyclopedia uses the existing wood, aged paper and gold palette. */
|
||||
.mengsan-deck-dialog-shuying .ms-deck-tabs { flex-wrap: wrap; }
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-filters { display: flex; flex-wrap: wrap; align-items: end; gap: 12px; flex-shrink: 0; }
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; color: var(--ms-muted); font-size: 13px; }
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-field:first-child { flex: 1 1 220px; }
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-input,
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-select { box-sizing: border-box; position: static; min-width: 0; min-height: 44px; width: 100%; margin: 0; padding: 8px 10px; border: 1px solid #96754e; border-radius: 6px; background: #392c23; color: var(--ms-ink); font: 15px/1.5 "Microsoft YaHei", sans-serif; }
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-input::placeholder { color: var(--ms-muted); opacity: 1; }
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-input:focus-visible,
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-select:focus-visible { outline: 3px solid #ffe0a1; outline-offset: 3px; }
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-grid .ms-deck-art { height: auto; aspect-ratio: 5 / 7; }
|
||||
@media (max-width: 600px) {
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-filters { gap: 8px; }
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-field:first-child { flex-basis: 100%; }
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-field:not(:first-child) { flex: 1 1 90px; }
|
||||
.mengsan-deck-dialog-shuying .ms-catalog-filters > button { padding: 8px 10px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user