fixed # 偷偷更新GM菜单内容

This commit is contained in:
2026-10-29 12:07:48 +08:00
parent 727f469e93
commit 59e62d22c9
5 changed files with 130 additions and 10 deletions
+101 -4
View File
@@ -1,6 +1,20 @@
import { AFFIX_INFO } from "../cards/affixes.js";
import { hasCardAffix } from "../cards/intrinsic-affixes.js";
import { describeLibraryCard } from "./card-library.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);
@@ -20,6 +34,10 @@ const button = (label, parent, action) => {
// 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;
@@ -32,6 +50,7 @@ export function mountGMManager(session, owner, piles, battle, resources, game) {
};
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();
@@ -60,9 +79,79 @@ export function mountGMManager(session, owner, piles, battle, resources, game) {
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 管理器");
@@ -70,8 +159,12 @@ export function mountGMManager(session, owner, piles, battle, resources, game) {
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);
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;
@@ -85,7 +178,11 @@ export function mountGMManager(session, owner, piles, battle, resources, game) {
};
resources.field(globalThis, "mengsanGM_shuying", open);
resources.field(game, "mengsanGM_shuying", open);
const handle = { dispose: close };
session.ownResource(handle, () => { disposed = true; close(); });
const handle = { dispose() {
disposed = true;
closePicker?.(); closePicker = null;
close();
} };
session.ownResource(handle, handle.dispose);
return handle;
}