mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-30 19:50:04 +08:00
189 lines
9.1 KiB
JavaScript
189 lines
9.1 KiB
JavaScript
import { AFFIX_INFO } from "../cards/affixes.js";
|
||
import { hasCardAffix } from "../cards/intrinsic-affixes.js";
|
||
import { describeLibraryCard, openCardLibrary } from "./card-library.js";
|
||
import { cardDefinitions } from "../cards/card-definitions.js";
|
||
import { createCardData } from "../cards/card-data.js";
|
||
import { DAZED_NAME, createDazedData } from "../cards/status-cards.js";
|
||
import { SLIMED_NAME, createSlimedData } from "../cards/slimed-card.js";
|
||
import { WOUND_NAME, createWoundData } from "../cards/wound-card.js";
|
||
import { INFECTION_NAME, createInfectionData } from "../cards/infection-card.js";
|
||
import { getDiagnostics } from "../../diagnostics/index.js";
|
||
|
||
const statusFactories = {
|
||
[DAZED_NAME]: createDazedData,
|
||
[SLIMED_NAME]: createSlimedData,
|
||
[WOUND_NAME]: createWoundData,
|
||
[INFECTION_NAME]: createInfectionData,
|
||
};
|
||
|
||
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;
|
||
let closePicker = null, sequence = 0, message = "";
|
||
const diagnostics = getDiagnostics().scope("mengsan.gm", () => ({
|
||
sessionId: session.id,
|
||
}));
|
||
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;
|
||
dialog.querySelector(".ms-gm-feedback").textContent = message;
|
||
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 = hasCardAffix(selected,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 chooseTemporaryCard = () => {
|
||
if (!available() || closePicker) return;
|
||
close();
|
||
closePicker = openCardLibrary(null, {
|
||
title: "添加临时牌", eyebrow: "梦三 · GM",
|
||
view: "catalog",
|
||
selection: {
|
||
label: "加入手牌 · 仅本场战斗",
|
||
description: "选择卡牌查看详情并加入手牌;正常弃置和洗牌," +
|
||
"战斗结束清理。默认黑桃 A、未强化。",
|
||
unavailableReason: card =>
|
||
Object.hasOwn(cardDefinitions, card.name) ||
|
||
Object.hasOwn(statusFactories, card.name) ? "" :
|
||
"该卡牌尚无可用的战斗创建规则。",
|
||
onSelect(card) {
|
||
if (!available() || !owner.isAlive()) {
|
||
throw new Error("当前战斗已结束或角色已阵亡,无法添加。");
|
||
}
|
||
if (!describeLibraryCard(card).available) {
|
||
throw new Error("该卡牌定义未加载。");
|
||
}
|
||
const id = `mengsan_gm_${session.id}_${++sequence}`;
|
||
const factory = Object.hasOwn(statusFactories, card.name)
|
||
? statusFactories[card.name] : null;
|
||
const data = factory ? factory(id) : createCardData({
|
||
id, name: card.name, suit: "spade", number: 1,
|
||
nature: null, affixes: [], upgrade: 0,
|
||
}, { enemy: true });
|
||
// GM injection must not await the paused native event queue.
|
||
const created = piles.addToDiscard(data);
|
||
if (!created) throw new Error("当前战斗无法创建临时牌。");
|
||
try {
|
||
owner.directgain([created]);
|
||
if (!owner.getCards("h").includes(created)) {
|
||
throw new Error("临时牌未能加入手牌。");
|
||
}
|
||
} catch (error) {
|
||
created.remove();
|
||
diagnostics.error("temporaryCard.failed", error, {
|
||
cardName: data.name, cardId: id,
|
||
});
|
||
game.log("GM 临时牌添加失败", data.name, id,
|
||
error.message);
|
||
throw error;
|
||
}
|
||
selected = created;
|
||
message = `已将【${describeLibraryCard(data).name}】加入手牌,` +
|
||
"仅本场战斗有效。";
|
||
game.log("GM 添加临时牌", owner, data.name, id,
|
||
session.id);
|
||
diagnostics.info("temporaryCard.added", {
|
||
cardName: data.name, cardId: id,
|
||
});
|
||
try { game.check(); }
|
||
catch (error) {
|
||
game.log("GM 临时牌已入手,选择状态刷新失败",
|
||
error.message);
|
||
}
|
||
},
|
||
},
|
||
onClose() {
|
||
closePicker = null;
|
||
if (available()) open();
|
||
},
|
||
});
|
||
};
|
||
const open = () => {
|
||
if (!available()) return false;
|
||
if (closePicker) return true;
|
||
if (dialog?.open) { render(); dialog.focus(); return true; }
|
||
const existing = document.querySelector(
|
||
"dialog.mengsan-deck-dialog-shuying[open]");
|
||
if (existing) { existing.focus(); return false; }
|
||
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, chooseTemporaryCard);
|
||
button("刷新手牌", dialog, render);
|
||
const feedback = node("p", "ms-gm-feedback", "", dialog);
|
||
feedback.setAttribute("role", "status");
|
||
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() {
|
||
disposed = true;
|
||
closePicker?.(); closePicker = null;
|
||
close();
|
||
} };
|
||
session.ownResource(handle, handle.dispose);
|
||
return handle;
|
||
}
|