Files
noname-shuying/mengsan/ui/gm-manager.js
T

189 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}