同步更新STS2 词缀

This commit is contained in:
2026-09-28 21:49:47 +08:00
parent e3913f8969
commit ced1b28cc2
7 changed files with 193 additions and 17 deletions
+5 -3
View File
@@ -1,8 +1,9 @@
import { lib, get, ui } from "../../../../noname.js";
import { AFFIX_INFO } from "../content/affixes.js";
const suits = { spade: "♠ 黑桃", heart: "♥ 红桃", club: "♣ 梅花", diamond: "♦ 方块" };
const types = { basic: "基本牌", trick: "锦囊牌", delay: "延时锦囊", equip: "装备牌" };
const affixes = { annihilate: "湮灭:使用后进入本场湮灭区,不再参与本场个人弃牌堆洗切。" };
const affixes = Object.fromEntries(Object.entries(AFFIX_INFO).map(([key, info]) => [key, `${info.name}:${info.description}`]));
const text = value => {
const template = document.createElement("template");
// Translation markup is parsed inertly; only plain text is inserted into the UI.
@@ -20,7 +21,7 @@ export function describeLibraryCard(card) {
nature: nature || "无属性", type: types[info?.type] || "未知类别",
red: card.suit === "heart" || card.suit === "diamond",
description: text(lib.translate[`${card.name}_info`] || "暂无卡牌介绍。"),
affixes: (Array.isArray(card.affixes) ? card.affixes : []).map(key => affixes[key] || `词缀:${translate(key)}`),
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,
available: Boolean(info),
};
@@ -150,7 +151,7 @@ export function openCardLibrary(run, options = {}) {
return close;
}
// Only the player's two mode-owned piles are exposed. No hand/equipment/exhaust/global pile.
// The battle view exposes only this player's own piles, including consumed cards.
export function mountBattlePiles(session, battle) {
let closeDialog = null, disposed = false;
const control = button("牌堆", ui.window, () => {
@@ -161,6 +162,7 @@ export function mountBattlePiles(session, battle) {
sections: () => [
{ title: "剩余牌堆", cards: battle.drawPile, ordered: true },
{ title: "弃牌堆", cards: battle.discardPile, ordered: false },
{ title: "消耗牌堆", cards: battle.exhaustPile, ordered: false },
],
});
}, "mengsan-battle-piles-button-shuying");
+90
View File
@@ -0,0 +1,90 @@
import { AFFIX_INFO } from "../content/affixes.js";
import { describeLibraryCard } from "./card-library.js";
const node = (tag, className, label, parent) => {
const element = document.createElement(tag);
element.className = className;
if (label != null) element.textContent = label;
parent?.appendChild(element);
return element;
};
const button = (label, parent, action) => {
const element = node("button", "ms-deck-button", label, parent);
element.type = "button";
element.addEventListener("click", action);
return element;
};
// Keep the command battle-scoped; further GM actions can be added as panels here.
export function mountGMManager(session, owner, piles, battle, resources, game) {
let dialog = null, selected = null, previousFocus = null, disposed = false;
const available = () => !disposed && session.active && !battle.resolving;
const close = () => {
if (!dialog) return;
const current = dialog;
dialog = null;
if (current.open) current.close();
current.remove();
if (previousFocus?.isConnected) previousFocus.focus();
previousFocus = null;
};
const render = () => {
if (!dialog) return;
const list = dialog.querySelector(".ms-gm-hand");
const editor = dialog.querySelector(".ms-gm-editor");
list.replaceChildren(); editor.replaceChildren();
const hand = owner.getCards("h").filter(card => card.storage?.mengsanCard_shuying);
if (!hand.includes(selected)) selected = null;
if (!hand.length) node("p", "ms-deck-empty", "当前没有可编辑的梦三手牌。", list);
for (const [index, card] of hand.entries()) {
const data = card.storage.mengsanCard_shuying;
const details = describeLibraryCard({ ...data, name: card.name, suit: card.suit, number: card.number });
const label = `${index + 1}. ${details.name} ${details.suit}${details.number}${details.affixes.length ? ` · ${details.affixes.map(value => value.split(":")[0]).join("、")}` : ""}`;
const item = button(label, list, () => { selected = card; render(); });
item.classList.add("ms-gm-card");
item.setAttribute("aria-pressed", String(selected === card));
}
if (!selected) { node("p", "ms-deck-empty", "选择一张手牌,然后添加词缀。", editor); return; }
node("h3", "", `编辑:${describeLibraryCard(selected.storage.mengsanCard_shuying).name}`, editor);
const choices = node("div", "ms-gm-affixes", null, editor);
for (const [key, info] of Object.entries(AFFIX_INFO)) {
const applied = selected.storage.mengsanCard_shuying.affixes.includes(key);
const item = button(`${info.name}${applied ? " · 已有" : ""}`, choices, () => {
if (!available() || !owner.getCards("h").includes(selected)) { render(); return; }
piles.addAffix(selected, key);
render();
});
item.disabled = applied;
item.title = info.description;
}
};
const open = () => {
if (!available()) return false;
if (dialog?.open) { render(); dialog.focus(); return true; }
previousFocus = document.activeElement;
dialog = node("dialog", "mengsan-deck-dialog-shuying mengsan-gm-dialog-shuying", null);
dialog.setAttribute("aria-label", "梦三 GM 管理器");
dialog.tabIndex = -1;
const header = node("header", "ms-deck-header", null, dialog);
node("h2", "ms-deck-title", "GM 管理器", header);
button("关闭", header, close);
node("p", "ms-deck-summary", "手牌词缀 · 仅本场战斗生效,不改永久牌组或存档。", dialog);
button("刷新手牌", dialog, render);
node("div", "ms-gm-hand", null, dialog);
node("section", "ms-gm-editor", null, dialog);
const created = dialog;
dialog.addEventListener("close", () => { if (dialog === created) close(); });
dialog.addEventListener("keydown", event => event.stopPropagation());
document.body.appendChild(dialog);
try { dialog.showModal(); } catch (error) { close(); throw error; }
render();
dialog.querySelector(".ms-deck-header button").focus();
return true;
};
resources.field(globalThis, "mengsanGM_shuying", open);
resources.field(game, "mengsanGM_shuying", open);
const handle = { dispose: close };
session.ownResource(handle, () => { disposed = true; close(); });
return handle;
}
+30 -5
View File
@@ -1,6 +1,8 @@
// Mode-owned piles use real Card nodes so native searches/gain/insert operations agree.
// DIY must access ui piles inside the acting player's engine event, never cache global nodes.
import { cardCost } from "./combat-rules.js";
import { AFFIX_INFO } from "../content/affixes.js";
export function installPersonalPiles(session, owner, battle, run, resources, env) {
const { game, ui, get, _status, document, MutationObserver, shuffle } = env;
@@ -29,7 +31,12 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
if (owned.has(card)) return;
owned.add(card); resources.card(card);
card.storage ||= {};
if (data) card.storage.mengsanCard_shuying = JSON.parse(JSON.stringify(data));
card.storage.mengsanOwnerId_shuying = owner.playerid;
if (data) {
card.storage.mengsanCard_shuying = JSON.parse(JSON.stringify(data));
// Retired affixes from old saves must not acquire new battle behaviour.
card.storage.mengsanCard_shuying.affixes = (card.storage.mengsanCard_shuying.affixes || []).filter(key => key !== "annihilate");
}
if (data && env.showCosts) {
card.dataset.mengsanCost = String(cardCost(data));
}
@@ -37,14 +44,17 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
if (!data) return;
card.destroyLog = false;
card.destroyed = (current, position) => {
if (released || position !== "discardPile") return false;
if (data.affixes?.includes("annihilate") && current.storage.mengsanUsed_shuying) {
if (released || !["discardPile", "equip", "judge"].includes(position)) return false;
if (current.storage.mengsanCard_shuying.affixes.includes("exhaust") && current.storage.mengsanConsumed_shuying) {
if (current.storage.mengsanExhausted_shuying) return true;
current.storage.mengsanExhausted_shuying = true;
battle.exhaustPile.push(snapshot(current));
refresh();
return true;
}
return false; // Actual discarded cards remain available to native skills.
};
if (data.affixes?.includes("annihilate")) card.addGaintag("湮灭");
for (const key of card.storage.mengsanCard_shuying.affixes) card.addGaintag(AFFIX_INFO[key]?.name || key);
if (data.upgrade) card.addGaintag(`强化+${data.upgrade}`);
}
function snapshot(card) {
@@ -122,6 +132,15 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
});
return {
take,
addAffix(card, key) {
if (released || !Object.hasOwn(AFFIX_INFO, key) || !owned.has(card) || !owner.getCards("h").includes(card)) return false;
const affixes = card.storage?.mengsanCard_shuying?.affixes;
if (!Array.isArray(affixes) || affixes.includes(key)) return false;
affixes.push(key);
card.addGaintag(AFFIX_INFO[key].name);
refresh();
return true;
},
// Engine/mod opening draws must not stack with the mode's four-card opening hand.
resetOpeningHand() {
for (const card of owner.getCards("h")) {
@@ -131,12 +150,18 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
refresh();
},
trimOpeningHand(limit = 4) {
for (const card of owner.getCards("h").slice(limit)) {
for (const card of owner.getCards("h").filter(card => !card.storage?.mengsanCard_shuying?.affixes?.includes("innate")).slice(limit)) {
if (owned.has(card)) draw.insertBefore(card, draw.firstChild);
else card.remove();
}
refresh();
},
drawInnate() {
const cards = Array.from(draw.childNodes).filter(card => card.storage?.mengsanCard_shuying?.affixes?.includes("innate") && !owner.getCards("h").includes(card));
if (cards.length) owner.directgain(cards);
refresh();
return cards;
},
withOwner(callback) { const before = forcedOwner; forcedOwner = true; try { return callback(); } finally { forcedOwner = before; } },
};
}