From 59e62d22c90153ccf29d1ee8b6d58ade9561a34d Mon Sep 17 00:00:00 2001 From: Art_Sakura <1754798088@qq.com> Date: Thu, 29 Oct 2026 12:07:48 +0800 Subject: [PATCH] =?UTF-8?q?fixed=20#=20=E5=81=B7=E5=81=B7=E6=9B=B4?= =?UTF-8?q?=E6=96=B0GM=E8=8F=9C=E5=8D=95=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension.js | 2 +- mengsan/mode.js | 2 + mengsan/ui/card-library.js | 27 ++++++++- mengsan/ui/gm-manager.js | 105 +++++++++++++++++++++++++++++++-- mengsan/ui/hud/status-model.js | 4 +- 5 files changed, 130 insertions(+), 10 deletions(-) diff --git a/extension.js b/extension.js index 068f2b5..ca3cccd 100644 --- a/extension.js +++ b/extension.js @@ -6,7 +6,7 @@ import { configureDiagnostics } from "./diagnostics/index.js"; export const type = "extension"; export let shuYing = new Object(); -const shuYingLocalVersion = "2.0.2.3-rc.5"; +const shuYingLocalVersion = "2.0.2.3-rc.6"; shuYing.name = "术樱包"; shuYing.editable = false; diff --git a/mengsan/mode.js b/mengsan/mode.js index 49bf1f1..cdb8fda 100644 --- a/mengsan/mode.js +++ b/mengsan/mode.js @@ -842,6 +842,8 @@ const requestBattleFinish = outcome => { recordBondDeath(player, _status.mengsanRun_shuying, message => game.log(message)); } + // Capture the final HUD state while its battle session is still active. + current.hud?.refresh(); const accepted = current.flow.requestFinish({session:current.session, hp:game.me?.hp || 0, outcome, defeatedEnemies:current.director?.defeatedEnemies ?? 1}); if (accepted) { current.pilesUI?.dispose(); diff --git a/mengsan/ui/card-library.js b/mengsan/ui/card-library.js index 2a164b2..646c189 100644 --- a/mengsan/ui/card-library.js +++ b/mengsan/ui/card-library.js @@ -116,6 +116,7 @@ export function openCardLibrary(run, options = {}) { const existing = document.querySelector("dialog.mengsan-deck-dialog-shuying[open]"); if (existing) { existing.focus(); return () => {}; } const previousFocus = document.activeElement; + const selection = options.selection; const dialog = element("dialog", "mengsan-deck-dialog-shuying", null); dialog.setAttribute("aria-label", options.title || "卡牌库"); const header = element("header", "ms-deck-header", null, dialog); @@ -143,12 +144,13 @@ export function openCardLibrary(run, options = {}) { onClose?.(close); }; button("关闭", header, close); - let view = !options.sections && options.view === "catalog" ? "catalog" : "deck"; + let view = selection || (!options.sections && options.view === "catalog") + ? "catalog" : "deck"; const navigation = element("nav", "ms-deck-tabs", null, dialog); navigation.setAttribute("aria-label", "卡牌列表视图"); const deckTab = button("持有牌", navigation, () => { view = "deck"; render(); }); const catalogTab = button("图鉴", navigation, () => { view = "catalog"; render(); }); - deckTab.hidden = catalogTab.hidden = Boolean(options.sections); + deckTab.hidden = catalogTab.hidden = Boolean(options.sections || selection); let sectionIndex = 0; const sectionTabs = options.sections ? options.sections().map((section, index) => button(section.title, navigation, () => { sectionIndex = index; render(); })) : []; @@ -222,6 +224,24 @@ export function openCardLibrary(run, options = {}) { element("p", "ms-deck-rules", details.description, copy); if (!details.available) element("p", "ms-deck-warning", "该卡牌定义未加载;保留存档记录,不删除此牌。", copy); for (const affix of details.affixes) element("p", "ms-deck-rules", affix, copy); + if (catalog && selection) { + const reason = !details.available ? "该卡牌定义未加载。" : + selection.unavailableReason?.(card) || ""; + const feedback = element("p", "ms-deck-rules", reason, copy); + feedback.setAttribute("role", "status"); + const confirm = button(selection.label, copy, () => { + if (closed || confirm.disabled) return; + confirm.disabled = true; + try { + selection.onSelect(card); + close(); + } catch (error) { + feedback.textContent = error.message || "添加失败,请重试。"; + confirm.disabled = false; + } + }); + confirm.disabled = Boolean(reason); + } body.scrollTop = 0; backButton.focus(); } function fitCards() { @@ -296,7 +316,8 @@ export function openCardLibrary(run, options = {}) { summary.textContent = options.sections ? "剩余牌堆按从左到右、从上到下的顺序摸取;第 1 张为下一张。" : `本次征程实际携带 · 共 ${current.length} 张 · 同名不同花色、点数分别展示`; } - footer.textContent = catalog ? "图鉴仅供查阅,不会获得卡牌或改变征程牌组 · 点击卡牌查看完整介绍" : + if (catalog && selection) footer.textContent = selection.description; + else footer.textContent = catalog ? "图鉴仅供查阅,不会获得卡牌或改变征程牌组 · 点击卡牌查看完整介绍" : "点击卡牌查看完整介绍 · 仅供查看,不改变牌组或抽牌顺序"; virtualGrid.setRecords(catalog || options.sections ? records : current.map(card => ({ card }))); if (!(catalog || options.sections ? records : current).length) { diff --git a/mengsan/ui/gm-manager.js b/mengsan/ui/gm-manager.js index 249d8ef..e28af91 100644 --- a/mengsan/ui/gm-manager.js +++ b/mengsan/ui/gm-manager.js @@ -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; } diff --git a/mengsan/ui/hud/status-model.js b/mengsan/ui/hud/status-model.js index 370282a..420ba0a 100644 --- a/mengsan/ui/hud/status-model.js +++ b/mengsan/ui/hud/status-model.js @@ -26,8 +26,8 @@ const effects = { lossStrength: ['撕裂', 'rupture', '力量', '自身回合失去生命时获得力量。'], vulnerableBonus: ['易伤增幅', 'vicious', '倍率加成', '对易伤敌人的伤害倍率额外加成。'], attackBlock: ['狂怒', 'rage', '格挡', '本回合每打出一张攻击牌获得格挡。'], - retaliation: ['火焰屏障', 'flame_barrier', '伤害', '本回合受到攻击后反击。'], - colossus: ['巨像', 'colossus', '', '本回合受到易伤敌人的攻击伤害减少50%。'], + retaliation: ['火焰屏障', 'flame_barrier', '伤害', '至下个自身回合开始,受到攻击后反击。'], + colossus: ['巨像', 'colossus', '', '至下个自身回合开始,受到易伤敌人的攻击伤害减少50%。'], freeAttack: ['免费攻击', 'free_attack', '', '下一张符合规则的攻击牌免费。'], noDraw: ['不可抽牌', 'no_draw', '', '本回合不能继续抽牌。'], noEnergy: ['无法获得费用', 'no_energy_gain', '', '本回合不能再获得费用。'],