mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-22 07:10:01 +08:00
fixed # 补全梦三怪物、卡牌
fixed # 修复天书乱斗中,过关技能不会重置次数的问题
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { createCardData } from "./card-data.js";
|
||||
import { cardUpgradeLevel } from "./upgrades.js";
|
||||
|
||||
// Route generated copies explicitly to their actor's real pile; never infer a public pile.
|
||||
export function createBattleCardCopier(game, getActiveBattle) {
|
||||
return (player, card, originals = []) => {
|
||||
const current = getActiveBattle();
|
||||
if (!current?.session.active) return null;
|
||||
const pile = player === game.me ? current.personalPiles : current.monsterPiles.get(player);
|
||||
if (!pile) throw new Error("复制牌的角色没有个人弃牌堆");
|
||||
if (!card || typeof card.name !== "string") throw new TypeError("复制牌缺少牌名");
|
||||
const physical = Array.isArray(originals) ? originals.find(item => item?.name === card.name) : null;
|
||||
const source = card.storage?.mengsanCard_shuying || physical?.storage?.mengsanCard_shuying || {};
|
||||
const serial = (current.generatedCardSerial || 0) + 1;
|
||||
if (!Number.isSafeInteger(serial)) throw new RangeError("战斗生成牌序号越界");
|
||||
const data = createCardData({
|
||||
id: `mengsan_copy_${current.session.id}_${serial}`,
|
||||
name: card.name,
|
||||
suit: card.suit || source.suit || "spade",
|
||||
number: card.number || source.number || 1,
|
||||
nature: card.nature || source.nature || null,
|
||||
upgrade: cardUpgradeLevel({name:card.name, storage:{mengsanCard_shuying:source}}),
|
||||
affixes: (source.affixes || []).filter(key => key !== "annihilate"),
|
||||
}, { enemy: true });
|
||||
// Fresh JSON metadata means no consumed/exhausted flags or source ID are shared.
|
||||
current.generatedCardSerial = serial;
|
||||
return pile.addToDiscard(data);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Actor-local discard recovery. Do not infer ownership from the active global UI pile.
|
||||
export function createBattleCardReclaimer(game, getActiveBattle, get) {
|
||||
return async player => {
|
||||
const current = getActiveBattle();
|
||||
if (!current?.session.active || !player.isAlive()) return null;
|
||||
const getPile = () => player === game.me
|
||||
? current.personalPiles : current.monsterPiles.get(player);
|
||||
const pile = getPile();
|
||||
if (!pile) throw new Error("回收牌的角色没有个人牌堆");
|
||||
const cards = pile.discardCards();
|
||||
if (!cards.length) return null;
|
||||
let chosen = cards[0];
|
||||
if (cards.length > 1) {
|
||||
const result = await player.chooseButton({
|
||||
createDialog: ["头槌:选择一张个人弃牌,放到个人抽牌堆顶部", cards],
|
||||
forced: true, selectButton: 1,
|
||||
ai(button) { return get.value(button.link, player); },
|
||||
}).forResult();
|
||||
if (!result.bool || result.links?.length !== 1) return null;
|
||||
chosen = result.links[0];
|
||||
}
|
||||
// The selection is asynchronous: reject stale battles, piles and removed cards.
|
||||
if (getActiveBattle() !== current || !current.session.active ||
|
||||
!player.isAlive() || getPile() !== pile || !cards.includes(chosen)) return null;
|
||||
if (!pile.moveDiscardToTop(chosen)) return null;
|
||||
game.log(player, "将", chosen, "放到个人抽牌堆顶部");
|
||||
return chosen;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 只向同一场仍在进行的战斗中的存活角色增加当前费用,不改变上限。
|
||||
export function createBattleEnergy(game, getActiveBattle) {
|
||||
const valid = (player, battle) => Boolean(battle &&
|
||||
battle === getActiveBattle() && battle.session?.active &&
|
||||
battle.players?.has(player) && player?.isAlive?.());
|
||||
const validateAmount = (player, amount) => {
|
||||
const current = player.storage?.mengsanEnergy_shuying;
|
||||
if (!Number.isSafeInteger(current) || current < 0 ||
|
||||
!Number.isSafeInteger(amount) || amount <= 0 ||
|
||||
!Number.isSafeInteger(current + amount)) {
|
||||
throw new Error("梦三费用数值无效");
|
||||
}
|
||||
};
|
||||
return {
|
||||
canUse(player) { return valid(player, getActiveBattle()); },
|
||||
capture(player, amount) {
|
||||
const battle = getActiveBattle();
|
||||
if (!valid(player, battle)) return null;
|
||||
validateAmount(player, amount);
|
||||
return battle;
|
||||
},
|
||||
grant(player, amount, battle) {
|
||||
if (!valid(player, battle)) return false;
|
||||
validateAmount(player, amount);
|
||||
player.storage.mengsanEnergy_shuying += amount;
|
||||
battle.energyUI.get(player)?.();
|
||||
if (player === game.me) battle.handUI?.refresh();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 原生格挡完全结算后才抽牌,固定当前战斗和个人牌堆身份。
|
||||
export function createBlockDrawer(game, getActiveBattle) {
|
||||
const valid = (player, battle) => Boolean(battle && getActiveBattle() === battle &&
|
||||
battle.session?.active && battle.players?.has(player) && player?.isAlive?.());
|
||||
const getPile = (player, battle) => player === game.me ? battle.personalPiles : battle.monsterPiles?.get(player);
|
||||
return async function blockDraw(player, block, draw) {
|
||||
if (!Number.isSafeInteger(block) || block < 0 || !Number.isSafeInteger(draw) || draw < 0) {
|
||||
throw new TypeError("格挡抽牌数值无效");
|
||||
}
|
||||
const battle = getActiveBattle();
|
||||
if (!valid(player, battle)) return;
|
||||
const pile = getPile(player, battle);
|
||||
if (typeof pile?.take !== "function") throw new Error("格挡抽牌缺少个人牌堆接口");
|
||||
await player.changeHujia(block);
|
||||
// 格挡的触发事件中可能结束战斗、死亡或换场,不摸新场次的牌。
|
||||
if (!valid(player, battle) || getPile(player, battle) !== pile) return;
|
||||
if (draw > 0) await player.draw(draw);
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,26 @@
|
||||
// 梦三永久牌组的可获得牌注册表。这里登记牌名与归属,不复制引擎中的效果定义。
|
||||
// 新增武将专属牌时填写 owner 为该武将 ID;通用牌填写 owner: null。
|
||||
const definitions = [
|
||||
{ name: "mengsan_songjianwushi", category: "utility", owner: null },
|
||||
{ name: "mengsan_pomie", category: "utility", owner: null },
|
||||
{ name: "mengsan_jianyi", category: "utility", owner: null },
|
||||
{ name: "mengsan_fangxue", category: "utility", owner: null },
|
||||
{ name: "mengsan_yujin", category: "damage", owner: null },
|
||||
{ name: "mengsan_wanmeidaji", category: "damage", owner: null },
|
||||
{ name: "mengsan_yubeidaji", category: "damage", owner: null },
|
||||
{ name: "mengsan_tupo", category: "damage", owner: null },
|
||||
{ name: "mengsan_touchui", category: "damage", owner: null },
|
||||
{ name: "mengsan_tiezhanbo", category: "damage", owner: null },
|
||||
{ name: "mengsan_shuangchongdaji", category: "damage", owner: null },
|
||||
{ name: "mengsan_shandianpili", category: "damage", owner: null },
|
||||
{ name: "sha", category: "damage", owner: null },
|
||||
{ name: "mengsan_zhongsha", category: "damage", owner: null },
|
||||
{ name: "mengsan_fennu", category: "damage", owner: null },
|
||||
{ name: "mengsan_rongrongzhiquan", category: "damage", owner: null },
|
||||
{ name: "mengsan_quanshenzhuangji", category: "damage", owner: null },
|
||||
{ name: "mengsan_jianbingdaji", category: "damage", owner: null },
|
||||
{ name: "mengsan_feijianhuixuanbiao", category: "damage", owner: null },
|
||||
{ name: "mengsan_fangyu", category: "utility", owner: null },
|
||||
{ name: "shan", category: "utility", owner: null },
|
||||
{ name: "tao", category: "recovery", owner: null },
|
||||
{ name: "jiu", category: "recovery", owner: null },
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// 坚毅只消耗使用者当时的手牌:普通牌随机,当前选择模式走原生选牌事件。
|
||||
export function createHandExhauster(game, getActiveBattle, get, randomHandExhaust) {
|
||||
const valid = (player, battle) => Boolean(battle &&
|
||||
getActiveBattle() === battle && battle.session?.active &&
|
||||
battle.players?.has(player) && player?.isAlive?.());
|
||||
const getPile = (player, battle) => player === game.me
|
||||
? battle.personalPiles : battle.monsterPiles?.get(player);
|
||||
const handCards = (player, event) => {
|
||||
const playing = new Set(event?.cards || []);
|
||||
return player.getCards("h").filter(card =>
|
||||
!playing.has(card) && !card.storage?.mengsanExhausted_shuying);
|
||||
};
|
||||
return {
|
||||
capture(player) {
|
||||
const battle = getActiveBattle();
|
||||
if (!valid(player, battle)) return null;
|
||||
if (typeof getPile(player, battle)?.exhaustFromHand !== "function") {
|
||||
throw new Error("坚毅缺少个人消耗牌堆接口");
|
||||
}
|
||||
if (typeof randomHandExhaust?.exhaust !== "function") throw new Error("坚毅缺少随机手牌消耗接口");
|
||||
return { battle, pile: getPile(player, battle) };
|
||||
},
|
||||
async exhaust(player, event, token, choose) {
|
||||
const battle = token?.battle, pile = token?.pile;
|
||||
if (!valid(player, battle) || getPile(player, battle) !== pile) return null;
|
||||
if (typeof pile?.exhaustFromHand !== "function") throw new Error("坚毅缺少个人消耗牌堆接口");
|
||||
if (!choose) return await randomHandExhaust.exhaust(player, event, battle);
|
||||
const cards = handCards(player, event);
|
||||
if (!cards.length) return null;
|
||||
let chosen = cards[0];
|
||||
if (cards.length > 1) {
|
||||
const result = await player.chooseButton({
|
||||
createDialog: ["坚毅:选择消耗一张你的手牌", cards],
|
||||
forced: true, selectButton: 1,
|
||||
ai(button) { return -get.value(button.link, player); },
|
||||
}).forResult();
|
||||
if (!result.bool || result.links?.length !== 1) return null;
|
||||
chosen = result.links[0];
|
||||
}
|
||||
// 选牌期间可能已结束、换场、失去此牌或替换牌堆,不提交陈旧选择。
|
||||
if (!valid(player, battle) || getPile(player, battle) !== pile ||
|
||||
!cards.includes(chosen) || !handCards(player, event).includes(chosen)) return null;
|
||||
return await pile.exhaustFromHand(chosen);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// 破灭通过原生使用事件执行自己的堆顶原牌;授权不写入永久牌数据。
|
||||
import { authorizeFreeCardUse } from "../battle/combat-rules.js";
|
||||
export function createHavocPlayer(game, getActiveBattle, status) {
|
||||
const valid = (player, battle) => Boolean(battle && getActiveBattle() === battle &&
|
||||
battle.session?.active && battle.players?.has(player) && player?.isAlive?.());
|
||||
const getPile = (player, battle) => player === game.me ? battle.personalPiles : battle.monsterPiles?.get(player);
|
||||
return async function playDrawTop(player) {
|
||||
const battle = getActiveBattle();
|
||||
if (!valid(player, battle)) return null;
|
||||
const pile = getPile(player, battle);
|
||||
if (typeof pile?.beginTopPlay !== "function" || typeof pile?.finishTopPlay !== "function") {
|
||||
throw new Error("破灭缺少个人牌堆接口");
|
||||
}
|
||||
const original = pile.beginTopPlay();
|
||||
if (!original) return null;
|
||||
let release = () => {};
|
||||
try {
|
||||
const stillValid = () => valid(player, battle) && getPile(player, battle) === pile;
|
||||
const choice = player.chooseUseTarget({
|
||||
card: original, cards: [original], forced: true, addCount: false,
|
||||
prompt: "破灭:使用抽牌堆顶部的牌",
|
||||
oncard() {
|
||||
// 目标选择期间可能已死亡、结束或换场,不执行陈旧的使用。
|
||||
if (!stillValid()) status.event?.cancel?.();
|
||||
},
|
||||
});
|
||||
release = authorizeFreeCardUse(player, original, choice, stillValid);
|
||||
await choice;
|
||||
} finally {
|
||||
release();
|
||||
// 只认这一张实体原牌,不添加消耗词缀,避免愤怒的副本继承消耗。
|
||||
await pile.finishTopPlay(original);
|
||||
}
|
||||
return original;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { getIntentHostiles } from "../battle/intent-effects.js";
|
||||
import { isPhrog, isPhrogActor } from "../monsters/phrog-intent.js";
|
||||
|
||||
export const INFECTION_NAME = "mengsan_infection_shuying";
|
||||
export function createInfectionData(id) {
|
||||
if (typeof id !== "string" || !id) throw new TypeError("感染牌缺少ID");
|
||||
return { id, name: INFECTION_NAME, suit: "spade", number: 1,
|
||||
nature: null, affixes: ["unplayable"], upgrade: 0 };
|
||||
}
|
||||
export const infectionDefinition = Object.freeze({
|
||||
type: "status", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_infection_shuying.png",
|
||||
enable: false, notarget: true,
|
||||
ai: { value: -5, useful: -5, order: 0 },
|
||||
cardPrompt: () => "不能被打出。回合结束弃牌前,若此牌在你的手牌中,你受到3点非攻击伤害。",
|
||||
});
|
||||
export function applyInfection(game, current, source, count) {
|
||||
if (!current?.session.active || !source?.isAlive() || !isPhrogActor(source)) return;
|
||||
if (count !== (isPhrog(source) ? 3 : 1)) throw new RangeError("感染牌数量与怪物不符");
|
||||
if (!current.infectionCards) {
|
||||
const entry = current.infectionCards = { sequence: 0, resolved: new WeakSet() };
|
||||
current.session.ownResource(entry, () => { if (current.infectionCards === entry) delete current.infectionCards; });
|
||||
}
|
||||
for (const target of getIntentHostiles(game, source)) {
|
||||
if (!current.session.active || !source.isAlive()) break;
|
||||
const pile = target === game.me ? current.personalPiles : current.monsterPiles.get(target);
|
||||
if (!pile) continue;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (!current.session.active || !source.isAlive()) break;
|
||||
const sequence = current.infectionCards.sequence + 1;
|
||||
if (!Number.isSafeInteger(sequence)) throw new RangeError("感染牌序号溢出");
|
||||
current.infectionCards.sequence = sequence;
|
||||
pile.addToDiscard(createInfectionData(`infection_${source.playerid}_${sequence}`));
|
||||
}
|
||||
game.log(target, `的个人弃牌堆加入${count}张【感染】`);
|
||||
}
|
||||
}
|
||||
export const infectionHand = player => [...new Set(player?.getCards("h") || [])].filter(card =>
|
||||
card.name === INFECTION_NAME && card.storage?.mengsanOwnerId_shuying === player.playerid);
|
||||
const resolutions = new WeakMap();
|
||||
export async function resolveInfection(current, player, phase) {
|
||||
if (!current?.session.active || !player?.isAlive() || !current.players.has(player)) return false;
|
||||
let resolved = resolutions.get(current);
|
||||
if (!resolved) { resolved = new WeakSet(); resolutions.set(current, resolved); }
|
||||
if (resolved.has(phase)) return false;
|
||||
resolved.add(phase);
|
||||
const cards = infectionHand(player);
|
||||
for (const card of cards) {
|
||||
if (!current.session.active || !player.isAlive()) break;
|
||||
if (!infectionHand(player).includes(card)) continue;
|
||||
const damage = player.damage(3, "nosource");
|
||||
damage.mengsanInfection_shuying = true;
|
||||
await damage;
|
||||
}
|
||||
return cards.length > 0;
|
||||
}
|
||||
+675
-16
@@ -2,17 +2,23 @@
|
||||
// 保留原牌名,以兼容原有牌的响应、装备技能及转化牌引用。
|
||||
import standard from "../../../../card/standard.js";
|
||||
import extra from "../../../../card/extra.js";
|
||||
import { _status, game } from "../../../../noname.js";
|
||||
import { _status, game, get } from "../../../../noname.js";
|
||||
import { cardCost, canPayCard, isActiveCardUse, SHA_DAMAGE,
|
||||
TRICK_DAMAGE } from "../battle/combat-rules.js";
|
||||
import { cardUpgradeLevel, cardUpgradeRule } from "./upgrades.js";
|
||||
|
||||
import { applyMengsanDebuff } from "../monsters/artifact-status.js";
|
||||
import { isAttackCard } from "../monsters/vine-tangled.js";
|
||||
import { DAZED_NAME, dazedDefinition } from "./status-cards.js";
|
||||
import { SLIMED_NAME, slimedDefinition } from "./slimed-card.js";
|
||||
import { INFECTION_NAME, infectionDefinition } from "./infection-card.js";
|
||||
|
||||
const packs = [standard, extra];
|
||||
const names = [...new Set(packs.flatMap(pack => pack.list.map(entry => entry[2])))];
|
||||
const sourceCard = name => standard.card[name] || extra.card[name];
|
||||
const sourceText = name => extra.translate[`${name}_info`] || standard.translate[`${name}_info`];
|
||||
|
||||
export function createMengsanCards() {
|
||||
export function createMengsanCards({ copyToDiscard, reclaimFromDiscard, temporaryStrength, countStrikeCards, randomHandExhaust, battleEnergy, handExhaust, playDrawTop, blockDraw, isBattleActive = () => true } = {}) {
|
||||
const card = {};
|
||||
const translate = {};
|
||||
for (const name of names) {
|
||||
@@ -52,14 +58,12 @@ export function createMengsanCards() {
|
||||
const damage = cardUpgradeLevel(used)
|
||||
? cardUpgradeRule("sha").damage : SHA_DAMAGE;
|
||||
return `梦三:主动使用消耗1费用;目标须使用【闪】,` +
|
||||
`否则受到${damage}点伤害。每回合使用次数不限。` +
|
||||
(cardUpgradeLevel(used) ? "已强化(上限1次)。" :
|
||||
"可强化1次:伤害变为9点。");
|
||||
`否则受到${damage}点伤害。每回合使用次数不限。`;
|
||||
};
|
||||
translate.sha_info = `梦三:主动使用消耗1费用。出牌阶段,对攻击范围内的一名角色使用;其须使用【闪】,否则受到${SHA_DAMAGE}点伤害。每回合使用次数不限。`;
|
||||
card.mengsan_zhongsha = {
|
||||
type: "basic",
|
||||
fullskin: true,
|
||||
fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_zhongsha.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_zhongsha" }),
|
||||
enable(used, player, event) {
|
||||
@@ -79,12 +83,8 @@ export function createMengsanCards() {
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
if (!target.isAlive()) return;
|
||||
target.storage.mengsanVulnerable_shuying =
|
||||
(target.storage.mengsanVulnerable_shuying || 0) +
|
||||
effect.vulnerable;
|
||||
target.addSkill("mengsan_vulnerable_shuying");
|
||||
target.markSkill("mengsan_vulnerable_shuying");
|
||||
game.log(target, `获得${effect.vulnerable}层易伤`);
|
||||
if (applyMengsanDebuff(target, "vulnerable", effect.vulnerable))
|
||||
game.log(target, `获得${effect.vulnerable}层易伤`);
|
||||
},
|
||||
ai: { order: 4, result: { target: -1.5 },
|
||||
tag: { damage: 1 } },
|
||||
@@ -96,9 +96,7 @@ export function createMengsanCards() {
|
||||
`一名其他角色造成${effect.damage}点伤害,然后给予其` +
|
||||
`${effect.vulnerable}层易伤。受到攻击伤害增加50%` +
|
||||
"(向上取整),每个自身回合结束减少1层。" +
|
||||
"每回合使用次数不限。" + (cardUpgradeLevel(used)
|
||||
? "已强化(上限1次)。" :
|
||||
"可强化1次:10点伤害、3层易伤。");
|
||||
"每回合使用次数不限。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_zhongsha = "重杀";
|
||||
@@ -107,6 +105,648 @@ export function createMengsanCards() {
|
||||
"一名其他角色造成8点伤害,然后给予其2层易伤。" +
|
||||
"易伤使受到的攻击伤害增加50%(向上取整)," +
|
||||
"每个自身回合结束减少1层。每回合使用次数不限。";
|
||||
// STS2 Setup Strike: the new temporary strength does not enhance its own hit.
|
||||
// STS2 Cinder: damage first, then exhaust one random remaining hand card.
|
||||
card.mengsan_yujin = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_yujin.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_yujin" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
if (!isBattleActive() || !player.isAlive() || !target?.isAlive() ||
|
||||
target === player || !player.isEnemyOf(target)) return;
|
||||
if (!randomHandExhaust?.capture || !randomHandExhaust?.exhaust) throw new Error("余烬缺少随机消耗接口");
|
||||
const battle = randomHandExhaust.capture();
|
||||
if (!battle) return;
|
||||
const rule = cardUpgradeRule("mengsan_yujin");
|
||||
const damage = target.damage(cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
if (isBattleActive() && player.isAlive()) await randomHandExhaust.exhaust(player, event, battle);
|
||||
},
|
||||
ai: { order: 6, result: { target: -2 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_yujin");
|
||||
const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return `梦三:主动使用消耗2费用。对一名敌方角色造成${amount}点伤害,然后随机消耗一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。每回合使用次数不限。`;
|
||||
},
|
||||
};
|
||||
translate.mengsan_yujin = "余烬";
|
||||
translate.mengsan_yujin_info = card.mengsan_yujin.cardPrompt({ name: "mengsan_yujin" });
|
||||
// STS2 Perfected Strike counts current physical combat cards, including itself/exhaust.
|
||||
card.mengsan_wanmeidaji = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_wanmeidaji.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_wanmeidaji" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
if (!isBattleActive() || !player.isAlive() || !target?.isAlive() ||
|
||||
target === player || !player.isEnemyOf(target)) return;
|
||||
if (typeof countStrikeCards !== "function") throw new Error("完美打击缺少打击牌计数接口");
|
||||
const count = countStrikeCards(player, event);
|
||||
if (!Number.isSafeInteger(count) || count < 0) throw new RangeError("打击牌数量无效");
|
||||
const rule = cardUpgradeRule("mengsan_wanmeidaji");
|
||||
const amount = rule.baseDamage + count * (cardUpgradeLevel(event.card) ? rule.perStrike : rule.basePerStrike);
|
||||
if (!Number.isSafeInteger(amount) || amount < 0) throw new RangeError("完美打击伤害无效");
|
||||
const damage = target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
damage.mengsanStrikeCount_shuying = count;
|
||||
await damage;
|
||||
},
|
||||
ai: { order: 7, result: { target: -2 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_wanmeidaji");
|
||||
const bonus = cardUpgradeLevel(used) ? rule.perStrike : rule.basePerStrike;
|
||||
return `梦三:主动使用消耗2费用。对一名敌方角色造成6点伤害,本场战斗的牌中每有一张名称含“打击”的牌,伤害增加${bonus}点(包括手牌、个人抽牌堆、个人弃牌堆、消耗牌及打出区)。每回合使用次数不限。`;
|
||||
},
|
||||
};
|
||||
translate.mengsan_wanmeidaji = "完美打击";
|
||||
translate.mengsan_wanmeidaji_info = card.mengsan_wanmeidaji.cardPrompt({ name: "mengsan_wanmeidaji" });
|
||||
card.mengsan_yubeidaji = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_yubeidaji.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_yubeidaji" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
if (!isBattleActive() || !player.isAlive() || !target?.isAlive() ||
|
||||
target === player || !player.isEnemyOf(target)) return;
|
||||
if (!temporaryStrength?.capture || !temporaryStrength?.grant) throw new Error("预备打击缺少临时力量接口");
|
||||
const battle = temporaryStrength.capture();
|
||||
if (!battle) return;
|
||||
const rule = cardUpgradeRule("mengsan_yubeidaji");
|
||||
const upgraded = cardUpgradeLevel(event.card);
|
||||
const damage = target.damage(upgraded ? rule.damage : rule.baseDamage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
if (isBattleActive() && player.isAlive()) {
|
||||
temporaryStrength.grant(player, upgraded ? rule.strength : rule.baseStrength, event, battle);
|
||||
}
|
||||
},
|
||||
ai: { order: 8, result: { player: 0.8, target: -1.5 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_yubeidaji");
|
||||
const upgraded = cardUpgradeLevel(used);
|
||||
return `梦三:主动使用消耗1费用。对一名敌方角色造成${upgraded ? rule.damage : rule.baseDamage}点伤害,然后在本回合内获得${upgraded ? rule.strength : rule.baseStrength}点力量。每回合使用次数不限。`;
|
||||
},
|
||||
};
|
||||
translate.mengsan_yubeidaji = "预备打击";
|
||||
translate.mengsan_yubeidaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成7点伤害,然后在本回合内获得2点力量。每回合使用次数不限。";
|
||||
// STS2 Breakthrough: native life loss is paid once before the all-enemy attack.
|
||||
card.mengsan_tupo = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_tupo.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_tupo" }),
|
||||
notarget: true, usable: Infinity,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
if (!game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target)).length) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
if (!game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target)).length) return;
|
||||
// loseHp bypasses block and awaits the normal dying/rescue process.
|
||||
await player.loseHp(1);
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
const rule = cardUpgradeRule("mengsan_tupo");
|
||||
const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage;
|
||||
const targets = game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target));
|
||||
for (const target of targets) {
|
||||
if (!isBattleActive() || !player.isAlive()) break;
|
||||
if (!target.isAlive() || !player.isEnemyOf(target)) continue;
|
||||
player.line(target, "green");
|
||||
const damage = target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
}
|
||||
},
|
||||
ai: { order: 7, result: { player(player) { return player.hp <= 1 ? -10 : 1; } }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_tupo");
|
||||
const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return `梦三:主动使用消耗1费用。失去1点生命,然后对所有敌方角色造成${amount}点伤害。每回合使用次数不限。`;
|
||||
},
|
||||
};
|
||||
translate.mengsan_tupo = "突破";
|
||||
translate.mengsan_tupo_info = "梦三:主动使用消耗1费用。失去1点生命,然后对所有敌方角色造成9点伤害。每回合使用次数不限。";
|
||||
// STS2 Headbutt: damage first, then choose from the actor's own real discard pile.
|
||||
card.mengsan_touchui = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_touchui.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_touchui" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
if (!isBattleActive() || !player.isAlive() || !target?.isAlive() ||
|
||||
target === player || !player.isEnemyOf(target)) return;
|
||||
if (typeof reclaimFromDiscard !== "function") throw new Error("头槌缺少个人弃牌回收接口");
|
||||
const rule = cardUpgradeRule("mengsan_touchui");
|
||||
const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage;
|
||||
const damage = target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
// Defeating one enemy does not prevent recovery unless combat actually ends.
|
||||
if (isBattleActive() && player.isAlive()) await reclaimFromDiscard(player);
|
||||
},
|
||||
ai: { order: 6, result: { player: 0.4, target: -1.5 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_touchui");
|
||||
const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return '梦三:主动使用消耗1费用。对一名敌方角色造成' + amount +
|
||||
'点伤害,然后选择个人弃牌堆中的一张牌放到个人抽牌堆顶部。每回合使用次数不限。';
|
||||
},
|
||||
};
|
||||
translate.mengsan_touchui = "头槌";
|
||||
translate.mengsan_touchui_info = "梦三:主动使用消耗1费用。对一名敌方角色造成9点伤害," +
|
||||
"然后选择个人弃牌堆中的一张牌放到个人抽牌堆顶部。每回合使用次数不限。";
|
||||
// STS2 Iron Wave: obtain block before resolving the attack.
|
||||
card.mengsan_tiezhanbo = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_tiezhanbo.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_tiezhanbo" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
const canResolve = () => isBattleActive() && player.isAlive() &&
|
||||
target?.isAlive() && target !== player && player.isEnemyOf(target);
|
||||
if (!canResolve()) return;
|
||||
const rule = cardUpgradeRule("mengsan_tiezhanbo");
|
||||
const upgraded = cardUpgradeLevel(event.card) > 0;
|
||||
await player.changeHujia(upgraded ? rule.block : rule.baseBlock);
|
||||
// Block triggers may end combat or remove the acting player/target.
|
||||
if (!canResolve()) return;
|
||||
const damage = target.damage(upgraded ? rule.damage : rule.baseDamage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
},
|
||||
ai: { order: 7, result: { player: 0.6, target: -1.2 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_tiezhanbo");
|
||||
const upgraded = cardUpgradeLevel(used) > 0;
|
||||
const block = upgraded ? rule.block : rule.baseBlock;
|
||||
const damage = upgraded ? rule.damage : rule.baseDamage;
|
||||
return '梦三:主动使用消耗1费用。获得' + block +
|
||||
'点格挡(护甲),然后对一名敌方角色造成' + damage +
|
||||
'点伤害。每回合使用次数不限。';
|
||||
},
|
||||
};
|
||||
translate.mengsan_tiezhanbo = "铁斩波";
|
||||
translate.mengsan_tiezhanbo_info = "梦三:主动使用消耗1费用。获得5点格挡(护甲)," +
|
||||
"然后对一名敌方角色造成5点伤害。每回合使用次数不限。";
|
||||
// STS2 Twin Strike: each hit resolves on the same target separately.
|
||||
card.mengsan_shuangchongdaji = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_shuangchongdaji.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_shuangchongdaji" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
const rule = cardUpgradeRule("mengsan_shuangchongdaji");
|
||||
const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage;
|
||||
for (let hitIndex = 0; hitIndex < rule.hits; hitIndex++) {
|
||||
if (!isBattleActive() || !player.isAlive() || !target?.isAlive() ||
|
||||
target === player || !player.isEnemyOf(target)) break;
|
||||
const damage = target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
}
|
||||
},
|
||||
ai: { order: 6, result: { target: -1.5 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_shuangchongdaji");
|
||||
const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return '梦三:主动使用消耗1费用。对一名敌方角色造成' +
|
||||
amount + '点伤害两次,两次伤害分别结算。每回合使用次数不限。';
|
||||
},
|
||||
};
|
||||
translate.mengsan_shuangchongdaji = "双重打击";
|
||||
translate.mengsan_shuangchongdaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成5点伤害两次," +
|
||||
"两次伤害分别结算。每回合使用次数不限。";
|
||||
// STS2 Thunderclap: hit each living enemy before adding new vulnerable.
|
||||
card.mengsan_shandianpili = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_shandianpili.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_shandianpili" }),
|
||||
notarget: true, usable: Infinity,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
if (!game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target)).length) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
const rule = cardUpgradeRule("mengsan_shandianpili");
|
||||
const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage;
|
||||
const targets = game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target));
|
||||
for (const target of targets) {
|
||||
if (!isBattleActive() || !player.isAlive()) break;
|
||||
if (!target.isAlive() || !player.isEnemyOf(target)) continue;
|
||||
player.line(target, "green");
|
||||
const damage = target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
}
|
||||
// 伤害结束后重新取得存活敌人,包含异蛙亡语刚生成的扭动虫。
|
||||
for (const target of game.filterPlayer(target => target !== player && target.isAlive() && player.isEnemyOf(target))) {
|
||||
if (!isBattleActive() || !player.isAlive()) break;
|
||||
if (applyMengsanDebuff(target, "vulnerable", rule.vulnerable))
|
||||
game.log(target, `获得${rule.vulnerable}层易伤`);
|
||||
}
|
||||
},
|
||||
ai: { order: 7, result: { player: 1 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_shandianpili");
|
||||
const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return `梦三:主动使用消耗1费用。对所有敌方角色造成${amount}点伤害,然后给予其1层易伤。` +
|
||||
"易伤使受到的攻击伤害增加50%(向上取整),每个自身回合结束减少1层。每回合使用次数不限。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_shandianpili = "闪电霹雳";
|
||||
translate.mengsan_shandianpili_info = "梦三:主动使用消耗1费用。对所有敌方角色造成4点伤害,然后给予其1层易伤。" +
|
||||
"易伤使受到的攻击伤害增加50%(向上取整),每个自身回合结束减少1层。每回合使用次数不限。";
|
||||
// Each throw resolves separately and samples the still-living enemies again.
|
||||
card.mengsan_feijianhuixuanbiao = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_feijianhuixuanbiao.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_feijianhuixuanbiao" }),
|
||||
notarget: true, usable: Infinity,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
if (!game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target)).length) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const rule = cardUpgradeRule("mengsan_feijianhuixuanbiao");
|
||||
const count = cardUpgradeLevel(event.card) ? rule.hits : rule.baseHits;
|
||||
for (let hitIndex = 0; hitIndex < count; hitIndex++) {
|
||||
if (!isBattleActive() || !player.isAlive()) break;
|
||||
const enemies = game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target));
|
||||
if (!enemies.length) break;
|
||||
// Use the engine helper with an inclusive index range.
|
||||
const target = enemies[get.rand(0, enemies.length - 1)];
|
||||
player.line(target, "green");
|
||||
const damage = target.damage(rule.damage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
}
|
||||
},
|
||||
ai: { order: 6, result: { player: 1 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_feijianhuixuanbiao");
|
||||
const count = cardUpgradeLevel(used) ? rule.hits : rule.baseHits;
|
||||
return `梦三:主动使用消耗1费用。每次对随机一名敌方角色造成${rule.damage}点伤害,共${count}次。` +
|
||||
"每次攻击重新随机选择存活的敌人,可以重复命中同一敌人。" +
|
||||
"每回合使用次数不限。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_feijianhuixuanbiao = "飞剑回旋镖";
|
||||
translate.mengsan_feijianhuixuanbiao_info = "梦三:主动使用消耗1费用。随机对敌人造成3点伤害3次。" +
|
||||
"每次攻击重新随机选择存活的敌人,可以重复命中同一敌人。每回合使用次数不限。";
|
||||
// Resolve damage before drawing from the acting player's mode-owned pile.
|
||||
card.mengsan_jianbingdaji = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_jianbingdaji.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_jianbingdaji" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
const rule = cardUpgradeRule("mengsan_jianbingdaji");
|
||||
const upgraded = cardUpgradeLevel(event.card) > 0;
|
||||
const damage = event.target.damage(upgraded ? rule.damage : rule.baseDamage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
// Defeating the last enemy may dispose the personal pile during damage.
|
||||
if (isBattleActive() && player.isAlive()) {
|
||||
await player.draw(upgraded ? rule.draw : rule.baseDraw);
|
||||
}
|
||||
},
|
||||
ai: { order: 6, result: { player: 0.6, target: -1.5 }, tag: { damage: 1, draw: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_jianbingdaji");
|
||||
const upgraded = cardUpgradeLevel(used) > 0;
|
||||
const damage = upgraded ? rule.damage : rule.baseDamage;
|
||||
const draw = upgraded ? rule.draw : rule.baseDraw;
|
||||
return `梦三:主动使用消耗1费用。对一名敌方角色造成${damage}点伤害,然后抽${draw}张牌。` +
|
||||
"每回合使用次数不限。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_jianbingdaji = "剑柄打击";
|
||||
translate.mengsan_jianbingdaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成9点伤害,然后抽1张牌。" +
|
||||
"每回合使用次数不限。";
|
||||
// Block determines base damage without spending the acting player's block.
|
||||
card.mengsan_quanshenzhuangji = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_quanshenzhuangji.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_quanshenzhuangji" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
const block = Number.isFinite(player.hujia) ? Math.max(0, player.hujia) : 0;
|
||||
const storedStrength = player.storage?.mengsanStrength_shuying;
|
||||
const strength = Number.isInteger(storedStrength) ? storedStrength : 0;
|
||||
// Native damage(0) stops before damageBegin1, so strength must be included here.
|
||||
const temporary = temporaryStrength?.amount(player) || 0;
|
||||
const damage = event.target.damage(Math.max(0, block + strength + temporary), player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
damage.mengsanCardStrengthApplied_shuying = true;
|
||||
damage.mengsanSetupStrengthApplied_shuying = true;
|
||||
await damage;
|
||||
},
|
||||
ai: {
|
||||
order: 5,
|
||||
result: { target(used, player) { return player.hujia > 0 ? -1.5 : 0; } },
|
||||
tag: { damage: 1 },
|
||||
},
|
||||
cardPrompt(used) {
|
||||
const cost = cardCost(used || { name: "mengsan_quanshenzhuangji" });
|
||||
return `梦三:主动使用消耗${cost}费用。对一名敌方角色造成等同于你当前格挡(护甲)值的伤害。` +
|
||||
"不消耗格挡,可以受到力量加成。" +
|
||||
"每回合使用次数不限。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_quanshenzhuangji = "全身撞击";
|
||||
translate.mengsan_quanshenzhuangji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成等同于你当前格挡(护甲)值的伤害。" +
|
||||
"不消耗格挡,可以受到力量加成。每回合使用次数不限。";
|
||||
// Double only the surviving target's existing vulnerable after damage resolves.
|
||||
card.mengsan_rongrongzhiquan = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_rongrongzhiquan.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_rongrongzhiquan" }),
|
||||
mengsanExhaust_shuying: true,
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
const rule = cardUpgradeRule("mengsan_rongrongzhiquan");
|
||||
const target = event.target;
|
||||
const damage = target.damage(cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
if (!target.isAlive() || !isBattleActive()) return;
|
||||
const stacks = target.storage?.mengsanVulnerable_shuying;
|
||||
if (!Number.isSafeInteger(stacks) || stacks <= 0) return;
|
||||
const doubled = stacks * 2;
|
||||
if (!Number.isSafeInteger(doubled)) throw new RangeError("易伤层数越界");
|
||||
if (applyMengsanDebuff(target, "vulnerable", stacks))
|
||||
game.log(target, `易伤层数从${stacks}翻倍为${doubled}`);
|
||||
},
|
||||
ai: { order: 6, result: { target: -1.5 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_rongrongzhiquan");
|
||||
const damage = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return `梦三:主动使用消耗1费用。对一名敌方角色造成${damage}点伤害,然后将该目标已有的易伤层数翻倍。` +
|
||||
"没有易伤时不会新增易伤。消耗:使用后退出本场战斗,不进入弃牌堆或洗牌。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_rongrongzhiquan = "熔融之拳";
|
||||
translate.mengsan_rongrongzhiquan_info = "梦三:主动使用消耗1费用。对一名敌方角色造成10点伤害,然后将该目标已有的易伤层数翻倍。" +
|
||||
"消耗:使用后退出本场战斗。";
|
||||
// STS2【愤怒】:攻击后生成独立的战斗复制品,不扩充永久牌组。
|
||||
card.mengsan_fennu = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_fennu.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_fennu" }),
|
||||
enable(used, player, event) {
|
||||
return !isActiveCardUse(event || _status.event, player) ||
|
||||
canPayCard(player, used);
|
||||
},
|
||||
usable: Infinity, selectTarget: 1,
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (typeof copyToDiscard !== "function") throw new Error("愤怒缺少战斗复制接口");
|
||||
const amount = cardUpgradeLevel(event.card)
|
||||
? cardUpgradeRule("mengsan_fennu").damage : 6;
|
||||
const damage = event.target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
if (copyToDiscard(player, event.card, event.cards)) {
|
||||
game.log(player, "将一张【愤怒】的复制品加入个人弃牌堆");
|
||||
}
|
||||
},
|
||||
ai: { order: 6, result: { target: -1 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const amount = cardUpgradeLevel(used)
|
||||
? cardUpgradeRule("mengsan_fennu").damage : 6;
|
||||
return `梦三:主动使用消耗0费用。对一名敌方角色造成${amount}点伤害,` +
|
||||
"然后将一张此牌的复制品加入你的个人弃牌堆。" +
|
||||
"复制品保留原牌状态和词缀,仅存在于本场战斗。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_fennu = "愤怒";
|
||||
translate.mengsan_fennu_info = "梦三:主动使用消耗0费用。对一名敌方角色造成6点伤害," +
|
||||
"然后将一张此牌的复制品加入你的个人弃牌堆。复制品保留原牌状态和词缀,仅存在于本场战斗。" +
|
||||
"每回合使用次数不限。";
|
||||
// STS2【放血】:先等待原生生命流失及濒死救援,再增加当前战斗费用。
|
||||
card.mengsan_fangxue = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_fangxue.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_fangxue" }),
|
||||
enable(used, player, event) {
|
||||
return isBattleActive() && Boolean(battleEnergy?.canUse(player)) &&
|
||||
(!isActiveCardUse(event || _status.event, player) || canPayCard(player, used));
|
||||
},
|
||||
usable: Infinity, toself: true, selectTarget: -1,
|
||||
filterTarget(used, player, target) { return target === player; },
|
||||
async content(event, trigger, player) {
|
||||
if (typeof battleEnergy?.capture !== "function" || typeof battleEnergy?.grant !== "function") {
|
||||
throw new Error("放血缺少战斗费用接口");
|
||||
}
|
||||
const rule = cardUpgradeRule("mengsan_fangxue");
|
||||
const amount = cardUpgradeLevel(event.card) ? rule.energy : rule.baseEnergy;
|
||||
const battle = battleEnergy.capture(player, amount);
|
||||
if (!battle) return;
|
||||
await player.loseHp(rule.loseHp);
|
||||
battleEnergy.grant(player, amount, battle);
|
||||
},
|
||||
ai: { order: 9, result: { player: player => player.hp > 3 ? 1 : -1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_fangxue");
|
||||
const amount = cardUpgradeLevel(used) ? rule.energy : rule.baseEnergy;
|
||||
return "梦三:主动使用消耗0费用。失去" + rule.loseHp + "点生命,然后获得" + amount + "点费用。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_fangxue = "放血";
|
||||
translate.mengsan_fangxue_info = "梦三:主动使用消耗0费用。失去3点生命,然后获得2点费用。";
|
||||
// STS2【坚毅】:先获得格挡,再消耗一张自身当时的手牌。
|
||||
card.mengsan_jianyi = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_jianyi.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_jianyi" }),
|
||||
enable(used, player, event) {
|
||||
return isBattleActive() && Boolean(player?.isAlive?.()) &&
|
||||
(!isActiveCardUse(event || _status.event, player) || canPayCard(player, used));
|
||||
},
|
||||
usable: Infinity, toself: true, selectTarget: -1,
|
||||
filterTarget(used, player, target) { return target === player; },
|
||||
async content(event, trigger, player) {
|
||||
if (typeof handExhaust?.capture !== "function" || typeof handExhaust?.exhaust !== "function") {
|
||||
throw new Error("坚毅缺少手牌消耗接口");
|
||||
}
|
||||
const battle = handExhaust.capture(player);
|
||||
if (!battle) return;
|
||||
const rule = cardUpgradeRule("mengsan_jianyi");
|
||||
const current = cardUpgradeLevel(event.card);
|
||||
await player.changeHujia(current ? rule.block : rule.baseBlock);
|
||||
await handExhaust.exhaust(player, event, battle, Boolean(current && rule.chooseExhaust));
|
||||
},
|
||||
ai: { order: 7, result: { player: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_jianyi");
|
||||
const current = cardUpgradeLevel(used);
|
||||
const block = current ? rule.block : rule.baseBlock;
|
||||
return "梦三:主动使用消耗1费用。获得" + block + "点格挡(护甲),然后" +
|
||||
(current && rule.chooseExhaust ? "选择消耗" : "随机消耗") +
|
||||
"一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_jianyi = "坚毅";
|
||||
translate.mengsan_jianyi_info = "梦三:主动使用消耗1费用。获得7点格挡(护甲),然后随机消耗一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。";
|
||||
// STS2【破灭】:免费使用自己的堆顶原牌,再将这一原牌消耗。
|
||||
card.mengsan_pomie = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_pomie.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_pomie" }),
|
||||
enable(used, player, event) {
|
||||
return isBattleActive() && Boolean(player?.isAlive?.()) &&
|
||||
(!isActiveCardUse(event || _status.event, player) || canPayCard(player, used));
|
||||
},
|
||||
usable: Infinity, toself: true, selectTarget: -1,
|
||||
filterTarget(used, player, target) { return target === player; },
|
||||
async content(event, trigger, player) {
|
||||
if (typeof playDrawTop !== "function") throw new Error("破灭缺少堆顶使用接口");
|
||||
await playDrawTop(player);
|
||||
},
|
||||
ai: { order: 6, result: { player: 1 } },
|
||||
cardPrompt(used) {
|
||||
return "梦三:主动使用消耗" + cardCost(used) +
|
||||
"费用。免费使用你抽牌堆顶部的牌,然后将该原牌消耗(退出本场战斗,不进入弃牌堆或洗牌)。抽牌堆为空时不生效。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_pomie = "破灭";
|
||||
translate.mengsan_pomie_info = "梦三:主动使用消耗1费用。免费使用你抽牌堆顶部的牌,然后将该原牌消耗(退出本场战斗,不进入弃牌堆或洗牌)。抽牌堆为空时不生效。";
|
||||
// STS2【耸肩无视】:先获得格挡,再抽一张牌。
|
||||
card.mengsan_songjianwushi = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_songjianwushi.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_songjianwushi" }),
|
||||
enable(used, player, event) {
|
||||
return isBattleActive() && Boolean(player?.isAlive?.()) &&
|
||||
(!isActiveCardUse(event || _status.event, player) || canPayCard(player, used));
|
||||
},
|
||||
usable: Infinity, toself: true, selectTarget: -1,
|
||||
filterTarget(used, player, target) { return target === player; },
|
||||
async content(event, trigger, player) {
|
||||
if (typeof blockDraw !== "function") throw new Error("耸肩无视缺少格挡抽牌接口");
|
||||
const rule = cardUpgradeRule("mengsan_songjianwushi");
|
||||
await blockDraw(player, cardUpgradeLevel(event.card) ? rule.block : rule.baseBlock, rule.draw);
|
||||
},
|
||||
ai: { order: 7, result: { player: 1 }, tag: { draw: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_songjianwushi");
|
||||
const block = cardUpgradeLevel(used) ? rule.block : rule.baseBlock;
|
||||
return "梦三:主动使用消耗1费用。获得" + block + "点格挡(护甲),然后抽" + rule.draw + "张牌。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_songjianwushi = "耸肩无视";
|
||||
translate.mengsan_songjianwushi_info = "梦三:主动使用消耗1费用。获得8点格挡(护甲),然后抽1张牌。";
|
||||
// 【防御】沿用梦三护甲事件,保留脆弱与伤害抵扣等既有规则。
|
||||
card.mengsan_fangyu = {
|
||||
type: "basic",
|
||||
fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_fangyu.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_fangyu" }),
|
||||
enable(used, player, event) {
|
||||
return !isActiveCardUse(event || _status.event, player) ||
|
||||
canPayCard(player, used);
|
||||
},
|
||||
usable: Infinity,
|
||||
toself: true,
|
||||
selectTarget: -1,
|
||||
filterTarget(used, player, target) { return target === player; },
|
||||
async content(event, trigger, player) {
|
||||
await player.changeHujia(5);
|
||||
},
|
||||
ai: { order: 7, result: { target: 1 } },
|
||||
cardPrompt() { return translate.mengsan_fangyu_info; },
|
||||
};
|
||||
translate.mengsan_fangyu = "防御";
|
||||
translate.mengsan_fangyu_info =
|
||||
"梦三:主动使用消耗1费用。出牌阶段,对自己使用,获得5点格挡(护甲)。" +
|
||||
"每回合使用次数不限。";
|
||||
// 【桃】的回复量也由梦三牌本体提供;救援仍不消耗费用。
|
||||
card.tao.content = async function (event) { await event.target.recover(8); };
|
||||
translate.tao_info = "梦三:主动使用消耗1费用,回复8点生命;濒死救援不消耗费用,同样回复8点生命。";
|
||||
@@ -139,5 +779,24 @@ export function createMengsanCards() {
|
||||
};
|
||||
translate.shandian_info = "梦三:主动使用消耗1费用。若判定为黑桃2~9,目标受到12点雷电伤害;否则移至下家的判定区。";
|
||||
card.shandian.cardPrompt = () => translate.shandian_info;
|
||||
return { card, translate, names: [...names, "mengsan_zhongsha"] };
|
||||
card[INFECTION_NAME] = { ...infectionDefinition };
|
||||
translate[INFECTION_NAME] = "感染";
|
||||
translate[`${INFECTION_NAME}_info`] = infectionDefinition.cardPrompt();
|
||||
card[SLIMED_NAME] = { ...slimedDefinition };
|
||||
translate[SLIMED_NAME] = "黏液";
|
||||
translate[`${SLIMED_NAME}_info`] = slimedDefinition.cardPrompt();
|
||||
card[DAZED_NAME] = { ...dazedDefinition };
|
||||
translate[DAZED_NAME] = "晕眩";
|
||||
translate[`${DAZED_NAME}_info`] = dazedDefinition.cardPrompt();
|
||||
for (const [name, definition] of Object.entries(card)) {
|
||||
if (!isAttackCard({ name }) || typeof definition.cardPrompt !== "function") continue;
|
||||
const prompt = definition.cardPrompt;
|
||||
definition.cardPrompt = function (used) {
|
||||
const text = prompt.call(this, used);
|
||||
const owner = used && typeof get.owner === "function" ? get.owner(used) : null;
|
||||
if (!isBattleActive() || !owner?.storage?.mengsanTangled_shuying || typeof text !== "string") return text;
|
||||
return text.replace(/主动使用消耗\d+费用/, `主动使用消耗${cardCost(used || { name }, owner)}费用`);
|
||||
};
|
||||
}
|
||||
return { card, translate, names: [...names, "mengsan_zhongsha", "mengsan_fangyu", "mengsan_fennu", "mengsan_feijianhuixuanbiao", "mengsan_jianbingdaji", "mengsan_quanshenzhuangji", "mengsan_rongrongzhiquan", "mengsan_shandianpili", "mengsan_shuangchongdaji", "mengsan_tiezhanbo", "mengsan_touchui", "mengsan_tupo", "mengsan_yubeidaji", "mengsan_wanmeidaji", "mengsan_yujin", "mengsan_fangxue", "mengsan_jianyi", "mengsan_pomie", "mengsan_songjianwushi"] };
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
if (!descriptor || !descriptor.configurable || (!Object.hasOwn(descriptor, "value") && typeof descriptor.get !== "function")) throw new Error("Unsupported pile property: " + key);
|
||||
}
|
||||
const draw = document.createElement("div"), discard = document.createElement("div");
|
||||
const exhausted = document.createElement("div"); exhausted.id = "special";
|
||||
// Detached nodes: engine get.position recognises these IDs; no duplicate document IDs.
|
||||
draw.id = "cardPile"; discard.id = "discardPile";
|
||||
const owned = new Set(), restores = [];
|
||||
const owned = new Set(), topPlaying = new Set(), restores = [];
|
||||
const strict = env.strict !== false;
|
||||
let released = false, forcedOwner = false;
|
||||
const personal = () => {
|
||||
@@ -41,7 +42,17 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
card.destroyLog = false;
|
||||
card.destroyed = (current, position) => {
|
||||
if (released || !["discardPile", "equip", "judge"].includes(position)) return false;
|
||||
if (current.storage.mengsanCard_shuying.affixes.includes("exhaust") && current.storage.mengsanConsumed_shuying) {
|
||||
if (position === "discardPile" && topPlaying.has(current)) {
|
||||
if (!current.storage.mengsanExhausted_shuying) {
|
||||
current.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(current));
|
||||
refresh();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const exhaust = env.lib?.card?.[current.name]?.mengsanExhaust_shuying === true ||
|
||||
current.storage.mengsanCard_shuying.affixes.includes("exhaust");
|
||||
if (exhaust && current.storage.mengsanConsumed_shuying) {
|
||||
if (current.storage.mengsanExhausted_shuying) return true;
|
||||
current.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(current));
|
||||
@@ -51,7 +62,7 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
return false; // Actual discarded cards remain available to native skills.
|
||||
};
|
||||
for (const key of card.storage.mengsanCard_shuying.affixes) card.addGaintag(AFFIX_INFO[key]?.name || key);
|
||||
if (data.upgrade) card.addGaintag(`强化+${data.upgrade}`);
|
||||
// 强化仅用手牌 +X 角标显示;数值仍保存在 mengsanCard_shuying.upgrade。
|
||||
}
|
||||
function snapshot(card) {
|
||||
const data = card.storage?.mengsanCard_shuying || {};
|
||||
@@ -124,10 +135,95 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
observer.disconnect(); released = true;
|
||||
for (const restore of restores.reverse()) restore();
|
||||
for (const card of owned) card.remove();
|
||||
owned.clear(); draw.remove(); discard.remove();
|
||||
owned.clear(); topPlaying.clear(); draw.remove(); discard.remove(); exhausted.remove();
|
||||
});
|
||||
return {
|
||||
take,
|
||||
// 取出当前堆顶,不算抽牌、不洗牌,也不触发不足牌堆失败。
|
||||
beginTopPlay() {
|
||||
if (released || !session.active || !owner.isAlive() || !draw.firstChild) return null;
|
||||
const card = draw.firstChild;
|
||||
adopt(card); card.original = "c"; card.fix?.();
|
||||
exhausted.appendChild(card); topPlaying.add(card); refresh();
|
||||
return card;
|
||||
},
|
||||
async finishTopPlay(card) {
|
||||
if (!topPlaying.has(card)) return null;
|
||||
try {
|
||||
if (released || !session.active) return null;
|
||||
if (card.storage?.mengsanExhausted_shuying) { card.remove(); return card; }
|
||||
if (owner.getCards("hej").includes(card)) {
|
||||
const loss = owner.lose(card, exhausted); loss.type = "mengsanExhaust";
|
||||
await loss;
|
||||
if (released || !session.active || card.parentNode !== exhausted) return null;
|
||||
}
|
||||
// 被使用后在排序区/弃牌堆,或无法使用而仍在暂存区,均消耗原牌一次。
|
||||
if (!owned.has(card)) return null;
|
||||
card.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(card)); card.remove(); refresh();
|
||||
return card;
|
||||
} finally { topPlaying.delete(card); }
|
||||
},
|
||||
// Explicit exhaustion is not a discard and must not touch the public piles.
|
||||
async exhaustFromHand(card) {
|
||||
if (released || !session.active || !owner.isAlive() ||
|
||||
!owner.getCards("h").includes(card) || card.storage?.mengsanExhausted_shuying) return null;
|
||||
adopt(card);
|
||||
const loss = owner.lose(card, exhausted);
|
||||
loss.type = "mengsanExhaust";
|
||||
await loss;
|
||||
// A cancelled/redirection event must not falsely consume its original.
|
||||
if (released || !session.active || card.parentNode !== exhausted) return null;
|
||||
if (!card.storage.mengsanExhausted_shuying) {
|
||||
card.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(card));
|
||||
}
|
||||
card.remove();
|
||||
refresh();
|
||||
return card;
|
||||
},
|
||||
// Current physical combat cards only. Exhausted originals remain in owned;
|
||||
// never add the serialized exhaust snapshots a second time.
|
||||
battleCards(playing = []) {
|
||||
if (released || !session.active) return [];
|
||||
const cards = new Set([...owner.getCards("h"), ...draw.childNodes, ...discard.childNodes, ...exhausted.childNodes]);
|
||||
for (const card of owned) {
|
||||
if (card.storage?.mengsanOwnerId_shuying === owner.playerid &&
|
||||
card.storage?.mengsanExhausted_shuying) cards.add(card);
|
||||
}
|
||||
// The caller supplies this actor's native use-card originals, not virtual cards.
|
||||
for (const card of playing) if (ui.ordering && card?.parentNode === ui.ordering) cards.add(card);
|
||||
return [...cards];
|
||||
},
|
||||
addToDiscard(data) {
|
||||
if (released || !session.active) return null;
|
||||
if (!data || typeof data.id !== "string" || !data.id ||
|
||||
typeof data.name !== "string" || !/^[a-z][a-z0-9_]*$/.test(data.name) ||
|
||||
!["spade", "heart", "club", "diamond"].includes(data.suit) ||
|
||||
!Number.isInteger(data.number) || data.number < 1 || data.number > 13 ||
|
||||
!Array.isArray(data.affixes) || data.affixes.some(key => !Object.hasOwn(AFFIX_INFO, key))) {
|
||||
throw new TypeError("战斗生成牌数据无效");
|
||||
}
|
||||
const card = game.createCard(data.name, data.suit, data.number, data.nature);
|
||||
adopt(card, data);
|
||||
discard.appendChild(card);
|
||||
refresh();
|
||||
return card;
|
||||
},
|
||||
// Return references only to cards currently in this actor's discard node.
|
||||
discardCards() {
|
||||
if (released || !session.active) return [];
|
||||
return Array.from(discard.childNodes).filter(card => owned.has(card) &&
|
||||
!card.storage?.mengsanExhausted_shuying);
|
||||
},
|
||||
moveDiscardToTop(card) {
|
||||
if (released || !session.active || !owned.has(card) ||
|
||||
card.parentNode !== discard || card.storage?.mengsanExhausted_shuying) return false;
|
||||
card.fix?.();
|
||||
draw.insertBefore(card, draw.firstChild);
|
||||
refresh();
|
||||
return true;
|
||||
},
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// STS2 Cinder selects from the actor's hand after damage, never from the draw pile.
|
||||
export function createRandomHandExhauster(game, getActiveBattle, getRun, nextRandom) {
|
||||
return {
|
||||
capture() { const battle = getActiveBattle(); return battle?.session.active ? battle : null; },
|
||||
async exhaust(player, event, battle) {
|
||||
if (!battle || getActiveBattle() !== battle || !battle.session.active || !player?.isAlive()) return null;
|
||||
const piles = player === game.me ? battle.personalPiles : battle.monsterPiles?.get(player);
|
||||
if (typeof piles?.exhaustFromHand !== "function") throw new Error("余烬缺少个人消耗牌堆接口");
|
||||
const playing = new Set(event?.cards || []);
|
||||
const hand = player.getCards("h").filter(card => !playing.has(card) && !card.storage?.mengsanExhausted_shuying);
|
||||
if (!hand.length) return null;
|
||||
const run = getRun();
|
||||
if (!run) throw new Error("余烬缺少征程随机状态");
|
||||
const value = nextRandom(run);
|
||||
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("余烬随机值无效");
|
||||
return await piles.exhaustFromHand(hand[Math.floor(value * hand.length)]);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { _status } from "../../../../noname.js";
|
||||
import { canPayCard, isActiveCardUse } from "../battle/combat-rules.js";
|
||||
export const SLIMED_NAME = "mengsan_slimed_shuying";
|
||||
export function createSlimedData(id) {
|
||||
if (typeof id !== "string" || !id) throw new TypeError("黏液牌缺少ID");
|
||||
return { id, name: SLIMED_NAME, suit: "spade", number: 1, nature: null, affixes: ["exhaust"], upgrade: 0 };
|
||||
}
|
||||
export const slimedDefinition = Object.freeze({
|
||||
type: "status", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_slimed_shuying.png",
|
||||
mengsanCost_shuying: 1, mengsanExhaust_shuying: true,
|
||||
enable(used, player, event) {
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
notarget: true,
|
||||
async content(event, trigger, player) { await player.draw(1); },
|
||||
ai: { value: -1, useful: 0, order: 1, result: { player: 1 } },
|
||||
cardPrompt: () => "梦三:主动使用消耗1费用。抽1张牌。消耗:使用后退出此次战斗。",
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
// 战斗生成的状态牌不登记到永久可获得牌表,也不写入征程牌组。
|
||||
export const DAZED_NAME = "mengsan_dazed_shuying";
|
||||
export function createDazedData(id) {
|
||||
if (typeof id !== "string" || !id) throw new TypeError("晕眩牌缺少ID");
|
||||
return { id, name: DAZED_NAME, suit: "spade", number: 1,
|
||||
nature: null, affixes: ["unplayable", "void"], upgrade: 0 };
|
||||
}
|
||||
export const dazedDefinition = Object.freeze({
|
||||
type: "status", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_dazed_shuying.png",
|
||||
enable: false, notarget: true,
|
||||
ai: { value: -1, useful: -1, order: 0 },
|
||||
cardPrompt: () => "不能被打出。虚无:弃牌阶段开始时移除此牌,退出此次战斗。",
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// Count current actor-owned combat cards; permanent save data is deliberately absent.
|
||||
export function createStrikeCounter(game, getActiveBattle, lib) {
|
||||
return (player, event) => {
|
||||
const battle = getActiveBattle();
|
||||
if (!battle?.session.active || !player?.isAlive()) return 0;
|
||||
const piles = player === game.me ? battle.personalPiles : battle.monsterPiles?.get(player);
|
||||
if (typeof piles?.battleCards !== "function") throw new Error("完美打击缺少个人战斗牌堆接口");
|
||||
const playing = new Set(event?.cards || []);
|
||||
const seen = new Set();
|
||||
for (let parent = event?.parent; parent && !seen.has(parent); parent = parent.parent) {
|
||||
seen.add(parent);
|
||||
if (parent.name === "useCard" && parent.player === player) {
|
||||
for (const card of parent.cards || []) playing.add(card);
|
||||
}
|
||||
}
|
||||
// Nested native attacks may leave earlier originals in the playing zone.
|
||||
for (const used of player.getHistory?.("useCard") || []) {
|
||||
for (const card of used.cards || []) playing.add(card);
|
||||
}
|
||||
let count = 0;
|
||||
for (const card of piles.battleCards(playing)) {
|
||||
const name = lib.translate[card.name];
|
||||
if (typeof name === "string" && name.includes("打击")) count++;
|
||||
}
|
||||
return count;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Temporary combat-only strength. Never alter permanent strength or the run/deck.
|
||||
const records = new WeakMap();
|
||||
const SKILL = "mengsan_setup_strength_shuying";
|
||||
|
||||
export function forgetTemporaryStrength(player) {
|
||||
const existed = records.delete(player);
|
||||
delete player.storage.mengsanSetupStrength_shuying;
|
||||
return existed;
|
||||
}
|
||||
|
||||
export function clearTemporaryStrength(player) {
|
||||
if (!records.has(player)) return false;
|
||||
forgetTemporaryStrength(player);
|
||||
player.removeSkill(SKILL);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function temporaryStrengthAmount(player, battle) {
|
||||
const entry = records.get(player);
|
||||
return entry?.battle === battle && battle?.session.active ? entry.amount : 0;
|
||||
}
|
||||
|
||||
export function temporaryStrengthExpires(player, phase) {
|
||||
const entry = records.get(player);
|
||||
return Boolean(entry && phase.name === "phase" && (!entry.turn || entry.turn === phase));
|
||||
}
|
||||
|
||||
export function createTemporaryStrength(game, getActiveBattle) {
|
||||
return {
|
||||
amount(player) { return temporaryStrengthAmount(player, getActiveBattle()); },
|
||||
capture() {
|
||||
const battle = getActiveBattle();
|
||||
return battle?.session.active ? battle : null;
|
||||
},
|
||||
grant(player, amount, event, battle) {
|
||||
if (!battle?.session.active || getActiveBattle() !== battle || !player.isAlive()) return false;
|
||||
if (!Number.isSafeInteger(amount) || amount <= 0) throw new RangeError("临时力量必须是正安全整数");
|
||||
const ancestor = event.getParent?.("phase");
|
||||
const turn = ancestor?.name === "phase" ? ancestor : null;
|
||||
let entry = records.get(player);
|
||||
if (entry && (entry.battle !== battle || entry.turn !== turn)) {
|
||||
clearTemporaryStrength(player);
|
||||
entry = null;
|
||||
}
|
||||
if (amount > Number.MAX_SAFE_INTEGER - (entry?.amount || 0)) throw new RangeError("临时力量层数越界");
|
||||
if (!entry) {
|
||||
entry = { battle, turn, amount: 0 };
|
||||
// Register cleanup before publishing state. A stale release cannot clear a later battle.
|
||||
battle.session.ownResource(entry, () => {
|
||||
if (records.get(player) === entry) forgetTemporaryStrength(player);
|
||||
});
|
||||
records.set(player, entry);
|
||||
}
|
||||
entry.amount += amount;
|
||||
player.storage.mengsanSetupStrength_shuying = entry.amount;
|
||||
player.addSkill(SKILL);
|
||||
player.markSkill(SKILL);
|
||||
game.log(player, `在本回合内获得${amount}点力量`);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,21 @@
|
||||
const upgrades = Object.freeze({
|
||||
mengsan_songjianwushi: Object.freeze({ maxLevel: 1, baseBlock: 8, block: 11, draw: 1 }),
|
||||
mengsan_pomie: Object.freeze({ maxLevel: 1, baseCost: 1, cost: 0 }),
|
||||
mengsan_jianyi: Object.freeze({ maxLevel: 1, baseBlock: 7, block: 9, chooseExhaust: true }),
|
||||
mengsan_fangxue: Object.freeze({ maxLevel: 1, loseHp: 3, baseEnergy: 2, energy: 3 }),
|
||||
mengsan_yujin: Object.freeze({ maxLevel: 1, baseDamage: 18, damage: 24, exhaust: 1 }),
|
||||
mengsan_wanmeidaji: Object.freeze({ maxLevel: 1, baseDamage: 6, damage: 6, basePerStrike: 2, perStrike: 3 }),
|
||||
mengsan_yubeidaji: Object.freeze({ maxLevel: 1, baseDamage: 7, damage: 9, baseStrength: 2, strength: 3 }),
|
||||
mengsan_tupo: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 13, loseHp: 1 }),
|
||||
mengsan_touchui: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 12 }),
|
||||
mengsan_tiezhanbo: Object.freeze({ maxLevel: 1, baseBlock: 5, block: 7, baseDamage: 5, damage: 7 }),
|
||||
mengsan_shuangchongdaji: Object.freeze({ maxLevel: 1, baseDamage: 5, damage: 7, hits: 2 }),
|
||||
mengsan_shandianpili: Object.freeze({ maxLevel: 1, baseDamage: 4, damage: 7, vulnerable: 1 }),
|
||||
mengsan_rongrongzhiquan: Object.freeze({ maxLevel: 1, baseDamage: 10, damage: 14, exhaust: true }),
|
||||
mengsan_quanshenzhuangji: Object.freeze({ maxLevel: 1, baseCost: 1, cost: 0, damageFromBlock: true }),
|
||||
mengsan_jianbingdaji: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 10, baseDraw: 1, draw: 2 }),
|
||||
mengsan_feijianhuixuanbiao: Object.freeze({ maxLevel: 1, damage: 3, baseHits: 3, hits: 4 }),
|
||||
mengsan_fennu: Object.freeze({ maxLevel: 1, baseDamage: 6, damage: 8 }),
|
||||
sha: Object.freeze({ maxLevel: 1, damage: 9 }),
|
||||
mengsan_zhongsha: Object.freeze({ maxLevel: 1, damage: 10,
|
||||
vulnerable: 3 }),
|
||||
|
||||
Reference in New Issue
Block a user