mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-24 00:30:07 +08:00
同步更新STS2 词缀
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
// Shared mode-owned affix registry; adding a new entry also exposes it in GM.
|
||||||
|
export const AFFIX_INFO = Object.freeze({
|
||||||
|
void: Object.freeze({ name: "虚无", description: "弃牌阶段开始时移除此牌,退出此次战斗;即使同时具有保留也会移除。" }),
|
||||||
|
retain: Object.freeze({ name: "保留", description: "弃牌阶段开始时,此牌不计入手牌上限;若同时具有虚无,仍会被移除。" }),
|
||||||
|
unplayable: Object.freeze({ name: "不能被打出", description: "此牌不能使用或打出,但可以被弃置。" }),
|
||||||
|
ingenious: Object.freeze({ name: "奇巧", description: "不因弃牌阶段进入弃牌堆时,若可以使用,则使用此牌。" }),
|
||||||
|
eternal: Object.freeze({ name: "永恒", description: "此牌不能从你的牌组移除或变化。" }),
|
||||||
|
innate: Object.freeze({ name: "固有", description: "战斗开始时移至手牌;若已在手牌中则不移动。" }),
|
||||||
|
exhaust: Object.freeze({ name: "消耗", description: "使用或打出后移出此次战斗,并可在消耗牌堆查看;消耗牌堆不参与洗牌。" }),
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@ export const defaultDeck = Object.freeze([
|
|||||||
["club", 4, "sha"], ["diamond", 6, "sha"],
|
["club", 4, "sha"], ["diamond", 6, "sha"],
|
||||||
["heart", 2, "shan"], ["diamond", 7, "shan"], ["club", 2, "shan"],
|
["heart", 2, "shan"], ["diamond", 7, "shan"], ["club", 2, "shan"],
|
||||||
["heart", 3, "tao"], ["spade", 3, "guohe"], ["heart", 7, "wuzhong"],
|
["heart", 3, "tao"], ["spade", 3, "guohe"], ["heart", 7, "wuzhong"],
|
||||||
["spade", 2, "bagua"], ["club", 6, "sha", null, Object.freeze(["annihilate"])],
|
["spade", 2, "bagua"], ["club", 6, "sha"],
|
||||||
].map(Object.freeze));
|
].map(Object.freeze));
|
||||||
|
|
||||||
// Character ID -> [suit, number, card name, optional nature, optional affixes].
|
// Character ID -> [suit, number, card name, optional nature, optional affixes].
|
||||||
@@ -14,7 +14,7 @@ export const characterDecks = {
|
|||||||
["spade", 7, "sha"], ["heart", 10, "sha"], ["club", 4, "sha"], ["diamond", 6, "sha"],
|
["spade", 7, "sha"], ["heart", 10, "sha"], ["club", 4, "sha"], ["diamond", 6, "sha"],
|
||||||
["heart", 2, "shan"], ["diamond", 7, "shan"], ["club", 2, "shan"],
|
["heart", 2, "shan"], ["diamond", 7, "shan"], ["club", 2, "shan"],
|
||||||
["heart", 3, "tao"], ["heart", 7, "wuzhong"], ["spade", 1, "juedou"],
|
["heart", 3, "tao"], ["heart", 7, "wuzhong"], ["spade", 1, "juedou"],
|
||||||
["spade", 2, "bagua"], ["club", 6, "sha", null, ["annihilate"]],
|
["spade", 2, "bagua"], ["club", 6, "sha"],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+50
-7
@@ -3,8 +3,9 @@ import { createMengsanCards } from "./content/mode-cards.js";
|
|||||||
import { buildBattlePlan, consumeSupports, createBattleDirector, grantSupport } from "./runtime/battle-director.js";
|
import { buildBattlePlan, consumeSupports, createBattleDirector, grantSupport } from "./runtime/battle-director.js";
|
||||||
import { installPersonalPiles } from "./runtime/personal-piles.js";
|
import { installPersonalPiles } from "./runtime/personal-piles.js";
|
||||||
import { createMonster } from "./runtime/monster.js";
|
import { createMonster } from "./runtime/monster.js";
|
||||||
import { PLAYER_ENERGY, PLAYER_HAND_LIMIT, payCard, isActiveCardUse } from "./runtime/combat-rules.js";
|
import { PLAYER_ENERGY, PLAYER_HAND_LIMIT, canPayCard, payCard, isActiveCardUse } from "./runtime/combat-rules.js";
|
||||||
import { mountBattlePiles } from "./runtime/card-library.js";
|
import { mountBattlePiles } from "./runtime/card-library.js";
|
||||||
|
import { mountGMManager } from "./runtime/gm-manager.js";
|
||||||
import { createPlayerTeardown } from "./runtime/skill-teardown.js";
|
import { createPlayerTeardown } from "./runtime/skill-teardown.js";
|
||||||
import { createBattleResources } from "./runtime/battle-resources.js";
|
import { createBattleResources } from "./runtime/battle-resources.js";
|
||||||
import { createModeStorage } from "./runtime/mode-storage.js";
|
import { createModeStorage } from "./runtime/mode-storage.js";
|
||||||
@@ -97,7 +98,8 @@ const applyReward = (run, rewardId) => {
|
|||||||
player.deck.push(createCardInstance(run, "tao"));
|
player.deck.push(createCardInstance(run, "tao"));
|
||||||
break;
|
break;
|
||||||
case "upgrade": {
|
case "upgrade": {
|
||||||
const card = player.deck[Math.floor(nextRandom(run) * player.deck.length)];
|
const eligible = player.deck.filter(card => !card.affixes?.includes("eternal"));
|
||||||
|
const card = eligible[Math.floor(nextRandom(run) * eligible.length)];
|
||||||
if (card) card.upgrade = Number(card.upgrade || 0) + 1;
|
if (card) card.upgrade = Number(card.upgrade || 0) + 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -238,7 +240,7 @@ const initBattleUnit = (player, spec) => {
|
|||||||
const installMonsterPile = (player, monster, current) => {
|
const installMonsterPile = (player, monster, current) => {
|
||||||
if (!monster) return;
|
if (!monster) return;
|
||||||
const run = _status.mengsanRun_shuying;
|
const run = _status.mengsanRun_shuying;
|
||||||
const battle = { drawPile: monster.createDeck(`${run.runId}_${player.playerid}`), discardPile: [] };
|
const battle = { drawPile: monster.createDeck(`${run.runId}_${player.playerid}`), discardPile: [], exhaustPile: [] };
|
||||||
for (const card of battle.drawPile) if (!lib.card[card.name]) throw new Error("怪物牌堆中的牌未加载:" + card.name);
|
for (const card of battle.drawPile) if (!lib.card[card.name]) throw new Error("怪物牌堆中的牌未加载:" + card.name);
|
||||||
shuffleBattlePile(run, battle.drawPile);
|
shuffleBattlePile(run, battle.drawPile);
|
||||||
current.monsterPiles.set(player, installPersonalPiles(current.session, player, battle, run, current.resources, {
|
current.monsterPiles.set(player, installPersonalPiles(current.session, player, battle, run, current.resources, {
|
||||||
@@ -305,6 +307,8 @@ const createScenario = (plan, current) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const prepareBattle = async (run, node, encounter, session, resources) => {
|
const prepareBattle = async (run, node, encounter, session, resources) => {
|
||||||
|
// Old saves keep their deck instances, but the retired affix no longer exists.
|
||||||
|
for (const card of run.player.deck) if (Array.isArray(card.affixes)) card.affixes = card.affixes.filter(key => key !== "annihilate");
|
||||||
if (encounter.requiredCharacter && run.player.character !== encounter.requiredCharacter) throw new Error("该关卡仅限指定主角,请开始刘备的新征程");
|
if (encounter.requiredCharacter && run.player.character !== encounter.requiredCharacter) throw new Error("该关卡仅限指定主角,请开始刘备的新征程");
|
||||||
const plan = buildBattlePlan(encounter, run);
|
const plan = buildBattlePlan(encounter, run);
|
||||||
for (const spec of [...plan.units, ...plan.rules.flatMap(r => r.effects.filter(e => e.type === "spawn").map(e => e.unit))]) {
|
for (const spec of [...plan.units, ...plan.rules.flatMap(r => r.effects.filter(e => e.type === "spawn").map(e => e.unit))]) {
|
||||||
@@ -372,21 +376,24 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
|
|||||||
run.player.permanentSkills.forEach(skill => {
|
run.player.permanentSkills.forEach(skill => {
|
||||||
if (lib.skill[skill] && !me.hasSkill(skill)) me.addSkill(skill);
|
if (lib.skill[skill] && !me.hasSkill(skill)) me.addSkill(skill);
|
||||||
});
|
});
|
||||||
for (const skill of ["mengsan_draw_shuying", "mengsan_card_use_shuying", "mengsan_scenario_shuying", "mengsan_card_payment_shuying", "mengsan_monster_draw_shuying"]) {
|
for (const skill of ["mengsan_draw_shuying", "mengsan_card_use_shuying", "mengsan_card_affixes_shuying", "mengsan_scenario_shuying", "mengsan_card_payment_shuying", "mengsan_monster_draw_shuying"]) {
|
||||||
if (!lib.skill.global.includes(skill)) {
|
if (!lib.skill.global.includes(skill)) {
|
||||||
game.addGlobalSkill(skill);
|
game.addGlobalSkill(skill);
|
||||||
session.ownResource({}, () => game.removeGlobalSkill(skill));
|
session.ownResource({}, () => game.removeGlobalSkill(skill));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activeBattle.pilesUI = mountBattlePiles(session, _status.mengsanBattle_shuying);
|
activeBattle.pilesUI = mountBattlePiles(session, _status.mengsanBattle_shuying);
|
||||||
|
activeBattle.gmUI = mountGMManager(session, me, activeBattle.personalPiles, _status.mengsanBattle_shuying, resources, game);
|
||||||
for (let index = 0; index < plan.units.length; index++) await equipBattleUnit(participants[index], plan.units[index], resources);
|
for (let index = 0; index < plan.units.length; index++) await equipBattleUnit(participants[index], plan.units[index], resources);
|
||||||
game.syncState();
|
game.syncState();
|
||||||
_status.event.trigger("gameStart");
|
_status.event.trigger("gameStart");
|
||||||
for (const participant of participants) await participant.draw(participant.storage.mengsanInitialHand_shuying ?? 4);
|
for (const participant of participants) await participant.draw(participant.storage.mengsanInitialHand_shuying ?? 4);
|
||||||
|
for (const participant of participants) currentBattle.monsterPiles.get(participant)?.drawInnate();
|
||||||
if (!session.active) return;
|
if (!session.active) return;
|
||||||
activeBattle.personalPiles.resetOpeningHand();
|
activeBattle.personalPiles.resetOpeningHand();
|
||||||
await game.mengsanDraw_shuying(me, 4);
|
await game.mengsanDraw_shuying(me, 4);
|
||||||
if (!session.active) return;
|
if (!session.active) return;
|
||||||
|
activeBattle.personalPiles.drawInnate();
|
||||||
await playDialogue(encounter.openingDialogue, { run, title: encounter.name || "开场剧情" });
|
await playDialogue(encounter.openingDialogue, { run, title: encounter.name || "开场剧情" });
|
||||||
if (!session.active) return;
|
if (!session.active) return;
|
||||||
await currentBattle.director.start();
|
await currentBattle.director.start();
|
||||||
@@ -441,6 +448,7 @@ const requestBattleFinish = outcome => {
|
|||||||
const accepted = current.flow.requestFinish({session:current.session, hp:game.me?.hp || 0, outcome, defeatedEnemies:current.director?.defeatedEnemies ?? 1});
|
const accepted = current.flow.requestFinish({session:current.session, hp:game.me?.hp || 0, outcome, defeatedEnemies:current.director?.defeatedEnemies ?? 1});
|
||||||
if (accepted) {
|
if (accepted) {
|
||||||
current.pilesUI?.dispose();
|
current.pilesUI?.dispose();
|
||||||
|
current.gmUI?.dispose();
|
||||||
stopBattleTurn(current.session, _status.eventManager);
|
stopBattleTurn(current.session, _status.eventManager);
|
||||||
if (_status.mengsanBattle_shuying) _status.mengsanBattle_shuying.resolving = true;
|
if (_status.mengsanBattle_shuying) _status.mengsanBattle_shuying.resolving = true;
|
||||||
current.finished();
|
current.finished();
|
||||||
@@ -681,19 +689,54 @@ const createMode = identityMode => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mengsan_card_use_shuying: {
|
mengsan_card_use_shuying: {
|
||||||
trigger: { global: "useCard1" },
|
trigger: { global: ["useCard1", "respond"] },
|
||||||
forced: true,
|
forced: true,
|
||||||
silent: true,
|
silent: true,
|
||||||
popup: false,
|
popup: false,
|
||||||
filter(event) {
|
filter(event) {
|
||||||
return event.player?.storage?.mengsanPlayer_shuying && event.cards?.some(card => card.storage?.mengsanCard_shuying);
|
return Boolean(activeBattle?.session.active && event.cards?.some(card => card.storage?.mengsanCard_shuying));
|
||||||
},
|
},
|
||||||
async content(event, trigger) {
|
async content(event, trigger) {
|
||||||
trigger.cards.forEach(card => {
|
trigger.cards.forEach(card => {
|
||||||
if (card.storage?.mengsanCard_shuying) card.storage.mengsanUsed_shuying = true;
|
if (card.storage?.mengsanCard_shuying) card.storage.mengsanConsumed_shuying = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mengsan_card_affixes_shuying: {
|
||||||
|
forced: true, silent: true, popup: false, priority: 100,
|
||||||
|
trigger: { global: ["phaseDiscardBegin", "cardsDiscardAfter"] },
|
||||||
|
filter(event) {
|
||||||
|
if (!activeBattle?.session.active) return false;
|
||||||
|
if (event.name === "phaseDiscard") return event.player?.getCards("h").some(card => card.storage?.mengsanCard_shuying?.affixes?.includes("void"));
|
||||||
|
return event.cards?.some(card => card.storage?.mengsanCard_shuying?.affixes?.includes("ingenious"));
|
||||||
|
},
|
||||||
|
async content(event, trigger) {
|
||||||
|
if (event.triggername === "phaseDiscardBegin") {
|
||||||
|
const cards = trigger.player.getCards("h").filter(card => card.storage?.mengsanCard_shuying?.affixes?.includes("void"));
|
||||||
|
if (cards.length) {
|
||||||
|
await trigger.player.lose(cards, ui.special);
|
||||||
|
for (const card of cards) card.remove();
|
||||||
|
activeBattle.pilesUI?.refresh();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (trigger.getParent("phaseDiscard")) return;
|
||||||
|
for (const card of trigger.cards || []) {
|
||||||
|
if (!activeBattle?.session.active || get.position(card, true) !== "d" || !card.storage?.mengsanCard_shuying?.affixes?.includes("ingenious") || card.storage.mengsanIngeniousResolving_shuying) continue;
|
||||||
|
const use = trigger.getParent("useCard"), respond = trigger.getParent("respond");
|
||||||
|
if (use?.cards?.includes(card) || respond?.cards?.includes(card)) continue;
|
||||||
|
const owner = game.playerMap[card.storage.mengsanOwnerId_shuying];
|
||||||
|
if (!owner?.isAlive() || !lib.filter.cardEnabled(card, owner) || !canPayCard(owner, card) || !owner.hasUseTarget(card)) continue;
|
||||||
|
card.storage.mengsanIngeniousResolving_shuying = true;
|
||||||
|
try { await owner.chooseUseTarget({ card, cards: [card], forced: true, prompt: "奇巧:使用此牌" }); }
|
||||||
|
finally { card.storage.mengsanIngeniousResolving_shuying = false; }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mod: {
|
||||||
|
ignoredHandcard(card) { if (card.storage?.mengsanCard_shuying?.affixes?.includes("retain")) return true; },
|
||||||
|
cardEnabled2(card) { if (card.storage?.mengsanCard_shuying?.affixes?.includes("unplayable")) return false; },
|
||||||
|
},
|
||||||
|
},
|
||||||
mengsan_yingyong_shuying: {
|
mengsan_yingyong_shuying: {
|
||||||
trigger: { source: "damageBegin1" },
|
trigger: { source: "damageBegin1" },
|
||||||
forced: true,
|
forced: true,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { lib, get, ui } from "../../../../noname.js";
|
import { lib, get, ui } from "../../../../noname.js";
|
||||||
|
import { AFFIX_INFO } from "../content/affixes.js";
|
||||||
|
|
||||||
const suits = { spade: "♠ 黑桃", heart: "♥ 红桃", club: "♣ 梅花", diamond: "♦ 方块" };
|
const suits = { spade: "♠ 黑桃", heart: "♥ 红桃", club: "♣ 梅花", diamond: "♦ 方块" };
|
||||||
const types = { basic: "基本牌", trick: "锦囊牌", delay: "延时锦囊", equip: "装备牌" };
|
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 text = value => {
|
||||||
const template = document.createElement("template");
|
const template = document.createElement("template");
|
||||||
// Translation markup is parsed inertly; only plain text is inserted into the UI.
|
// 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] || "未知类别",
|
nature: nature || "无属性", type: types[info?.type] || "未知类别",
|
||||||
red: card.suit === "heart" || card.suit === "diamond",
|
red: card.suit === "heart" || card.suit === "diamond",
|
||||||
description: text(lib.translate[`${card.name}_info`] || "暂无卡牌介绍。"),
|
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,
|
upgrade: Number.isFinite(card.upgrade) ? card.upgrade : 0,
|
||||||
available: Boolean(info),
|
available: Boolean(info),
|
||||||
};
|
};
|
||||||
@@ -150,7 +151,7 @@ export function openCardLibrary(run, options = {}) {
|
|||||||
return close;
|
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) {
|
export function mountBattlePiles(session, battle) {
|
||||||
let closeDialog = null, disposed = false;
|
let closeDialog = null, disposed = false;
|
||||||
const control = button("牌堆", ui.window, () => {
|
const control = button("牌堆", ui.window, () => {
|
||||||
@@ -161,6 +162,7 @@ export function mountBattlePiles(session, battle) {
|
|||||||
sections: () => [
|
sections: () => [
|
||||||
{ title: "剩余牌堆", cards: battle.drawPile, ordered: true },
|
{ title: "剩余牌堆", cards: battle.drawPile, ordered: true },
|
||||||
{ title: "弃牌堆", cards: battle.discardPile, ordered: false },
|
{ title: "弃牌堆", cards: battle.discardPile, ordered: false },
|
||||||
|
{ title: "消耗牌堆", cards: battle.exhaustPile, ordered: false },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}, "mengsan-battle-piles-button-shuying");
|
}, "mengsan-battle-piles-button-shuying");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
// Mode-owned piles use real Card nodes so native searches/gain/insert operations agree.
|
// 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.
|
// DIY must access ui piles inside the acting player's engine event, never cache global nodes.
|
||||||
import { cardCost } from "./combat-rules.js";
|
import { cardCost } from "./combat-rules.js";
|
||||||
|
import { AFFIX_INFO } from "../content/affixes.js";
|
||||||
|
|
||||||
|
|
||||||
export function installPersonalPiles(session, owner, battle, run, resources, env) {
|
export function installPersonalPiles(session, owner, battle, run, resources, env) {
|
||||||
const { game, ui, get, _status, document, MutationObserver, shuffle } = 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;
|
if (owned.has(card)) return;
|
||||||
owned.add(card); resources.card(card);
|
owned.add(card); resources.card(card);
|
||||||
card.storage ||= {};
|
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) {
|
if (data && env.showCosts) {
|
||||||
card.dataset.mengsanCost = String(cardCost(data));
|
card.dataset.mengsanCost = String(cardCost(data));
|
||||||
}
|
}
|
||||||
@@ -37,14 +44,17 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
card.destroyLog = false;
|
card.destroyLog = false;
|
||||||
card.destroyed = (current, position) => {
|
card.destroyed = (current, position) => {
|
||||||
if (released || position !== "discardPile") return false;
|
if (released || !["discardPile", "equip", "judge"].includes(position)) return false;
|
||||||
if (data.affixes?.includes("annihilate") && current.storage.mengsanUsed_shuying) {
|
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));
|
battle.exhaustPile.push(snapshot(current));
|
||||||
|
refresh();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false; // Actual discarded cards remain available to native skills.
|
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}`);
|
if (data.upgrade) card.addGaintag(`强化+${data.upgrade}`);
|
||||||
}
|
}
|
||||||
function snapshot(card) {
|
function snapshot(card) {
|
||||||
@@ -122,6 +132,15 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
take,
|
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.
|
// Engine/mod opening draws must not stack with the mode's four-card opening hand.
|
||||||
resetOpeningHand() {
|
resetOpeningHand() {
|
||||||
for (const card of owner.getCards("h")) {
|
for (const card of owner.getCards("h")) {
|
||||||
@@ -131,12 +150,18 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
|||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
trimOpeningHand(limit = 4) {
|
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);
|
if (owned.has(card)) draw.insertBefore(card, draw.firstChild);
|
||||||
else card.remove();
|
else card.remove();
|
||||||
}
|
}
|
||||||
refresh();
|
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; } },
|
withOwner(callback) { const before = forcedOwner; forcedOwner = true; try { return callback(); } finally { forcedOwner = before; } },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1115,6 +1115,12 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; }
|
|||||||
.mengsan-battle-piles-button-shuying:focus-visible { outline: 3px solid #ffe0a1; outline-offset: 3px; }
|
.mengsan-battle-piles-button-shuying:focus-visible { outline: 3px solid #ffe0a1; outline-offset: 3px; }
|
||||||
.mengsan-deck-dialog-shuying .ms-deck-section { grid-column: 1 / -1; color: var(--ms-gold); font-size: 18px; padding-top: 8px; }
|
.mengsan-deck-dialog-shuying .ms-deck-section { grid-column: 1 / -1; color: var(--ms-gold); font-size: 18px; padding-top: 8px; }
|
||||||
.mengsan-deck-dialog-shuying .ms-deck-order { color: #6b341b; font-weight: 700; font-size: 13px; }
|
.mengsan-deck-dialog-shuying .ms-deck-order { color: #6b341b; font-weight: 700; font-size: 13px; }
|
||||||
|
.mengsan-gm-dialog-shuying { width: min(780px, calc(100vw - 32px)) !important; }
|
||||||
|
.mengsan-gm-dialog-shuying .ms-gm-hand { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; overflow: auto; min-height: 80px; max-height: 38%; padding: 4px; }
|
||||||
|
.mengsan-gm-dialog-shuying .ms-gm-card { text-align: left; overflow-wrap: anywhere; }
|
||||||
|
.mengsan-gm-dialog-shuying .ms-gm-editor { overflow: auto; flex: 1; min-height: 0; border-top: 1px solid #96754e; padding: 12px 4px; }
|
||||||
|
.mengsan-gm-dialog-shuying .ms-gm-affixes { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); gap: 10px; margin-top: 12px; }
|
||||||
|
.mengsan-gm-dialog-shuying .ms-gm-affixes button:disabled { opacity: .55; cursor: not-allowed; }
|
||||||
@media (max-height: 500px) { .mengsan-battle-piles-button-shuying { top: 70px; right: max(8px, env(safe-area-inset-right)); } }
|
@media (max-height: 500px) { .mengsan-battle-piles-button-shuying { top: 70px; right: max(8px, env(safe-area-inset-right)); } }
|
||||||
|
|
||||||
/* Only session-created Mengsan players; retain internal identity and side. */
|
/* Only session-created Mengsan players; retain internal identity and side. */
|
||||||
|
|||||||
Reference in New Issue
Block a user