fixed # 补全梦三怪物、卡牌

fixed # 修复天书乱斗中,过关技能不会重置次数的问题
This commit is contained in:
2026-10-03 04:42:19 +08:00
parent b005a7ff15
commit c448da39c2
131 changed files with 5021 additions and 246 deletions
+41
View File
@@ -0,0 +1,41 @@
# 梦三卡牌立绘规范
用户于 2026-10-02 指定以截图 `C:\Users\Diuse\Downloads\Ai\49.png` 中“防御”为同系列基准。
- 后续新增及转换的梦三卡牌使用约 5:7 的完整竖幅立绘;手牌常规尺寸 112×156,窄屏 82×114。禁止直接使用横向拆包图作为运行时卡面,也不要拉伸横图。
- 画风:泛黄宣纸、黑褐水墨、飞白与淡赭色、中国古代人物或兵器;根据卡牌效果独立安排竖幅构图。拆包图只作题材参考,不修改拆包原件。
- 立绘不绘制名称、费用、花色、强化角标或边框。左上花色点数、左侧中段费用、右下名称由游戏原生节点和梦三样式叠加;左侧信息栏和右下名称需留空间。
- 卡牌定义使用 `fullimage: true` 与明确的 `image` 路径,不使用横图 `fullskin`。新增卡名须加入 `ui/hand/style.css` 的本地完整卡面选择器;长牌名同时加入长名称窄屏规则。保持 `cover`,不使用 `100% 100%` 拉伸。
- 费用和强化角标继续使用现有 `data-*` 与伪元素,避免向卡牌子节点写入额外文字而触发十周年 UI 扫描。
- 不改变卡牌费用、效果、强化、奖励与存档数据;不修改全局标准/军争牌或十周年 UI。
- 接入后检查完整立绘、花色/费用/名称、扇形叠牌、悬停放大和窄屏。文件检查、隔离行为测试及预览不代表实机验收。
现有本地卡面检查:愤怒、飞剑回旋镖、剑柄打击、全身撞击、熔融之拳及晕眩统一竖幅;防御与重杀原有竖幅保留。
## 卡牌描述(2026-10-02)
后续卡牌只描述当前费用与当前效果,不写是否可升级/强化、次数或上限,也不写升级前后对比或升级后变化。普通牌显示普通数值,已强化的牌直接显示其当前数值。此规范同步适用于悬停介绍、静态翻译、奖励摘要、牌库详情及磨砺选择说明;保留强化机制、操作按钮和紧凑 +X 角标。
闪电霹雳已按本规范接入竖幅水墨立绘。
双重打击:对应拆包 twin_strike.png 的双道斩击主题,使用同规格竖幅水墨立绘。1费,两次独立伤害;描述只显示当前牌数值。
铁斩波:对应拆包 iron_wave.png 的钢刃与防护波主题;竖幅水墨立绘。1费,先获得格挡再攻击,沿用护甲、脆弱、力量与易伤规则;描述只显示当前牌数值。
头槌:对应拆包 headbutt.png 的额头撞击主题;竖幅水墨立绘。1费,先攻击再选择一张个人弃牌置于个人抽牌堆顶部。移动原实体牌,保留ID、花色、强化与词缀,不新增永久牌。
突破:对应拆包 breakthrough.png 的突破包围与群体横扫主题;5:7竖幅宣纸水墨立绘。1费,先通过原生 loseHp 失去1点生命,再对所有存活敌方造成9点伤害。失去生命不抵扣格挡;等待正常濒死/救援结算,若角色死亡或战斗结束则停止攻击。描述只显示当前牌费用与效果。
预备打击:对应 setup_strike.png 的挥拳与蓄势主题;5:7竖幅宣纸水墨立绘。1费,先造成7点伤害,再在当前完整回合内获得2点临时力量。按网页主表而非未正式发布的更新历史实现;描述只显示当前效果。临时力量可叠加,独立于永久力量,当前完整回合结束/死亡/战斗退出时清理。
完美打击:对应 perfected_strike.png 的精准挥刀主题;全幅5:7竖幅宣纸水墨立绘。2费,基础6点伤害,本场战斗每张当前名称含“打击”的牌增加2点伤害。统计手牌、个人抽牌堆、个人弃牌堆、消耗牌及本人打出区的真实牌,去重并包含此牌自身;生成/复制/变形即时生效,不读取永久牌组、不统计公共或敌方牌。描述只显示当前牌费用与效果。
余烬:对应 cinder.png 的灼热弯刀与燃尽主题;5:7完整竖幅宣纸水墨立绘。2费,先造成18点伤害,再从当时自己的剩余手牌中随机消耗一张。遵循网页0.101.0后的手牌规则,不消耗抽牌堆顶部。通过原生失牌事件移入私有消耗区,不进入弃牌堆/洗牌,不更改永久牌组;没有手牌时仍造成伤害。描述只显示当前费用与效果。
放血:对应 bloodletting.png 的气血换取力量主题;完整5:7竖幅宣纸水墨立绘。0费,先通过原生 loseHp 失去3点生命,再获得2点当前费用,不增加费用上限。等待濒死救援;死亡、战斗结束或切换后不再获得费用。属于技能牌,缠身不额外增加攻击费用;描述只显示当前费用与效果。
坚毅:对应 true_grit.png 的顶住巨岩、防御与坚忍主题;完整5:7竖幅宣纸水墨立绘。1费,先获得7点格挡,再从当时自己的剩余手牌中随机消耗一张;当前选择模式通过原生选牌事件选择自己的手牌。移动原实体牌到私有消耗区,保留ID、花色、当前状态和词缀,不改永久牌组;无手牌时仍获得格挡。等待格挡事件,防止战斗结束、换场、死亡及陈旧选牌继续失牌;沿用脆弱的格挡衰减。描述只显示当前费用与效果。
破灭:对应 HAVOC/havoc.png 的宽刃旋斩与黑披风主题;完整5:7竖幅宣纸水墨立绘。普通1费,当前另一状态0费;免费使用自己的抽牌堆顶部实体牌,再仅消耗该原牌。费用豁免绑定原生选择/使用父链和实体身份,不改卡牌费用或词缀,愤怒生成的副本正常进入自己的弃牌堆,不跟随原牌消耗(对应Wiki0.104.0修复)。原生可用性和目标规则保持有效,无法使用的堆顶牌仍消耗;目标由原生使用流程选择。空抽牌堆不洗牌、不触发抽牌不足失败。保留当前数值、ID、花色、词缀和永久牌组,防止换场/死亡期间陈旧使用。描述只显示当前费用和效果。
耸肩无视:对应 SHRUG_IT_OFF/shrug_it_off.png 的耸肩摊手、厚重肩甲弹开攻击主题;完整5:7竖幅宣纸水墨立绘。1费,先通过原生 changeHujia 获得8点格挡,再通过原生 draw 抽1张牌;当前牌直接显示其实际格挡值。沿用脆弱对格挡的衰减与个人牌堆抽牌/洗牌/不足失败流程;等待格挡完全结算,战斗结束、死亡、换场或个人牌堆被替换后不再抽牌,不触碰公共和其他角色牌堆,不修改永久牌组;无消耗关键词。描述只显示当前费用与效果。
Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

+13 -7
View File
@@ -1,6 +1,6 @@
// Candidate external coordinator. Finish callbacks return a boolean, NEVER a drain promise. // Candidate external coordinator. Finish callbacks return a boolean, NEVER a drain promise.
const copy = value => JSON.parse(JSON.stringify(value)); const copy = value => JSON.parse(JSON.stringify(value));
export function createBattleFlow({settlement, chooseReward, showMap, showEnding, quiesce, releaseSkills = async () => {}, presentVictory = async () => {}}) { export function createBattleFlow({settlement, chooseReward, chooseDefeat, showMap, showEnding, quiesce, releaseSkills = async () => {}, presentVictory = async () => {}}) {
let state = "map", active = null, work = null, job = null, error = null; let state = "map", active = null, work = null, job = null, error = null;
async function drive() { async function drive() {
try { try {
@@ -13,16 +13,20 @@ export function createBattleFlow({settlement, chooseReward, showMap, showEnding,
await presentVictory(copy(job.pending)); await presentVictory(copy(job.pending));
job.victoryPresented = true; job.victoryPresented = true;
} }
if (job.pending.state !== "chosen") { // Also reopen a legacy chosen defeat: it never had a recovery choice.
if (job.pending.state !== "chosen" ||
(job.pending.outcome === "defeat" && !job.pending.failureAction)) {
state = "choosing"; state = "choosing";
if (!job.hasChoice) { if (!job.hasChoice) {
job.choiceId = job.pending.outcome === "defeat" ? null job.choiceId = job.pending.outcome === "defeat" ? await chooseDefeat()
: await chooseReward(copy(job.pending.choices), { : await chooseReward(copy(job.pending.choices), {
boss: job.pending.boss, boss: job.pending.boss,
fixedRewards: copy(job.pending.fixedRewards || []), fixedRewards: copy(job.pending.fixedRewards || []),
rewardPackage: copy(job.pending.rewardPackage || null), rewardPackage: copy(job.pending.rewardPackage || null),
}); });
if (job.pending.outcome !== "defeat" && job.pending.choices.length && job.choiceId !== null && !job.pending.choices.some(c => c.id === job.choiceId)) throw new Error("Invalid reward selection"); if (job.pending.outcome !== "defeat" && !job.pending.rewardPackage &&
job.choiceId !== null &&
!job.pending.choices.some(c => c.id === job.choiceId && c.kind !== "gold")) throw new Error("Invalid reward selection");
job.hasChoice = true; job.hasChoice = true;
} }
job.pending = await settlement.choose(job.input.run.runId, job.pending.id, job.choiceId); job.pending = await settlement.choose(job.input.run.runId, job.pending.id, job.choiceId);
@@ -47,7 +51,9 @@ export function createBattleFlow({settlement, chooseReward, showMap, showEnding,
job.nextNode = await showMap(mapRun); job.nextNode = await showMap(mapRun);
job.committed.run = mapRun; job.committed.run = mapRun;
} }
else await showEnding(job.committed.route, copy(job.committed.run)); else if (!["retry", "new"].includes(job.committed.route)) {
await showEnding(job.committed.route, copy(job.committed.run));
}
job.delivered = true; job.delivered = true;
} }
state = job.committed.route === "map" ? "map" : "ended"; state = job.committed.route === "map" ? "map" : "ended";
@@ -73,7 +79,7 @@ export function createBattleFlow({settlement, chooseReward, showMap, showEnding,
state = "starting"; state = "starting";
try { try {
await quiesce(); await quiesce();
active = {session, run, node:copy(node), encounter:copy(encounter)};job = null;work = null; active = {session, run, retryRun:copy(run), node:copy(node), encounter:copy(encounter)};job = null;work = null;
// start MUST schedule only; it must not await the battle root's completion. // start MUST schedule only; it must not await the battle root's completion.
state = "battle"; start(); state = "battle"; start();
} catch (cause) { state = "blocked"; error = cause; throw cause; } } catch (cause) { state = "blocked"; error = cause; throw cause; }
@@ -82,7 +88,7 @@ export function createBattleFlow({settlement, chooseReward, showMap, showEnding,
if (state !== "battle" || session !== active.session) return false; if (state !== "battle" || session !== active.session) return false;
if (!["victory", "defeat"].includes(outcome)) throw new Error("Unknown outcome"); if (!["victory", "defeat"].includes(outcome)) throw new Error("Unknown outcome");
if (!Number.isInteger(defeatedEnemies) || defeatedEnemies < 0) throw new Error("Invalid defeated enemy count"); if (!Number.isInteger(defeatedEnemies) || defeatedEnemies < 0) throw new Error("Invalid defeated enemy count");
job = {input:{run:copy(active.run),node:copy(active.node),encounter:{...copy(active.encounter),defeatedEnemies},hp,outcome}}; job = {input:{run:copy(active.run),retryRun:copy(active.retryRun),node:copy(active.node),encounter:{...copy(active.encounter),defeatedEnemies},hp,outcome}};
state = "stopping"; state = "stopping";
active.session.requestStop(); active.session.requestStop();
launch(); launch();
+85 -21
View File
@@ -8,7 +8,7 @@ const copy = value => JSON.parse(JSON.stringify(value));
const idFor = (run, node) => JSON.stringify([run.runId, run.actIndex, node.id]); const idFor = (run, node) => JSON.stringify([run.runId, run.actIndex, node.id]);
export function createBattleSettlement({ export function createBattleSettlement({
store, config, getRandomRewardChoices, applyReward, completeNode, store, config, getRandomRewardChoices, applyReward, completeNode,
enterNextAct, now = Date.now, logRewardPackage = () => {}, enterNextAct, createRun, now = Date.now, logRewardPackage = () => {},
}) { }) {
const readRun = () => store.read()[config.saveKey]; const readRun = () => store.read()[config.saveKey];
function current(storage, runId) { function current(storage, runId) {
@@ -25,9 +25,9 @@ export function createBattleSettlement({
return Object.freeze({ return Object.freeze({
readRun, readRun,
// Persist fixed candidates before opening choice UI. No mutation of the live run. // Persist fixed candidates before opening choice UI. No mutation of the live run.
async prepare({run: snapshot, node, encounter, hp, outcome = "victory"}) { async prepare({run: snapshot, retryRun, node, encounter, hp, outcome = "victory"}) {
if (!["victory", "defeat"].includes(outcome)) throw new Error("Unknown battle outcome"); if (!["victory", "defeat"].includes(outcome)) throw new Error("Unknown battle outcome");
if (outcome === "victory" && (!Number.isFinite(hp) || (!encounter.skipRandomReward && (!Number.isSafeInteger(encounter.gold) || encounter.gold < 0)))) throw new Error("Invalid battle result"); if (outcome === "victory" && (!Number.isFinite(hp) || (!encounter.rewardPackage && (!Number.isSafeInteger(encounter.gold) || encounter.gold < 0)))) throw new Error("Invalid battle result");
const id = idFor(snapshot, node); const id = idFor(snapshot, node);
return store.update(storage => { return store.update(storage => {
const run = current(storage, snapshot.runId); const run = current(storage, snapshot.runId);
@@ -42,7 +42,7 @@ export function createBattleSettlement({
} }
// Battle may have changed RNG/card state since the last checkpoint. // Battle may have changed RNG/card state since the last checkpoint.
const base = copy(snapshot); delete base.battleFlow; const base = copy(snapshot); delete base.battleFlow;
const bondGrowth = settleBondBattle(base); const bondGrowth = settleBondBattle(base, node);
if (outcome === "victory") { if (outcome === "victory") {
base.player.hp = Math.max(1, Math.min(hp, base.player.maxHp ?? hp)); base.player.hp = Math.max(1, Math.min(hp, base.player.maxHp ?? hp));
base.statistics.defeatedEnemies += encounter.defeatedEnemies ?? 1; base.statistics.defeatedEnemies += encounter.defeatedEnemies ?? 1;
@@ -50,21 +50,68 @@ export function createBattleSettlement({
const relicRecovery = hp > 0 const relicRecovery = hp > 0
? applyBattleEndRelics(base, outcome) : 0; ? applyBattleEndRelics(base, outcome) : 0;
const rewards = outcome === "victory" && !encounter.skipRandomReward ? getRandomRewardChoices(base, encounter.rewardPool).filter(c => c.name) : []; const rewards = outcome === "victory" && !encounter.skipRandomReward ? getRandomRewardChoices(base, encounter.rewardPool).filter(c => c.name) : [];
if (outcome === "victory" && !encounter.skipRandomReward && !rewards.length) throw new Error("Empty battle reward pool"); // Persist guaranteed gold alongside loot; award it once in the chosen result.
// Persist the configured amount as a candidate, never as automatic victory income. const choices = outcome === "victory" && !encounter.rewardPackage
const choices = outcome === "victory" && !encounter.skipRandomReward
? [{id:"mengsan.reward.gold.shuying", kind:"gold", name:"金币", amount:encounter.gold}, ...rewards] : []; ? [{id:"mengsan.reward.gold.shuying", kind:"gold", name:"金币", amount:encounter.gold}, ...rewards] : [];
const rewardPackage = outcome === "victory" && encounter.rewardPackage const rewardPackage = outcome === "victory" && encounter.rewardPackage
? prepareRewardPackage(base, encounter.rewardPackage, ? prepareRewardPackage(base, encounter.rewardPackage,
getRandomRewardChoices) : null; getRandomRewardChoices) : null;
run.battleFlow.pending = {id, state:"awaitingChoice", nodeId:node.id, base, choices:copy(choices), fixedRewards:copy(encounter.fixedRewards || []), victoryDialogue:copy(encounter.victoryDialogue || []), boss:!!encounter.boss, outcome, relicRecovery, bondGrowth}; run.battleFlow.pending = {id, state:"awaitingChoice", nodeId:node.id, base, choices:copy(choices), fixedRewards:copy(encounter.fixedRewards || []), victoryDialogue:copy(encounter.victoryDialogue || []), boss:!!encounter.boss, outcome, relicRecovery, bondGrowth};
run.battleFlow.pending.rewardPackage = copy(rewardPackage); run.battleFlow.pending.rewardPackage = copy(rewardPackage);
if (outcome === "victory" && !rewardPackage) {
run.battleFlow.pending.rewardMode = "one-or-skip";
}
if (outcome === "defeat") {
const beforeBattle = retryRun || run;
if (beforeBattle.runId !== run.runId ||
beforeBattle.actIndex !== run.actIndex ||
beforeBattle.revision !== run.revision) throw new Error("Stale retry checkpoint");
run.battleFlow.pending.retryRun = copy(beforeBattle);
run.battleFlow.pending.retryRun.battleFlow = {
receipts:copy(run.battleFlow.receipts), pending:null,
};
run.battleFlow.pending.retryNode = copy(node);
run.battleFlow.pending.retryEncounter = copy(encounter);
}
return run.battleFlow.pending; return run.battleFlow.pending;
}); });
}, },
async choose(runId, id, choiceId) { async choose(runId, id, choiceId) {
return store.update(storage => { return store.update(storage => {
const {item} = pending(storage, runId, id); const {run, item} = pending(storage, runId, id);
if (item.outcome === "defeat") {
const action = typeof choiceId === "string" ? choiceId : choiceId?.action;
if (!["retry", "new"].includes(action)) throw new Error("Invalid defeat action");
if (item.failureAction) {
if (item.failureAction !== action ||
(action === "new" && item.choiceId.character !== choiceId.character)) {
throw new Error("Defeat choice already locked");
}
return item;
}
if (action === "new") {
if (!config.characters.includes(choiceId.character) || typeof createRun !== "function") {
throw new Error("Invalid new journey character");
}
// Generate once in the chosen checkpoint; a commit retry must not reroll it.
item.nextRun = createRun(choiceId.character);
if (!item.nextRun || item.nextRun.status !== "running" || item.nextRun.runId === runId) {
throw new Error("Invalid new journey");
}
item.result = copy(item.base);
item.result.status = "failed";
} else {
// Old pending saves have no retryRun: retain their last successful save.
item.result = copy(item.retryRun || run);
item.result.status = "running";
item.result.battleFlow = {receipts:copy(run.battleFlow.receipts), pending:null};
item.retryNode ||= copy(run.map?.nodes.find(node => node.id === item.nodeId));
if (!item.retryNode || item.retryNode.completed) throw new Error("Missing retry battle node");
}
item.state = "chosen"; item.choiceId = copy(choiceId);
item.failureAction = action; item.route = action; item.completedAt = now();
return item;
}
if (item.state === "chosen") { if (item.state === "chosen") {
const sameChoice = item.rewardPackage const sameChoice = item.rewardPackage
? item.choiceId?.cardId === choiceId?.cardId && ? item.choiceId?.cardId === choiceId?.cardId &&
@@ -80,16 +127,24 @@ export function createBattleSettlement({
applyRewardPackage(result, item.rewardPackage, choiceId, applyRewardPackage(result, item.rewardPackage, choiceId,
applyReward); applyReward);
} }
const choice = item.choices.find(c => c.id === choiceId); if (!item.rewardPackage) {
if (!item.rewardPackage && item.choices.length && choiceId !== null) { const reward = item.choices.find(c => c.id === choiceId && c.kind !== "gold");
if (!choice) throw new Error("Reward is not a fixed candidate"); if (choiceId !== null && !reward) {
if (choice.kind === "gold") { throw new Error("Choose one pending reward or skip");
if (!Number.isSafeInteger(choice.amount) || choice.amount < 0) throw new Error("Invalid pending gold reward"); }
result.player.gold += choice.amount; // This result is persisted before commit; retries never credit gold twice.
result.statistics.goldEarned += choice.amount; for (const gold of item.choices.filter(c => c.kind === "gold")) {
} else applyReward(result, choice.effectId || choice.id); if (!Number.isSafeInteger(gold.amount) || gold.amount < 0) throw new Error("Invalid pending gold reward");
} else if (!item.rewardPackage && choiceId !== null) throw new Error("Unexpected reward selection"); result.player.gold += gold.amount;
for (const rewardId of item.fixedRewards || []) applyReward(result, rewardId); result.statistics.goldEarned += gold.amount;
}
if (reward) {
applyReward(result, reward.effectId || reward.id);
}
for (const rewardId of item.fixedRewards || []) applyReward(result, rewardId);
} else {
for (const rewardId of item.fixedRewards || []) applyReward(result, rewardId);
}
if (!completeNode(result, item.nodeId)) throw new Error("Node completion failed"); if (!completeNode(result, item.nodeId)) throw new Error("Node completion failed");
route = item.boss && !enterNextAct(result) ? "victory" : "map"; route = item.boss && !enterNextAct(result) ? "victory" : "map";
} else result.status = "failed"; } else result.status = "failed";
@@ -111,6 +166,14 @@ export function createBattleSettlement({
const {run, item} = pending(storage, runId, id); const {run, item} = pending(storage, runId, id);
if (item.state !== "chosen") throw new Error("Durable chosen result required"); if (item.state !== "chosen") throw new Error("Durable chosen result required");
const result = copy(item.result); const result = copy(item.result);
if (item.route === "retry") {
// A failed attempt does not complete this node or add its victory receipt.
result.battleFlow = {receipts:copy(run.battleFlow.receipts), pending:null,
retry:{node:copy(item.retryNode), encounter:copy(item.retryEncounter || null)}};
storage[config.saveKey] = result;
return {route:"retry", run:result, retryNode:copy(item.retryNode),
retryEncounter:copy(item.retryEncounter || null), duplicate:false};
}
result.battleFlow = {receipts:[...run.battleFlow.receipts, id], pending:null}; result.battleFlow = {receipts:[...run.battleFlow.receipts, id], pending:null};
if (item.route === "map") storage[config.saveKey] = result; if (item.route === "map") storage[config.saveKey] = result;
else { else {
@@ -118,12 +181,13 @@ export function createBattleSettlement({
profile.completedRuns ||= []; profile.completedRuns ||= [];
if (!profile.completedRuns.some(r => r.runId === runId)) { if (!profile.completedRuns.some(r => r.runId === runId)) {
if (item.route === "victory") profile.wins = (profile.wins || 0) + 1; if (item.route === "victory") profile.wins = (profile.wins || 0) + 1;
profile.completedRuns.push({runId, settlementId:id, outcome:item.route, character:result.player.character, profile.completedRuns.push({runId, settlementId:id, outcome:item.route === "new" ? "defeat" : item.route, character:result.player.character,
completedNodes:result.statistics.completedNodes, goldEarned:result.statistics.goldEarned, completedAt:item.completedAt}); completedNodes:result.statistics.completedNodes, goldEarned:result.statistics.goldEarned, completedAt:item.completedAt});
} }
delete storage[config.saveKey]; // Same transaction as profile; never a second clear write. if (item.route === "new") storage[config.saveKey] = copy(item.nextRun);
else delete storage[config.saveKey]; // Same transaction as profile; never a second clear write.
} }
return {route:item.route, run:result, duplicate:false}; return {route:item.route, run:item.route === "new" ? copy(item.nextRun) : result, duplicate:false};
}); });
}, },
}); });
+39 -7
View File
@@ -1,15 +1,22 @@
// 梦三独有的出牌经济,不修改 noname 的全局牌定义。 // 梦三独有的出牌经济,不修改 noname 的全局牌定义。
import { isAttackCard } from "../monsters/vine-tangled.js";
import { _status } from "../../../../noname.js"; import { _status } from "../../../../noname.js";
import { cardUpgradeLevel, cardUpgradeRule } from "../cards/upgrades.js";
export const PLAYER_ENERGY = 3; export const PLAYER_ENERGY = 3;
export const PLAYER_HAND_LIMIT = 3; export const PLAYER_HAND_LIMIT = 3;
export const SHA_DAMAGE = 6; export const SHA_DAMAGE = 6;
export const TRICK_DAMAGE = 4; export const TRICK_DAMAGE = 4;
const COSTS = Object.freeze({ sha: 1, mengsan_zhongsha: 2, const COSTS = Object.freeze({ sha: 1, mengsan_zhongsha: 2, mengsan_fangyu: 1, mengsan_fennu: 0,
mengsan_feijianhuixuanbiao: 1, mengsan_jianbingdaji: 1, mengsan_quanshenzhuangji: 1, mengsan_rongrongzhiquan: 1, mengsan_shandianpili: 1, mengsan_shuangchongdaji: 1, mengsan_tiezhanbo: 1, mengsan_touchui: 1, mengsan_tupo: 1, mengsan_yubeidaji: 1, mengsan_wanmeidaji: 2, mengsan_yujin: 2, mengsan_fangxue: 0, mengsan_jianyi: 1, mengsan_pomie: 1, mengsan_songjianwushi: 1,
tao: 1, jiu: 1, shan: 0, wuxie: 0, tao: 1, jiu: 1, shan: 0, wuxie: 0,
juedou: 2, nanman: 2, wanjian: 2, taoyuan: 2 }); juedou: 2, nanman: 2, wanjian: 2, taoyuan: 2 });
export const cardCost = card => COSTS[card?.name] ?? 1; export const cardCost = (card, player = null) => {
const rule = cardUpgradeLevel(card) ? cardUpgradeRule(card.name) : null;
const base = rule?.cost ?? COSTS[card?.name] ?? 1;
return base + (player?.storage?.mengsanTangled_shuying > 0 && isAttackCard(card) ? 1 : 0);
};
export const isActiveCardUse = (event, player) => { export const isActiveCardUse = (event, player) => {
if (!event || !player) return false; if (!event || !player) return false;
@@ -17,7 +24,7 @@ export const isActiveCardUse = (event, player) => {
let inPhaseUse = false; let inPhaseUse = false;
for (let current = event; current && !seen.has(current); current = current.parent) { for (let current = event; current && !seen.has(current); current = current.parent) {
seen.add(current); seen.add(current);
if (current.name === "dying" || current.name === "chooseToRespond") return false; if (current.name === "dying" || current.name === "respond" || current.name === "chooseToRespond") return false;
if (current.name === "chooseToUse" && (current.respondTo || String(current.type || "").startsWith("respond"))) return false; if (current.name === "chooseToUse" && (current.respondTo || String(current.type || "").startsWith("respond"))) return false;
if (current.name === "phaseUse" && current.player === player) inPhaseUse = true; if (current.name === "phaseUse" && current.player === player) inPhaseUse = true;
} }
@@ -26,10 +33,35 @@ export const isActiveCardUse = (event, player) => {
_status.currentPhase === player); _status.currentPhase === player);
}; };
export const canPayCard = (player, card) => (player?.storage?.mengsanEnergy_shuying ?? Infinity) >= cardCost(card); // 仅授权破灭的这一条原生选择/使用父链及同一张实体牌;副本和其他使用不免费。
const freeUses = new WeakMap();
export function authorizeFreeCardUse(player, card, choice, valid) {
const entries = freeUses.get(player) || new Set();
freeUses.set(player, entries);
const entry = { card, choice, valid }; entries.add(entry);
return () => { entries.delete(entry); if (!entries.size) freeUses.delete(player); };
}
function isFreeCardUse(player, card, event) {
for (const entry of freeUses.get(player) || []) {
if (!entry.valid() || card?.name !== entry.card.name ||
!(card === entry.card || card?.cards?.includes(entry.card))) continue;
const seen = new Set();
for (let current = event; current && !seen.has(current); current = current.parent) {
seen.add(current);
if (current.name === "useCard" && (current.player !== player ||
current.cards?.length !== 1 || current.cards[0] !== entry.card)) break;
if (current === entry.choice) return true;
}
}
return false;
}
export const payCard = (player, card) => { export const canPayCard = (player, card, event = _status.event) =>
if (!canPayCard(player, card)) return false; isFreeCardUse(player, card, event) || (player?.storage?.mengsanEnergy_shuying ?? Infinity) >= cardCost(card, player);
player.storage.mengsanEnergy_shuying -= cardCost(card);
export const payCard = (player, card, event = _status.event) => {
if (isFreeCardUse(player, card, event)) return true;
if (!canPayCard(player, card, event)) return false;
player.storage.mengsanEnergy_shuying -= cardCost(card, player);
return true; return true;
}; };
+5 -2
View File
@@ -1,3 +1,6 @@
import { slowAttackDamage } from "../monsters/effigy-slow.js";
import { shrinkAttackDamage } from "../monsters/shrinker-status.js";
export function attackHitCount(intent) { export function attackHitCount(intent) {
const hits = intent.hits ?? 1; const hits = intent.hits ?? 1;
if (!Number.isInteger(hits) || hits < 1) { if (!Number.isInteger(hits) || hits < 1) {
@@ -16,7 +19,7 @@ export function outgoingAttackDamage(intent, source) {
if (source?.storage?.mengsanWeak_shuying > 0) { if (source?.storage?.mengsanWeak_shuying > 0) {
damage = Math.floor(damage * 0.75); damage = Math.floor(damage * 0.75);
} }
return damage; return shrinkAttackDamage(damage, source);
} }
export function previewAttackDamage(intent, source, target) { export function previewAttackDamage(intent, source, target) {
@@ -24,5 +27,5 @@ export function previewAttackDamage(intent, source, target) {
if (target?.storage?.mengsanVulnerable_shuying > 0) { if (target?.storage?.mengsanVulnerable_shuying > 0) {
damage = Math.ceil(damage * 1.5); damage = Math.ceil(damage * 1.5);
} }
return damage; return slowAttackDamage(damage, target);
} }
+4 -8
View File
@@ -1,3 +1,5 @@
import { applyMengsanDebuff } from "../monsters/artifact-status.js";
import { applyTangled } from "../monsters/vine-tangled.js";
import { attackHitCount, outgoingAttackDamage } from "./intent-damage.js"; import { attackHitCount, outgoingAttackDamage } from "./intent-damage.js";
import { beginDeathBlow, isDeathBlowIntent } from "./death-blow.js"; import { beginDeathBlow, isDeathBlowIntent } from "./death-blow.js";
@@ -11,14 +13,8 @@ export function createIntentExecutor(game, getActiveBattle) {
Boolean(getActiveBattle()?.session.active); Boolean(getActiveBattle()?.session.active);
const applyDebuff = (target, move) => { const applyDebuff = (target, move) => {
if (!move.debuff) return; if (!move.debuff) return;
const frail = move.debuff === "脆弱"; if (move.debuff === "缠结") { applyTangled(getActiveBattle(), target, move.stacks); return; }
const key = frail ? "mengsanFrail_shuying" : applyMengsanDebuff(target, move.debuff === "脆弱" ? "frail" : "vulnerable", move.stacks);
"mengsanVulnerable_shuying";
target.storage[key] = (target.storage[key] || 0) + move.stacks;
target.addSkill(frail ? "mengsan_frail_shuying" :
"mengsan_vulnerable_shuying");
target.markSkill(frail ? "mengsan_frail_shuying" :
"mengsan_vulnerable_shuying");
}; };
return async (source, move) => { return async (source, move) => {
for (const target of getIntentHostiles(game, source)) { for (const target of getIntentHostiles(game, source)) {
+14 -2
View File
@@ -43,7 +43,15 @@ export function markBondDown(run, id) {
if (npc) npc.down = true; if (npc) npc.down = true;
} }
export function prepareBondBattle(run, random) { // 第一章第一层的角色专属开场:不判定助战,也不累计本场羁绊成长。
const isCharacterOpeningBattle = (run, node) =>
run.actIndex === 0 && node?.floor === 0;
export function prepareBondBattle(run, random, node = null) {
if (isCharacterOpeningBattle(run, node)) {
run.bondBattle = null;
return null;
}
const bonds = ensureBonds(run); const bonds = ensureBonds(run);
const id = bonds.selected; const id = bonds.selected;
const npc = bonds.npcs[id]; const npc = bonds.npcs[id];
@@ -54,7 +62,11 @@ export function prepareBondBattle(run, random) {
return combat; return combat;
} }
export function settleBondBattle(run) { export function settleBondBattle(run, node = null) {
if (isCharacterOpeningBattle(run, node)) {
run.bondBattle = null;
return null;
}
const battle = run.bondBattle; const battle = run.bondBattle;
if (!battle) return null; if (!battle) return null;
const npc = ensureBonds(run).npcs[battle.id]; const npc = ensureBonds(run).npcs[battle.id];
+29
View File
@@ -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);
};
}
+29
View File
@@ -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;
};
}
+31
View File
@@ -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;
},
};
}
+19
View File
@@ -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);
};
}
+18
View File
@@ -1,8 +1,26 @@
// 梦三永久牌组的可获得牌注册表。这里登记牌名与归属,不复制引擎中的效果定义。 // 梦三永久牌组的可获得牌注册表。这里登记牌名与归属,不复制引擎中的效果定义。
// 新增武将专属牌时填写 owner 为该武将 ID;通用牌填写 owner: null。 // 新增武将专属牌时填写 owner 为该武将 ID;通用牌填写 owner: null。
const definitions = [ 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: "sha", category: "damage", owner: null },
{ name: "mengsan_zhongsha", 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: "shan", category: "utility", owner: null },
{ name: "tao", category: "recovery", owner: null }, { name: "tao", category: "recovery", owner: null },
{ name: "jiu", category: "recovery", owner: null }, { name: "jiu", category: "recovery", owner: null },
+46
View File
@@ -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);
},
};
}
+36
View File
@@ -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;
};
}
+56
View File
@@ -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
View File
@@ -2,17 +2,23 @@
// 保留原牌名,以兼容原有牌的响应、装备技能及转化牌引用。 // 保留原牌名,以兼容原有牌的响应、装备技能及转化牌引用。
import standard from "../../../../card/standard.js"; import standard from "../../../../card/standard.js";
import extra from "../../../../card/extra.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, import { cardCost, canPayCard, isActiveCardUse, SHA_DAMAGE,
TRICK_DAMAGE } from "../battle/combat-rules.js"; TRICK_DAMAGE } from "../battle/combat-rules.js";
import { cardUpgradeLevel, cardUpgradeRule } from "./upgrades.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 packs = [standard, extra];
const names = [...new Set(packs.flatMap(pack => pack.list.map(entry => entry[2])))]; const names = [...new Set(packs.flatMap(pack => pack.list.map(entry => entry[2])))];
const sourceCard = name => standard.card[name] || extra.card[name]; const sourceCard = name => standard.card[name] || extra.card[name];
const sourceText = name => extra.translate[`${name}_info`] || standard.translate[`${name}_info`]; 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 card = {};
const translate = {}; const translate = {};
for (const name of names) { for (const name of names) {
@@ -52,14 +58,12 @@ export function createMengsanCards() {
const damage = cardUpgradeLevel(used) const damage = cardUpgradeLevel(used)
? cardUpgradeRule("sha").damage : SHA_DAMAGE; ? cardUpgradeRule("sha").damage : SHA_DAMAGE;
return `梦三:主动使用消耗1费用;目标须使用【闪】,` + return `梦三:主动使用消耗1费用;目标须使用【闪】,` +
`否则受到${damage}点伤害。每回合使用次数不限。` + `否则受到${damage}点伤害。每回合使用次数不限。`;
(cardUpgradeLevel(used) ? "已强化(上限1次)。" :
"可强化1次:伤害变为9点。");
}; };
translate.sha_info = `梦三:主动使用消耗1费用。出牌阶段,对攻击范围内的一名角色使用;其须使用【闪】,否则受到${SHA_DAMAGE}点伤害。每回合使用次数不限。`; translate.sha_info = `梦三:主动使用消耗1费用。出牌阶段,对攻击范围内的一名角色使用;其须使用【闪】,否则受到${SHA_DAMAGE}点伤害。每回合使用次数不限。`;
card.mengsan_zhongsha = { card.mengsan_zhongsha = {
type: "basic", type: "basic",
fullskin: true, fullimage: true,
image: "ext:术樱包/mengsan/assets/cards/mengsan_zhongsha.png", image: "ext:术樱包/mengsan/assets/cards/mengsan_zhongsha.png",
mengsanCost_shuying: cardCost({ name: "mengsan_zhongsha" }), mengsanCost_shuying: cardCost({ name: "mengsan_zhongsha" }),
enable(used, player, event) { enable(used, player, event) {
@@ -79,12 +83,8 @@ export function createMengsanCards() {
damage.mengsanAttack_shuying = true; damage.mengsanAttack_shuying = true;
await damage; await damage;
if (!target.isAlive()) return; if (!target.isAlive()) return;
target.storage.mengsanVulnerable_shuying = if (applyMengsanDebuff(target, "vulnerable", effect.vulnerable))
(target.storage.mengsanVulnerable_shuying || 0) + game.log(target, `获得${effect.vulnerable}层易伤`);
effect.vulnerable;
target.addSkill("mengsan_vulnerable_shuying");
target.markSkill("mengsan_vulnerable_shuying");
game.log(target, `获得${effect.vulnerable}层易伤`);
}, },
ai: { order: 4, result: { target: -1.5 }, ai: { order: 4, result: { target: -1.5 },
tag: { damage: 1 } }, tag: { damage: 1 } },
@@ -96,9 +96,7 @@ export function createMengsanCards() {
`一名其他角色造成${effect.damage}点伤害,然后给予其` + `一名其他角色造成${effect.damage}点伤害,然后给予其` +
`${effect.vulnerable}层易伤。受到攻击伤害增加50%` + `${effect.vulnerable}层易伤。受到攻击伤害增加50%` +
"(向上取整),每个自身回合结束减少1层。" + "(向上取整),每个自身回合结束减少1层。" +
"每回合使用次数不限。" + (cardUpgradeLevel(used) "每回合使用次数不限。";
? "已强化(上限1次)。" :
"可强化1次:10点伤害、3层易伤。");
}, },
}; };
translate.mengsan_zhongsha = "重杀"; translate.mengsan_zhongsha = "重杀";
@@ -107,6 +105,648 @@ export function createMengsanCards() {
"一名其他角色造成8点伤害,然后给予其2层易伤。" + "一名其他角色造成8点伤害,然后给予其2层易伤。" +
"易伤使受到的攻击伤害增加50%(向上取整)," + "易伤使受到的攻击伤害增加50%(向上取整)," +
"每个自身回合结束减少1层。每回合使用次数不限。"; "每个自身回合结束减少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); }; card.tao.content = async function (event) { await event.target.recover(8); };
translate.tao_info = "梦三:主动使用消耗1费用,回复8点生命;濒死救援不消耗费用,同样回复8点生命。"; translate.tao_info = "梦三:主动使用消耗1费用,回复8点生命;濒死救援不消耗费用,同样回复8点生命。";
@@ -139,5 +779,24 @@ export function createMengsanCards() {
}; };
translate.shandian_info = "梦三:主动使用消耗1费用。若判定为黑桃2~9,目标受到12点雷电伤害;否则移至下家的判定区。"; translate.shandian_info = "梦三:主动使用消耗1费用。若判定为黑桃2~9,目标受到12点雷电伤害;否则移至下家的判定区。";
card.shandian.cardPrompt = () => translate.shandian_info; 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"] };
} }
+100 -4
View File
@@ -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); 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 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. // Detached nodes: engine get.position recognises these IDs; no duplicate document IDs.
draw.id = "cardPile"; discard.id = "discardPile"; draw.id = "cardPile"; discard.id = "discardPile";
const owned = new Set(), restores = []; const owned = new Set(), topPlaying = new Set(), restores = [];
const strict = env.strict !== false; const strict = env.strict !== false;
let released = false, forcedOwner = false; let released = false, forcedOwner = false;
const personal = () => { const personal = () => {
@@ -41,7 +42,17 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
card.destroyLog = false; card.destroyLog = false;
card.destroyed = (current, position) => { card.destroyed = (current, position) => {
if (released || !["discardPile", "equip", "judge"].includes(position)) return false; 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; if (current.storage.mengsanExhausted_shuying) return true;
current.storage.mengsanExhausted_shuying = true; current.storage.mengsanExhausted_shuying = true;
battle.exhaustPile.push(snapshot(current)); 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. 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); 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) { function snapshot(card) {
const data = card.storage?.mengsanCard_shuying || {}; const data = card.storage?.mengsanCard_shuying || {};
@@ -124,10 +135,95 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
observer.disconnect(); released = true; observer.disconnect(); released = true;
for (const restore of restores.reverse()) restore(); for (const restore of restores.reverse()) restore();
for (const card of owned) card.remove(); for (const card of owned) card.remove();
owned.clear(); draw.remove(); discard.remove(); owned.clear(); topPlaying.clear(); draw.remove(); discard.remove(); exhausted.remove();
}); });
return { return {
take, 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) { addAffix(card, key) {
if (released || !Object.hasOwn(AFFIX_INFO, key) || !owned.has(card) || !owner.getCards("h").includes(card)) return false; if (released || !Object.hasOwn(AFFIX_INFO, key) || !owned.has(card) || !owner.getCards("h").includes(card)) return false;
const affixes = card.storage?.mengsanCard_shuying?.affixes; const affixes = card.storage?.mengsanCard_shuying?.affixes;
+19
View File
@@ -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)]);
},
};
}
+19
View File
@@ -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张牌。消耗:使用后退出此次战斗。",
});
+14
View File
@@ -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: () => "不能被打出。虚无:弃牌阶段开始时移除此牌,退出此次战斗。",
});
+27
View File
@@ -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;
};
}
+62
View File
@@ -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;
},
};
}
+17
View File
@@ -1,4 +1,21 @@
const upgrades = Object.freeze({ 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 }), sha: Object.freeze({ maxLevel: 1, damage: 9 }),
mengsan_zhongsha: Object.freeze({ maxLevel: 1, damage: 10, mengsan_zhongsha: Object.freeze({ maxLevel: 1, damage: 10,
vulnerable: 3 }), vulnerable: 3 }),
+12
View File
@@ -0,0 +1,12 @@
import { INKLET_CHARACTER } from "../../../monsters/inklet-intent.js";
// 网页未指定固定群体数量;梦三适配为三只,前/中/后位置固定,生命在实际入场抽取。
export function createInkletBattlePlan() {
return {
units: Array.from({ length: 3 }, (_, index) => ({
id: `inklet_${index + 1}`, character: INKLET_CHARACTER, camp: "enemy",
tier: "normal", inkletPosition: index + 1, hand: 4, inheritSkills: false,
})),
rules: [],
};
}
+9 -3
View File
@@ -9,7 +9,7 @@ const act1Map = {
1, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 1,
], ],
nodeWeights: { battle: 65, event: 15, rest: 10, shop: 10 }, nodeWeights: { battle: 50, elite: 15, event: 15, rest: 10, shop: 10 },
requiredFloors: { 2: "battle", 10: "chest", 16: "rest" }, requiredFloors: { 2: "battle", 10: "chest", 16: "rest" },
chestRewardPool: "act1.pool.chest", chestRewardPool: "act1.pool.chest",
fixedNodes: [ fixedNodes: [
@@ -20,8 +20,14 @@ const act1Map = {
contentId: "act1.battle.liubeiOpening", contentId: "act1.battle.liubeiOpening",
}, },
], ],
enemies: ["mengsan_flyconid_shuying", "mengsan_raider_trio_shuying"], enemies: [
eliteEnemies: ["re_xiahoudun", "re_zhangliao", "re_xuzhu"], "mengsan_flyconid_shuying", "mengsan_raider_trio_shuying",
"mengsan_soldier_shuying", "mengsan_fogmog_shuying",
"mengsan_mawler_shuying", "mengsan_vine_shambler_shuying",
"mengsan_nibbit_shuying", "mengsan_nibbit_pair_shuying", "mengsan_cubex_construct_shuying", "mengsan_shrinker_beetle_shuying", "mengsan_twig_slime_m_shuying", "mengsan_twig_slime_s_shuying", "mengsan_leaf_slime_m_shuying", "mengsan_leaf_slime_s_shuying", "mengsan_slithering_strangler_shuying", "mengsan_snapping_jaxfruit_shuying", "mengsan_fuzzy_wurm_crawler_shuying",
"mengsan_inklet_shuying",
],
eliteEnemies: ["re_xiahoudun", "re_zhangliao", "re_xuzhu", "mengsan_byrdonis_shuying", "mengsan_bygone_effigy_shuying", "mengsan_phrog_parasite_shuying"],
boss: "re_lvbu", boss: "re_lvbu",
baseGold: 20, baseGold: 20,
}; };
+8
View File
@@ -0,0 +1,8 @@
import { NIBBIT_CHARACTER } from "../../../monsters/nibbit-intent.js";
export const NIBBIT_PAIR_ENCOUNTER = "mengsan_nibbit_pair_shuying";
export function createNibbitPairBattlePlan() {
return { units: [
{ id: "nibbit_front", character: NIBBIT_CHARACTER, camp: "enemy", tier: "normal", hand: 4, inheritSkills: false },
{ id: "nibbit_back", character: NIBBIT_CHARACTER, camp: "enemy", tier: "normal", hand: 4, inheritSkills: false },
], rules: [] };
}
+58
View File
@@ -9,6 +9,44 @@ export function createScenarioCharacters(Character) {
skills: [], img: "image/character/shibing1.jpg" }), skills: [], img: "image/character/shibing1.jpg" }),
mengsan_flyconid_shuying: new Character({ sex: "none", group: "qun", hp: 48, maxHp: 48, mengsan_flyconid_shuying: new Character({ sex: "none", group: "qun", hp: 48, maxHp: 48,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/flyconid-portrait.png" }), skills: [], img: "extension/术樱包/mengsan/assets/portraits/flyconid-portrait.png" }),
mengsan_fuzzy_wurm_crawler_shuying: new Character({ sex: "none", group: "qun", hp: 56, maxHp: 56,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/fuzzy-wurm-crawler-forest-portrait.png" }),
mengsan_inklet_shuying: new Character({ sex: "none", group: "qun", hp: 14, maxHp: 14,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/inklet-forest-portrait.png" }),
mengsan_snapping_jaxfruit_shuying: new Character({ sex: "none", group: "qun", hp: 32, maxHp: 32,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/snapping-jaxfruit-forest-portrait.png" }),
mengsan_slithering_strangler_shuying: new Character({ sex: "none", group: "qun", hp: 54, maxHp: 54,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/slithering-strangler-forest-portrait.png" }),
mengsan_leaf_slime_s_shuying: new Character({ sex: "none", group: "qun", hp: 13, maxHp: 13,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/leaf-slime-s-forest-portrait.png" }),
mengsan_leaf_slime_m_shuying: new Character({ sex: "none", group: "qun", hp: 34, maxHp: 34,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/leaf-slime-m-forest-portrait.png" }),
mengsan_twig_slime_s_shuying: new Character({ sex: "none", group: "qun", hp: 9, maxHp: 9,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/twig-slime-s-forest-portrait.png" }),
mengsan_twig_slime_m_shuying: new Character({ sex: "none", group: "qun", hp: 27, maxHp: 27,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/twig-slime-m-forest-portrait.png" }),
mengsan_shrinker_beetle_shuying: new Character({ sex: "none", group: "qun", hp: 39, maxHp: 39,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/shrinker-beetle-forest-portrait.png" }),
mengsan_phrog_parasite_shuying: new Character({ sex: "none", group: "qun", hp: 62, maxHp: 62,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/phrog-parasite-forest-portrait.png" }),
mengsan_wriggler_shuying: new Character({ sex: "none", group: "qun", hp: 19, maxHp: 19,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/wriggler-forest-portrait.png" }),
mengsan_bygone_effigy_shuying: new Character({ sex: "none", group: "qun", hp: 127, maxHp: 127,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/bygone-effigy-forest-portrait.png" }),
mengsan_byrdonis_shuying: new Character({ sex: "none", group: "qun", hp: 82, maxHp: 82,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/byrdonis-forest-portrait.png" }),
mengsan_cubex_construct_shuying: new Character({ sex: "none", group: "qun", hp: 65, maxHp: 65,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/cubex-construct-forest-portrait.png" }),
mengsan_nibbit_shuying: new Character({ sex: "none", group: "qun", hp: 44, maxHp: 44,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/nibbit-forest-portrait.png" }),
mengsan_vine_shambler_shuying: new Character({ sex: "none", group: "qun", hp: 61, maxHp: 61,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/vine-shambler-forest-portrait.png" }),
mengsan_mawler_shuying: new Character({ sex: "none", group: "qun", hp: 72, maxHp: 72,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/mawler-forest-portrait.png" }),
mengsan_fogmog_shuying: new Character({ sex: "none", group: "qun", hp: 74, maxHp: 74,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/fogmog-forest-portrait.png" }),
mengsan_eye_with_teeth_shuying: new Character({ sex: "none", group: "qun", hp: 6, maxHp: 6,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/eye-with-teeth-forest-portrait.png" }),
mengsan_raider_brute_shuying: new Character({ sex: "none", group: "qun", hp: 32, maxHp: 32, mengsan_raider_brute_shuying: new Character({ sex: "none", group: "qun", hp: 32, maxHp: 32,
skills: [], img: "extension/术樱包/mengsan/assets/portraits/raider-brute-portrait.png" }), skills: [], img: "extension/术樱包/mengsan/assets/portraits/raider-brute-portrait.png" }),
mengsan_raider_assassin_shuying: new Character({ sex: "none", group: "qun", hp: 21, maxHp: 21, mengsan_raider_assassin_shuying: new Character({ sex: "none", group: "qun", hp: 21, maxHp: 21,
@@ -30,6 +68,26 @@ export const scenarioTranslations = {
mengsan_taoyuan_bond_shuying_info: "先天:本征程初始结识关羽、张飞,二人羁绊均为8级。可在行军菜单中指定一名助战角色;战斗结束,到场增加50%升级进度,未到场增加25%。", mengsan_taoyuan_bond_shuying_info: "先天:本征程初始结识关羽、张飞,二人羁绊均为8级。可在行军菜单中指定一名助战角色;战斗结束,到场增加50%升级进度,未到场增加25%。",
mengsan_soldier_shuying: "士兵", mengsan_soldier_shuying: "士兵",
mengsan_flyconid_shuying: "飞蝇菌子", mengsan_flyconid_shuying: "飞蝇菌子",
mengsan_fogmog_shuying: "雾菇",
mengsan_mawler_shuying: "蛮兽",
mengsan_vine_shambler_shuying: "藤蔓蹒跚者",
mengsan_nibbit_shuying: "小啃兽",
mengsan_cubex_construct_shuying: "立柱构造体",
mengsan_byrdonis_shuying: "多尼斯异鸟",
mengsan_bygone_effigy_shuying: "旧日雕像",
mengsan_phrog_parasite_shuying: "异蛙寄生虫",
mengsan_wriggler_shuying: "扭动虫",
mengsan_nibbit_pair_shuying: "小啃兽双只",
mengsan_shrinker_beetle_shuying: "缩小甲虫",
mengsan_twig_slime_m_shuying: "树枝史莱姆(中)",
mengsan_twig_slime_s_shuying: "树枝史莱姆(小)",
mengsan_leaf_slime_m_shuying: "树叶史莱姆(中)",
mengsan_leaf_slime_s_shuying: "树叶史莱姆(小)",
mengsan_slithering_strangler_shuying: "蛇行扼杀者",
mengsan_snapping_jaxfruit_shuying: "闪光贾克斯果",
mengsan_inklet_shuying: "墨宝",
mengsan_fuzzy_wurm_crawler_shuying: "毛绒伏地虫",
mengsan_eye_with_teeth_shuying: "利齿之眼",
mengsan_raider_brute_shuying: "劫掠者暴徒", mengsan_raider_brute_shuying: "劫掠者暴徒",
mengsan_raider_assassin_shuying: "劫掠者刺客", mengsan_raider_assassin_shuying: "劫掠者刺客",
mengsan_raider_axe_shuying: "劫掠者斧手", mengsan_raider_axe_shuying: "劫掠者斧手",
+88
View File
@@ -11,6 +11,78 @@ export const rewards = {
card: { name: "mengsan_zhongsha" }, name: "获得一张【重杀】", card: { name: "mengsan_zhongsha" }, name: "获得一张【重杀】",
description: "2费:造成8点伤害,然后给予目标2层易伤。", description: "2费:造成8点伤害,然后给予目标2层易伤。",
}, },
"shared.reward.card.songjianwushi": {
card: { name: "mengsan_songjianwushi" }, name: "获得一张【耸肩无视】",
description: "1费:获得8点格挡,然后抽1张牌。",
},
"shared.reward.card.pomie": {
card: { name: "mengsan_pomie" }, name: "获得一张【破灭】",
description: "1费:免费使用抽牌堆顶部的牌,然后将该原牌消耗。",
},
"shared.reward.card.jianyi": {
card: { name: "mengsan_jianyi" }, name: "获得一张【坚毅】",
description: "1费:获得7点格挡,然后随机消耗一张你的手牌。",
},
"shared.reward.card.fangxue": {
card: { name: "mengsan_fangxue" }, name: "获得一张【放血】",
description: "0费:失去3点生命,然后获得2点费用。",
},
"shared.reward.card.yujin": {
card: { name: "mengsan_yujin" }, name: "获得一张【余烬】",
description: "2费:造成18点伤害,然后随机消耗一张你的手牌。",
},
"shared.reward.card.wanmeidaji": {
card: { name: "mengsan_wanmeidaji" }, name: "获得一张【完美打击】",
description: "2费:造成6点伤害,本场战斗的牌中每有一张名称含“打击”的牌,伤害增加2点。",
},
"shared.reward.card.yubeidaji": {
card: { name: "mengsan_yubeidaji" }, name: "获得一张【预备打击】",
description: "1费:造成7点伤害,然后在本回合内获得2点力量。",
},
"shared.reward.card.tupo": {
card: { name: "mengsan_tupo" }, name: "获得一张【突破】",
description: "1费:失去1点生命,然后对所有敌人造成9点伤害。",
},
"shared.reward.card.touchui": {
card: { name: "mengsan_touchui" }, name: "获得一张【头槌】",
description: "1费:造成9点伤害,将个人弃牌堆中的一张牌放到个人抽牌堆顶部。",
},
"shared.reward.card.tiezhanbo": {
card: { name: "mengsan_tiezhanbo" }, name: "获得一张【铁斩波】",
description: "1费:获得5点格挡,然后对一名敌人造成5点伤害。",
},
"shared.reward.card.shuangchongdaji": {
card: { name: "mengsan_shuangchongdaji" }, name: "获得一张【双重打击】",
description: "1费:对一名敌人造成5点伤害两次,两次分别结算。",
},
"shared.reward.card.shandianpili": {
card: { name: "mengsan_shandianpili" }, name: "获得一张【闪电霹雳】",
description: "1费:对所有敌人造成4点伤害,给予1层易伤。",
},
"shared.reward.card.feijianhuixuanbiao": {
card: { name: "mengsan_feijianhuixuanbiao" }, name: "获得一张【飞剑回旋镖】",
description: "1费:每次随机对一名敌人造成3点伤害,共3次。",
},
"shared.reward.card.jianbingdaji": {
card: { name: "mengsan_jianbingdaji" }, name: "获得一张【剑柄打击】",
description: "1费:造成9点伤害,然后抽1张牌。",
},
"shared.reward.card.quanshenzhuangji": {
card: { name: "mengsan_quanshenzhuangji" }, name: "获得一张【全身撞击】",
description: "1费:造成当前格挡值的伤害,不消耗格挡。",
},
"shared.reward.card.rongrongzhiquan": {
card: { name: "mengsan_rongrongzhiquan" }, name: "获得一张【熔融之拳】",
description: "1费:造成10点伤害,再将目标已有易伤层数翻倍。消耗。",
},
"shared.reward.card.fennu": {
card: { name: "mengsan_fennu" }, name: "获得一张【愤怒】",
description: "0费:造成6点伤害,将一张自身复制品加入个人弃牌堆。",
},
"shared.reward.card.fangyu": {
card: { name: "mengsan_fangyu" }, name: "获得一张【防御】",
description: "1费:获得5点格挡。",
},
"shared.reward.card.tao": { effectId: "card_tao", name: "获得一张【桃】", description: "加入独立永久牌组" }, "shared.reward.card.tao": { effectId: "card_tao", name: "获得一张【桃】", description: "加入独立永久牌组" },
"shared.reward.card.wuzhong": { card: { name: "wuzhong" }, name: "获得一张【无中生有】", description: "加入独立永久牌组" }, "shared.reward.card.wuzhong": { card: { name: "wuzhong" }, name: "获得一张【无中生有】", description: "加入独立永久牌组" },
"shared.reward.card.upgrade": { effectId: "upgrade", name: "随机强化", description: "随机强化一张尚未满级的可强化牌" }, "shared.reward.card.upgrade": { effectId: "upgrade", name: "随机强化", description: "随机强化一张尚未满级的可强化牌" },
@@ -26,6 +98,22 @@ export const rewardPools = {
"shared.pool.battle.normal": [ "shared.pool.battle.normal": [
"shared.reward.card.sha", "shared.reward.card.sha",
"shared.reward.card.zhongsha", "shared.reward.card.zhongsha",
"shared.reward.card.fangyu",
"shared.reward.card.fennu",
"shared.reward.card.rongrongzhiquan",
"shared.reward.card.quanshenzhuangji",
"shared.reward.card.jianbingdaji",
"shared.reward.card.feijianhuixuanbiao",
"shared.reward.card.shandianpili",
"shared.reward.card.shuangchongdaji",
"shared.reward.card.tiezhanbo",
"shared.reward.card.touchui",
"shared.reward.card.tupo",
"shared.reward.card.yujin",
"shared.reward.card.fangxue",
"shared.reward.card.jianyi",
"shared.reward.card.pomie",
"shared.reward.card.songjianwushi", "shared.reward.card.wanmeidaji", "shared.reward.card.yubeidaji",
"shared.reward.card.tao", "shared.reward.card.tao",
"shared.reward.card.wuzhong", "shared.reward.card.wuzhong",
], ],
+14
View File
@@ -0,0 +1,14 @@
# 旧日雕像 BYGONE_EFFIGY
2026-10-03:https://sts2.huijiwiki.com/wiki/旧日雕像;https://sts2.huijiwiki.com/wiki/缓慢
注册mengsan_bygone_effigy_shuying;第一层精英池,普通生命127,无随机生命抽取。A8生命132、A9斩击15只记录,不混入当前普通数值。
SVG开始→沉睡→唤醒→斩击(自循环)。第1回合沉睡,取消原生出牌阶段,不耗费用;第2回合苏醒10力量,仅一次;第3回合起斩击13基础伤害,叠加力量后23。
其余意图消耗1费用,之后保留梦三精英原生出牌,抽2、上限5、费用3;原生攻击牌也受力量修正。首回合不是单纯隐藏意图后仍可以出杀。
缓慢初始能力1层,图标数值显示本回合额外伤害百分比,不是能力层数。友方共用每只雕像上的计数,每次完成使用一张牌后(useCardAfter)增加10%;攻击本身结算时用之前已完成牌数,虚拟牌/多目标牌只算一次;防御、治疗也计数。纯响应respond不计数;敌方出牌、取消使用、战斗外事件不计数。
回合边界phaseBefore/phaseAfter清零,状态仍在,不仅雕像自己回合结束才清零。友方主动回合的支援使用牌也计入相同敌方状态。
伤害倍率只作用于攻击类别卡牌或明确标记mengsanAttack_shuying的脚本攻击。非攻击遗物伤害不放大,护甲不影响计数。梦三整数伤害适配:力量/虚弱/易伤后乘(1+10%×牌数),向下取整,再由引擎抵扣护甲;意图预测使用同一计算。
费用不足与击晕沿用现有advance/retry,不重新沉睡;死亡、会话结束中止后续攻击/增益。缓慢、循环、力量会话清理,旧会话不误删新会话状态。永久牌堆不变。
原画:https://huiji-public.huijistatic.com/sts2/uploads/b/b5/Bygone_effigy.png
内置image_gen基于原画和密林风格重绘独立3:4不透明立绘;完整雕像、镰刀、盾牌、石台,无UI文字。
提示词:D:\noname\resources\app\.codex-artifacts\mengsan-hand-fan-20261001\effigy-prompts.json
原画版权归原权利方,网页文本CC BY-NC-SA 4.0。隔离测试不是实机验收。
+16
View File
@@ -0,0 +1,16 @@
# 多尼斯异鸟 BYRDONIS
2026-10-03读取:https://sts2.huijiwiki.com/wiki/多尼斯异鸟
密林精英,注册mengsan_byrdonis_shuying,加入act1Map.eliteEnemies,不加入普通池。现有精英保留。
普通生命81~84,每次实际敌方入场使用征程随机源均匀抽取;资料默认82不消耗随机数,显式hp优先。
初始领地意识1层;自身phaseAfter获得层数对应的力量,不是意图攻击后立刻获得。击晕或费用不足仍在活着的回合结束时触发。非自身回合、死亡或会话结束不结算。
行动SVG:开始→俯冲→啄击→俯冲,严格交替,无随机决策。网页表格称俯冲为“飞扑(Swoop)”,为同一行动。
俯冲17基础攻击伤害;啄击3基础攻击伤害×3,每段受力量/虚弱/易伤/缩小修正。无其他效果,正常前三回合17、4×3、19;力量在各回合结束后变为1、2、3。
进阶只记录不混入当前普通数值:A8生命90,A9俯冲19、啄击4×3。
使用梦三精英原生抽2、手牌上限5、费用3和精英牌堆。意图消耗1费用,原生出牌阶段保留;原生牌也获得既有力量修正,脚本意图不重复加力量。
费用不足跳过并推进;击晕retry保留招式、advance推进;眩晕恢复不移除领地意识。多只实例独立,不因重复规划、席位或死亡恢复重新抽生命。
循环、领地意识和力量随战斗会话清理;旧会话清理不能误删新会话的状态。
网页原画来源:https://huiji-public.huijistatic.com/sts2/uploads/d/d5/Byrdonis.png
立绘参考原画及既有密林风格由内置image_gen重绘,单只异鸟独立3:4不透明背景,无UI文字。
提示词:D:\noname\resources\app\.codex-artifacts\mengsan-hand-fan-20261001\byrdonis-prompts.json
原画版权归原权利方;网页文本CC BY-NC-SA 4.0。隔离检查不代表游戏实机验收。
+18
View File
@@ -0,0 +1,18 @@
# 立柱构造体 CUBEX_CONSTRUCT
2026-10-03读取:https://sts2.huijiwiki.com/wiki/立柱构造体
普通密林怪物;生命65,开场13格挡与1层人工制品。注册mengsan_cubex_construct_shuying,加入第一层普通怪物池。
蓄能:2力量,仅开场。以后重复轰击→重复轰击→排出循环。重复轰击先7基础攻击伤害后2力量;排出5基础伤害×2。每段受力量/虚弱/易伤/缩小修正。
SVG路径:A→蓄能→重复轰击→重复轰击2→排出→重复轰击,明确不回到蓄能。
初次命中预期(无其他修正):9、11、11×2,之后13、15、15×2。
进阶仅记录不混入正常难度:A8生命70;A9重复轰击8、排出6×2,蓄能仍2力量。
人工制品:https://sts2.huijiwiki.com/wiki/人工制品,页面提示“免疫负面效果”,正面可叠加不允许负数。
计次规则参考该页所链接前作人工制品:https://sts.huijiwiki.com/wiki/人工制品(免疫X次负面效果)。在梦三按每层抵消一次负面状态施加适配;一次施加多层也只消耗1层。
接入易伤/虚弱/脆弱/缠结/紧缠/缩小,包括重杀、闪电霹雳、融融之拳及开场遗物。伤害先结算、正面力量不消耗人工制品;无效/零层申请、已有状态自然衰减不消耗。
击晕是梦三既有意图控制而非叠层状态,维持既有retry/advance规则;不拦截原生三国杀乐不思蜀等原生技能。
意图消耗1费用,随后可用原生牌。费用不足跳过并推进;眩晕retry保留当前招式、advance推进。死亡与战斗结束中止后续段及力量增益。
人工制品及循环状态仅保留于本场战斗,使用会话资源清理与旧会话防误删保护,不写入永久牌堆。
立绘:网页原画Cubex_construct.png实际WebP,来源https://huiji-public.huijistatic.com/sts2/uploads/3/3a/Cubex_construct.png;内置图像工具参考原画及密林风格重绘独立不透明3:4背景立绘。
原画版权归原权利方;页面文本CC BY-NC-SA 4.0。
提示词:D:\noname\resources\app\.codex-artifacts\mengsan-hand-fan-20261001\cubex-prompts.json
隔离测试不代表实机验收。
+14
View File
@@ -0,0 +1,14 @@
# 雾菇与利齿之眼
来源:[雾菇](https://sts2.huijiwiki.com/wiki/雾菇)、[晕眩](https://sts2.huijiwiki.com/wiki/晕眩),读取日期2026-10-02。
网页立绘保留原始字节;Wiki标注CC BY-NC-SA 4.0。晕眩卡图复用项目现有STS2资源。
- 默认使用网页普通难度:雾菇74生命;重击8伤害并在结算后获得1力量;头槌14伤害。
- 第一自身行动使用虚幻孢子,召唤一个6生命的利齿之眼。之后先重击;一次重击后40%再重击、60%头槌;两次连续重击后必定头槌;头槌后必定重击。
- 利齿之眼只发动牵制:向敌对角色的个人弃牌堆加入3张晕眩。无个人牌堆的剧情援军不污染公共牌堆。
- 晕眩仅存在于本场战斗,不能使用或打出,弃牌阶段开始按既有虚无机制移除;不进入永久牌组或奖励池,没有费用角标。
- 利齿之眼死亡后,在下一次主角回合开始前满血复活,复用原席位;雾菇死亡后不再复活或行动,爪牙不阻止胜利。
- 第一关普通遭遇池增加雾菇;利齿之眼不单独进入遭遇池。其他关卡和刘备专属开场不变。
- 沿用梦三行动技能消耗1费用、雾菇随后照常出牌的既有规则;爪牙不额外抽牌或出牌。本次未新增STS2进阶难度开关。
检查属于隔离验证,不代表完整游戏或十周年UI实机验收。
+27
View File
@@ -0,0 +1,27 @@
# 毛绒伏地虫(Fuzzy Wurm Crawler)
来源:https://sts2.huijiwiki.com/wiki/毛绒伏地虫 ,读取日期2026-10-02。
原立绘:https://huiji-public.huijistatic.com/sts2/uploads/2/21/Fuzzy_wurm_crawler.png 。
Wiki页面标示CC BY-NC-SA 4.0,原游戏美术权利归对应权利人;保留来源说明。
## 普通配置
- ID:`mengsan_fuzzy_wurm_crawler_shuying`;加入第一层普通战斗池,不影响角色专属开局。
- 实际入场时使用征程随机源,从55、56、57等概率抽取生命;资料展示和未指定随机源的怪物定义为56。显式关卡生命配置优先。
- 三步循环:酸液黏球 -> 吸入 -> 酸液黏球 -> 回到开头;周期相邻边界会出现两次酸液攻击。
- 酸液黏球基础伤害4,受累计力量影响。
- 吸入获得7力量;力量累积到战斗结束,不设置次数上限。
- 未受到其他修正时,前几次酸液实际伤害为4、11、11、18、18;不是永久固定4。
- 每招消耗1费用,敌方保留个人牌堆使用阶段,酸液作用于所有存活敌对阵营角色。
- 无费用则跳过并推进三步循环,不获得力量。击晕advance推进;retry保持原循环位置,恢复原意图后再尝试,不重复授予力量。
- 网页进阶8+生命58~59、9+酸液基础6,本次只记录,沿用现有普通数值,不混入普通关卡。
## 立绘
- `assets/portraits/fuzzy-wurm-crawler-original.webp`是网页透明图,按实际WebP格式保存。
- `assets/portraits/fuzzy-wurm-crawler-forest-portrait.png`由内置图像工具基于原图、参照已有密林立绘独立绘制,紫色毛绒身体、圆形利齿口、浅色分节腹部和侧向触须保留,背景完全不透明。
- 只包含本怪物,不与其他怪物共用立绘。
## 验证边界
隔离检查覆盖生命抽取和真实入场初始化函数、三步循环、累计力量与意图显示、费用、击晕、死亡/停止会话和旧怪物回归。不等同游戏实机运行或十周年UI视觉验收。
+24
View File
@@ -0,0 +1,24 @@
# 墨宝(Inklet)
来源:https://sts2.huijiwiki.com/wiki/墨宝 、https://sts2.huijiwiki.com/wiki/滑溜 ,读取日期2026-10-02。
网页原画:https://huiji-public.huijistatic.com/sts2/uploads/7/7b/Inklet.png 。
Wiki标示CC BY-NC-SA 4.0;原游戏美术权利归对应权利人。保留原图及来源记录。
## 配置
- ID `mengsan_inklet_shuying`,普通怪物;加入第一层普通战斗池。角色专属开局不变。
- 普通生命11~17;实际入场通过征程随机源等概率抽取,显式关卡生命优先,资料默认14。
- 网页描述成群且前/中/后起手不同,未指定固定数量;梦三适配为三只群体关卡,不声称三只是网页固定数量。
- 各只固定出生位置1/2/3,不因同伴死亡或支援插队重排。奇数位刺击3伤害起手;偶数位旋风2×3起手后刺击。
- 起手后进入循环:等概率选择锐利凝视10或旋风2×3 -> 刺击3 -> 等概率选择 -> 刺击。随机招不是每回合任意三选一。
- 各次意图仅生成一次;预览、重复计划、击晕retry不会重抽。每招1费用,无费用跳过并推进;击晕advance推进,retry保留。
- 沿用梦三怪物适配:意图攻击所有存活敌对阵营角色;保留个人牌堆;死亡或会话关闭及时停止多段攻击。
- 初始滑溜1:下次实际生命损失限制为1点;伤害完全被护甲抵挡不消耗;直接失去生命也适用。每段分别结算,第一段消耗后其他段正常。
- 梦三模式局部changeHp包装保留原生事件、护甲抵挡及消耗,滑溜层数仅在实际changeHp负向通知时减少,不改全局引擎。正向恢复、零伤害、关闭会话和其他怪物不受影响。斩杀直接死亡不拦截。
- 网页进阶8+生命12~18,9+刺击4、旋风3×3、凝视11,本次只记录,不混入普通配置。
## 立绘与检查
- 原图保存为 `assets/portraits/inklet-original.png`;独立立绘为 `assets/portraits/inklet-forest-portrait.png`。
- 内置图像工具以原图为造型参考、飞蝇菌子立绘为画风参考,生成单只墨黑身体、发光眼缝、不透明密林背景竖图;不与其他怪物共用立绘。
- 隔离验证覆盖实际入场初始化、群体计划、站位、随机循环、真实本地引擎changeHp内容的护甲/生命结算、滑溜消耗、费用与击晕、会话停止、旧怪物回归。不是实机或十周年UI验收。
+13
View File
@@ -0,0 +1,13 @@
# 树叶史莱姆(中)
来源:https://sts2.huijiwiki.com/wiki/树叶史莱姆(中)
普通配置:生命32~35(展示默认34);无初始能力。第1回合黏糊射击,向敌对角色个人弃牌堆加入2张黏液;随后团块射击(8点攻击伤害)与黏糊射击严格交替。
进阶参考:8+生命33~36,9+攻击9;当前未混入普通配置。
怪物ID:`mengsan_leaf_slime_m_shuying`,加入第一幕普通怪物池;首层专属开局及后续幕不变。每次意图耗1费用,保留原生用牌阶段;眩晕retry保留意图、advance进入下一行动。复用已有黏液卡(耗1费用,抽1张,消耗),不进入永久卡组和奖励注册表。中型状态与小型独立,后者仍等权随机冲撞/黏液。
原立绘:https://huiji-public.huijistatic.com/sts2/uploads/9/96/Leaf_slime_m.png
使用内置image_gen参照原立绘、密林风格二绘,单独不透明3:4立绘。网页与资源依据灰机Wiki所示许可,保留来源。
验证为隔离执行真实项目模块及引擎边界夹具,不是实机验收。
+17
View File
@@ -0,0 +1,17 @@
# 树叶史莱姆(小)
资料:https://sts2.huijiwiki.com/wiki/树叶史莱姆(小) ,状态卡:https://sts2.huijiwiki.com/wiki/黏液 。读取日期2026-10-02。
普通数值:生命11~15,资料默认13,无初始能力。仅实际入场从征程随机源抽取生命;进阶8+生命12~16、9+冲撞4伤害不混入普通配置。
每次预测行动在冲撞(3伤害)和黏液(给各存活敌对角色的个人弃牌堆加入1张黏液)之间等权重随机。网页未标注冷却/连续次数限制,不额外添加。无个人牌堆的剧情援军不污染公共牌堆。每个预测行动耗1费用,保留原生出牌阶段。
黏液是可使用的状态牌:耗1费用、抽1张牌、消耗。不同于晕眩,不具不能打出或虚无。未使用时可正常弃置并重新抽到;使用后通过既有个人牌堆消耗机制退出本场战斗。状态牌不进入永久可获得牌表、奖励、强化表或存档牌组。
击晕advance推进一次、retry保留原预测行动;费用不足跳过效果并推进。重复意图刷新不重复抽随机数,死亡或战斗结束不再产生效果。
已接入怪物类、角色注册、第一层普通关卡池、行动/意图UI、击晕和力量修正;不改专属首关、第二/第三层、全局十周年UI。
网页透明立绘作为身份参考、飞蝇菌子密林立绘作为画风参考,由内置图像工具生成单独3:4不透明密林背景立绘。黏液卡图使用网页原始PNG,按现有全幅卡面呈现,不改写图片。完整提示词和来源位于验证目录leafslime-prompts.json。
验证为隔离状态机及实际源码执行,不代表实机/十周年UI验收。
+26
View File
@@ -0,0 +1,26 @@
# 蛮兽(Mawler)
资料来源:https://sts2.huijiwiki.com/wiki/蛮兽 (2026-10-02读取)。
网页原图:https://huiji-thumb.huijistatic.com/sts2/uploads/thumb/a/a6/Mawler.png/360px-Mawler.png 。
Wiki 页面标明 CC BY-NC-SA 4.0;角色及美术的原始权利归对应权利人,本项目保留来源说明。
## 配置与行动
- 角色ID:`mengsan_mawler_shuying`;第一层普通关卡随机池,不替换角色专属开局。
- 普通数值:72生命;网页进阶8+为76生命,进阶9+爪击5×2、狂乱撕扯16,仅记录,不映射到现有普通难度。
- 爪击:4点伤害,独立结算2次。首个回合必定选择;后续不能连续发动。
- 狂乱撕扯:14点伤害,不能连续发动。
- 怒吼:给予3层易伤,整场战斗最多成功发动一次。
- 首回合后从符合条件的招式中等权重选择;怒吼成功发动后永久移出该战斗的池。
- 规则沿用梦三:每招消耗1费用,作用于所有存活敌对阵营角色;仍可继续使用个人牌堆中的牌。
- 无费用和击晕不消耗怒吼次数;击晕支持推进或重试,意图不重复抽取随机数,不跨战斗共享状态。
## 立绘
- `assets/portraits/mawler-original.webp`:网页透明原图,按实际WebP格式保存。
- `assets/portraits/mawler-forest-portrait.png`:基于网页原形象,参照已有飞蝇菌子密林画风,经内置图像工具独立重绘;完整不透明背景、竖版3:4,头部置中供头像裁切。
- 本图只包含蛮兽,不与其他怪物共用。
## 验证范围
隔离验证覆盖注册、普通关卡池、冷却/等权重/单次怒吼、双段伤害/易伤、费用、击晕、死亡和会话停止、意图显示与旧怪物回归;不等同真实游戏运行或十周年UI视觉验收。
+14
View File
@@ -0,0 +1,14 @@
# 小啃兽 NIBBIT
来源:https://sts2.huijiwiki.com/wiki/小啃兽 (2026-10-02读取网页与SVG箭头)
普通怪物,密林;生命42~46,实际入场从征程随机源均匀抽取;资料默认44,不消耗随机数。无初始能力。
正常行动:顶撞12伤害 → 犹豫斩击6伤害并获得5格挡 → 哈气获得2力量 → 顶撞。
单只顶撞起手;双只按关卡units前后顺序固定为斩击/哈气起手,各自进入同一循环。开战时固定状态,不因同伴死亡或席位变化重置。
第一层普通池包括单只与双只遭遇;双只不占用永久牌堆/支援数据。超过两只的非标准自定义配置采用单只起手作为兼容回退,非Wiki定义。
意图消耗1费用、随后仍可使用原生卡牌;费用不足跳过本次动作并推进,眩晕支持retry/advance,遵循既有梦三适配。
力量影响脚本攻击与原生牌伤害;斩击先攻击后获格挡。既有伤害执行器继续处理虚弱、易伤及死亡/战斗结束。
进阶数据仅记录不启用:A8生命44~48、斩击格挡6;A9顶撞13、斩击7、哈气力量3。
立绘:网页原画Nibbit.png(实际WebP)仅作参考;内置图像工具参考原画与密林立绘重绘独立不透明3:4背景图。
原画署名/来源:https://huiji-public.huijistatic.com/sts2/uploads/f/f6/Nibbit.png;页面文本CC BY-NC-SA 4.0,原画版权归原权利方。
完整提示词:D:\noname\resources\app\.codex-artifacts\mengsan-hand-fan-20261001\nibbit-prompts.json
验证属于隔离运行与实际文件检查,不代表实机验收。
+15
View File
@@ -0,0 +1,15 @@
# 异蛙寄生虫 / 扭动虫 / 感染
2026-10-03依据 https://sts2.huijiwiki.com/wiki/异蛙寄生虫 与 https://sts2.huijiwiki.com/wiki/感染。
普通数值:异蛙61~64生命,第一层精英池;固定感染(3张感染进个人弃牌)→甩动(4×4)循环。
寄生物4,死亡即召唤4只扭动虫。主网页正文以四只描述衍生怪,X未显式显示数值,交叉核对 https://slaythespire.wiki.gg/wiki/Slay_the_Spire_2:Phrog_Parasite 明确死亡生成4只。
扭动虫17~21生命,不单独进入精英关卡随机池;不是雾菇幻象,必须全部消灭才算胜利。
各组站位固定1~4,奇数位污秽啃咬6→扭动(1感染、2力量)循环,偶数位反向开局。
亡语生成的扭动虫完整首回合取消,不抽牌、不发动意图、不出原生牌、不耗费用,恢复后按原始奇偶站位起手。直接配置的扭动虫不强制跳过。
其余意图消耗1费用,保留梦三精英原生出牌;抽2,上限5,费用3。力量也作用于原生攻击。
死亡钩子原生引擎会await;等待4只入场后再继续伤害/卡牌,召唤期间阻止胜利结算。开战预留4个额外席位,计入死亡保留席位和所有后续剧情召唤,超过8总席位提前报错,不静默减少数量。
飞剑回旋镖原来逐击重新取存活敌人,后续击可以命中新生扭动虫。闪电霹雳改为先完整伤害再向当前存活敌人施加易伤,新生扭动虫也获得易伤,不重复吃刚结束的范围伤害。
感染不能被打出,无默认虚无;只生成到战斗个人弃牌,不进入永久奖励、商店或征程牌组。持有者结束出牌后的弃牌阶段开始时,每张仍在手牌的感染造成3点非攻击无来源伤害,走正常护甲及濒死救援;优先于虚无消耗,事件去重,死亡/换场中止后续结算。此时机沿用梦三现有虚无/弃牌实现,不是结束阶段之后重新读取空手牌。
A8异蛙66~68、虫18~22;A9甩动5×4、啃咬7只记录,不混入普通数值。按用户给定网页主表感染3,不混入其他网站较新测试补丁的4。
两种怪物分别采用基于网页原画的3:4不透明密林重绘,感染为5:7宣纸黑褐水墨完整卡面。无UI文字或角标。内置image_gen提示词保存于 D:\noname\resources\app\.codex-artifacts\mengsan-hand-fan-20261001\phrog-prompts.json。
原画版权归原权利方,网页文本CC BY-NC-SA 4.0;隔离验证不等于实机验收。
+14
View File
@@ -0,0 +1,14 @@
# 缩小甲虫
来源:https://sts2.huijiwiki.com/wiki/缩小甲虫
普通生命38~40(展示默认39),无初始能力。首回合缩小:给予存活敌对角色缩小状态;随后大啃7点、践踏13点攻击伤害严格交替。页面SVG行为箭头已逐条核对。
进阶8+生命40~42、9+攻击8/14,仅参考,不混入普通数值。
ID:`mengsan_shrinker_beetle_shuying`,第一幕普通怪物池,专属开局和后续幕不变。意图耗1费用、保留原生用牌阶段;眩晕retry保留动作、advance推进。HP沿用种子随机,行动不使用随机数。
缩小令攻击伤害减少30%,非攻击伤害不变。灰机状态页未列出持续与叠加规则,交叉核对 https://slaythespire.wiki.gg/wiki/Slay_the_Spire_2:Shrinker_Beetle 的怪物描述:不叠加、来源死亡移除;甲虫汁的限时效果不是本怪物技能。本地采用既有伤害管线:力量→虚弱→缩小向下取整→易伤向上取整→原生护甲。来源引用保存在本场战斗,死亡/战斗清理移除,复活不恢复旧缩小;多来源减伤仍只30%。不改角色整体尺寸或十周年UI,状态以“缩”标记显示。
原画:https://huiji-public.huijistatic.com/sts2/uploads/a/ab/Shrinker_beetle.png
网页原画为透明立绘,内置image_gen按原画身份与已有密林风格二绘独立不透明3:4立绘;保留来源及网页所示CC BY-NC-SA 4.0许可提示。生成提示保存于验证记录。
模块隔离与边界夹具检查不是实机验收。
+17
View File
@@ -0,0 +1,17 @@
# 蛇行扼杀者
资料:https://sts2.huijiwiki.com/wiki/蛇行扼杀者 ,紧缠:https://sts2.huijiwiki.com/wiki/紧缠
实现普通数值,不将进阶8+/9+数值混入普通关卡。生命53~55,资料默认54;仅实际入场从征程随机源抽取生命。无初始能力。
- 缠身:给予所有存活敌对角色3层紧缠。
- 缠身后等权重随机重击(7伤害、结算攻击后获得5护甲)或甩动(12伤害);攻击后必定回到缠身。
- 紧缠可叠加,每个受缠角色自己的结束阶段开始时受到层数对应的非攻击伤害,原生护甲正常抵扣;不自动衰减,不受力量、虚弱和易伤的攻击修正。
- 按施加来源独立记录。同名多怪不会互相覆盖;来源死亡立即移除其贡献,复活不会恢复旧紧缠;受缠角色死亡及战斗资源释放时清除。本状态只保存在本场战斗,不写入征程存档。
- 预测行动每次耗1费用;费用不足则跳过效果并推进一次。击晕advance推进一次、retry保留原意图,随机攻击不会因重复刷新或retry重新抽取。保留原生出牌阶段。
已注册角色、怪物类型、普通关卡随机列表、行动执行器、意图UI、击晕与力量适配。角色专属首关及后续章节不改。
立绘:网页透明原图作为身份参考,飞蝇菌子密林立绘作为画风参考,经内置图像生成工具二次绘制为独立3:4不透明背景图片。完整提示词位于同一验证目录的strangler-prompts.json。
验证仅为隔离状态机/实际源码执行检查,不代表用户实机或十周年UI验收。
+28
View File
@@ -0,0 +1,28 @@
# 闪光贾克斯果(Snapping Jaxfruit)
来源:https://sts2.huijiwiki.com/wiki/闪光贾克斯果 ,读取日期2026-10-02。
网页原画:https://huiji-public.huijistatic.com/sts2/uploads/5/59/Snapping_jaxfruit.png 。
Wiki标示CC BY-NC-SA 4.0;原游戏美术权利归对应权利人,保留原图和来源。
## 普通配置
- ID `mengsan_snapping_jaxfruit_shuying`;第一层普通关卡池,不修改角色专属开局及后续层。
- 普通生命31~33,实际入场使用征程随机源等概率抽取。显式关卡生命优先;资料默认32且不消耗随机数。
- 没有初始能力。每次使用能量球:以基础伤害3加当前力量攻击,之后自身获得2力量,持续叠加至战斗结束,不设成长上限。
- 无其他修正时能量球伤害序列为3、5、7、9、11……;本次获得的力量不提前加到本次攻击。
- 沿用梦三敌方适配:意图攻击所有存活敌对角色;对多个目标攻击完成后只获得一次2力量,不按目标数叠加;保留个人牌堆使用阶段。
- 每招消耗1费用,无费用跳过且不攻击、不获得力量。击晕advance推进记录,retry保留原意图,不重复发动或增益;重复计划不消耗行动随机数。
- 力量也作用于原生卡牌攻击;已应用力量的脚本攻击不重复加成。虚弱、易伤与预览一致。
- 攻击中若自身因反伤死亡或会话停止,则停止后续目标且不再获得力量。全部目标死亡后,如果自身仍存活且会话仍活跃,能力仍可正常增加力量。
- 新战斗不继承上场力量或行动缓存,持久玩家牌堆不变。
- 网页进阶8+生命34~36,9+能量球基础伤害4:仅记录,不混入普通配置。
- 网页0.108.0“不再连续遇到两场都包含该怪物”标为未进入正式版,本次不实现该待发布规则。
## 立绘
- `assets/portraits/snapping-jaxfruit-original.png`保留网页原图。
- `assets/portraits/snapping-jaxfruit-forest-portrait.png`为内置图像工具独立绘制,保留红紫色发光果实、绿色叶片、暗红尖萼和三根分叉芽触须,添加不透明手绘密林背景。仅包含本怪物。
## 验证边界
隔离验证覆盖真实入场函数、31~33生命、固定单招、攻击先于力量增加、多目标只增益一次、费用、击晕、虚弱/易伤、死亡及会话停止、原生卡牌力量及旧怪物回归。不是完整游戏或十周年UI实机验收。
+11
View File
@@ -0,0 +1,11 @@
# 树枝史莱姆(中)
来源:https://sts2.huijiwiki.com/wiki/树枝史莱姆(中)
普通生命26~28(注册展示默认27),无初始能力。首回合黏糊射击,向敌对角色个人弃牌堆加入1张黏液;黏糊射击不可连续使用,必接戳刺扑击(11点攻击伤害)。允许的随机分支等权;戳刺扑击最多连用两次,达到上限后必接黏糊射击。
进阶8+生命27~29、9+攻击12,仅参考,未混入普通配置。
ID:`mengsan_twig_slime_m_shuying`,第一幕普通怪物池;专属开局及后续幕不变。独立lastMove/consecutive状态,计划意图不推进,实际行动或advance眩晕推进,retry保持动作与连续计数。每次意图消耗1费用,保留原生用牌阶段。复用已有黏液卡:耗1费用、抽1张牌、消耗,不进入永久卡组或奖励。
原立绘:https://huiji-public.huijistatic.com/sts2/uploads/c/c5/Twig_slime_m.png
内置image_gen按网页原图、密林画风二绘独立不透明3:4立绘,不替换小型图。保留灰机Wiki来源及其所示许可信息。
隔离模块和边界夹具验证不是实机验收。
+11
View File
@@ -0,0 +1,11 @@
# 树枝史莱姆(小)
来源:https://sts2.huijiwiki.com/wiki/树枝史莱姆(小)
普通怪物,密林;无初始能力。普通生命7~11(注册默认9),每回合冲撞造成4点攻击伤害。进阶8+生命8~12、9+攻击5,仅记录参考,未混入普通配置。
ID:`mengsan_twig_slime_s_shuying`。第一幕普通怪物池包含此怪物,专属开局及其他幕不变。行动耗1费用,保留原生卡牌阶段;retry眩晕保留意图,advance推进记录。行动不消耗随机数,初始生命沿用种子随机。没有黏液、分裂或其他网页未列出的能力。
原图:https://huiji-public.huijistatic.com/sts2/uploads/8/8e/Twig_slime_s.png
内置image_gen参考网页原图与密林风格二绘,不透明3:4独立立绘,不替换树叶史莱姆。保留灰机Wiki来源及所示许可信息。
隔离验证真实项目模块,不是实机验收。
+11
View File
@@ -0,0 +1,11 @@
# 梦三手牌:41、42 图交互修正
本次基线是用户已应用的扇形手牌版本,修改限于 `ui/hand/`,不改十周年源码或设置。
- `showAim()` 将鼠标及卡牌的视口坐标,经 SVG `getScreenCTM().inverse()` 换算到 SVG 局部坐标;兼容 body zoom、父层缩放与平移,不重复套用 documentZoom。
- 自动单目标/无目标拖动期间,临时隐藏 `ui.control`、`ui.confirm` 和当前事件的 `endButton`。取消、松手、换事件及退出恢复;多目标维持原确认按钮。
- 合法自动提交时,沿原生鼠标提交流程先关闭旧确认及结束控件,再调用 `ui.click.ok()`。关闭动画中的旧控件不再残留显示;不关闭下一次新创建的控件。
- 新挂载标记将阶段图片隐藏范围限制在梦三手牌生命周期。仅匹配十周年 `lbtn/tips/` 的阶段图片,不隐藏响应提示图片。
- 短点击、复杂/技能/响应选择仍走原生路径;自定义确认替换的事件不接管。
验证是隔离浏览器测试,不等同于所有 UI 扩展组合及实际游戏验收。应用脚本直接写入对应文件,不生成安装目录备份;交付修正副本与安装状态须区分。
+38
View File
@@ -0,0 +1,38 @@
# 梦三扇形手牌:本次候选变更
## 参考与范围
依据用户本地 36.png(扇形)、37.png(悬停)、38.png(目标牌拖动)与39.png(无目标牌拖动)。
仅扩展现有梦三 `ui/hand/` 子模块;不改十周年UI源码,不复制真实卡面,不搬动卡牌父容器,不自行调用 `useCard()`。
卡面仍沿用当前引擎/主题;不是尖塔卡面的重绘。
## 排列与悬停
- `fanSlots()` 将全部可见真实手牌以中心对称展开;中心最高,两端降低并旋转,最多左右13度。
- 桌面卡牌112×156,小屏82×114;张数增加时收窄间距,不依赖横向滚动。
- 悬停时归正、上抬62像素并放大1.35倍,置于手牌最上层;保持原生选中/可用状态。
- 原生布局写入的 transform 不被删除;梦三CSS仅在启用时覆盖,退出恢复。
## 输入
- 主动出牌阶段、真实单张手牌且费用足够:位移9像素进入拖动。
- 单目标牌:卡牌自动置于手牌横向中心,指针拖动一条金色细线;指向合法目标后松手,经 `game.check()` 再调用 `ui.click.ok()`。
- 金线参照本地十周年 `effects/config.js` 的 rgb(250,220,140) 与2.6线宽;独立SVG,不加载其私有动画接口,不用尖塔箭头/骨节。
- 无需手动指定目标(包括引擎自动分配全部目标)的牌:卡牌随指针,上移至少60像素且越过手牌区上边缘后松手确认。没有在移动途中扣费或使用。
- 手动多目标:用户确认保留原来的选目标与确认;本模块只保留选中卡牌,不自动替用户选齐目标或提交。
- 技能虚拟牌、响应、复杂选择、自定义替换处理、已存在选择等不接管,继续走原引擎交互。
- 短点击保留原生行为;无合法目标、费用不足、Escape、右键、失焦、指针取消回收拖动,不能误出牌。
## 生命周期与安全
新 `fan.js` 负责命名空间变量、悬停和拖动位置;`input.js` 负责局部输入转接。
`mode.js` 向挂载函数传入现有引擎的 get/_status/canPayCard/isActiveCardUse,未改变出牌经济。
选牌和试选目标调用期间同步暂设 touchnocheck,避免移动途中自动确认;finally恢复原值及属性是否存在。
只在当前事件身份未改变时取消自身选择;换事件、换容器、会话结束后不提交旧事件。
卸载移除监听、SVG、观察器、标记与变量,保留真实卡牌和其他扩展的属性;重复卸载安全。
## 证据边界与交付
已运行独立浏览器场景,使用本次真实子模块、原生 `ui.click.card` 函数体;目标选择、确认及 game.check 服务仍为测试替身。
这不等于完整游戏、十周年布局、技能、真实触屏或AI实测通过。需用户进入梦三核对真实卡面尺寸与目标点击。
交付为候选源码包和可应用补丁;已安装源码保持原字节未改动。回滚脚本只接受候选副本ZIP,恢复本次基线覆盖包,不会触碰游戏安装目录。
+13
View File
@@ -0,0 +1,13 @@
# 藤蔓蹒跚者
来源:https://sts2.huijiwiki.com/wiki/藤蔓蹒跚者 与 https://sts2.huijiwiki.com/wiki/缠结
普通生命固定61,无初始能力;挥击(6点攻击×2)→紧绕藤蔓(8点攻击,之后施加1回合缠结)→大啃(16点攻击)循环。已核对网页SVG箭头。进阶8+生命64,9+伤害7×2/9/18,仅参考,不混入普通配置。
ID `mengsan_vine_shambler_shuying`;第一幕普通怪物池,角色专属开局和后续幕不变。每次意图耗1费用、保留原生用牌阶段;retry不推进、advance推进;固定生命和行动均不额外消耗种子随机数。
缠结持续回合可叠加,但攻击牌始终只加1费用,自身回合结束减1;状态不与来源死亡绑定。梦三攻击牌按现有damage类别识别,另外涵盖南蛮、万箭、火攻;防御、回复、状态牌不加费。加费按实际使用的最终牌名和当前强化费用计算(0费攻击变1),响应和濒死救援沿用不付费规则。费用校验、支付、玩家手牌角标、实体手牌提示一致,永久牌库和奖励展示保持基础费用。状态引用仅存在本场战斗,结束清理,不写入永久卡组或存档。
原尺寸图片下载失败,使用页面已加载的360px原画缩略图作为身份参考(实际格式WebP):https://huiji-thumb.huijistatic.com/sts2/uploads/thumb/1/1c/Vine_shambler.png/360px-Vine_shambler.png
内置image_gen按原画身份和已有密林风格生成不透明3:4独立立绘。保留灰机Wiki来源和所示CC BY-NC-SA 4.0许可提示。提示词在本次验证记录中。
隔离真实模块及边界夹具验证不是游戏实机验收。
+674 -49
View File
@@ -1,5 +1,14 @@
import { createScenarioCharacters, scenarioTranslations } from "./content/scenario-characters.js"; import { createScenarioCharacters, scenarioTranslations } from "./content/scenario-characters.js";
import { createMengsanCards } from "./cards/mode-cards.js"; import { createMengsanCards } from "./cards/mode-cards.js";
import { createStrikeCounter } from "./cards/strike-counter.js";
import { createBlockDrawer } from "./cards/block-draw.js";
import { createHavocPlayer } from "./cards/havoc-play.js";
import { createHandExhauster } from "./cards/hand-exhaust.js";
import { createBattleEnergy } from "./cards/battle-energy.js";
import { createRandomHandExhauster } from "./cards/random-hand-exhaust.js";
import { createBattleCardCopier } from "./cards/battle-card-copy.js";
import { createBattleCardReclaimer } from "./cards/battle-card-reclaim.js";
import { createTemporaryStrength, temporaryStrengthAmount, temporaryStrengthExpires, clearTemporaryStrength, forgetTemporaryStrength } from "./cards/temporary-strength.js";
import { createRelicBattle, createRelicSkills } from "./relics/battle.js"; import { createRelicBattle, createRelicSkills } from "./relics/battle.js";
import { grantRelic, getRelic, initializeRelicMaxHp } import { grantRelic, getRelic, initializeRelicMaxHp }
from "./relics/definitions.js"; from "./relics/definitions.js";
@@ -20,7 +29,32 @@ import {
isFlyconid, isFlyconid,
isRaider, isRaider,
} from "./monsters/actions.js"; } from "./monsters/actions.js";
import { constrictTotal, resolveConstrict, clearConstrictSource } from "./monsters/strangler-constrict.js";
import { isShrunk, clearShrinkSource, shrinkAttackDamage } from "./monsters/shrinker-status.js";
import { advanceTangled } from "./monsters/vine-tangled.js";
import { isMawler, recordMawlerAction } from "./monsters/mawler-intent.js";
import { isVine, recordVineAction } from "./monsters/vine-intent.js";
import { isCubex, recordCubexAction, initializeCubex } from "./monsters/cubex-intent.js";
import { isEffigy, recordEffigyAction, initializeEffigy } from "./monsters/effigy-intent.js";
import { slowPercent, canCountSlowCard, recordSlowCard, resetSlow, slowApplies, slowAttackDamage } from "./monsters/effigy-slow.js";
import { BYRDONIS_CHARACTER, isByrdonis, rollByrdonisHp, recordByrdonisAction, initializeByrdonis, resolveTerritorial } from "./monsters/byrdonis-intent.js";
import { NIBBIT_CHARACTER, isNibbit, rollNibbitHp, recordNibbitAction, bindNibbitOpenings } from "./monsters/nibbit-intent.js";
import { SHRINKER_CHARACTER, isShrinker, rollShrinkerHp, recordShrinkerAction } from "./monsters/shrinker-intent.js";
import { TWIGMEDIUM_CHARACTER, isTwigmedium, rollTwigmediumHp, recordTwigmediumAction } from "./monsters/twigmedium-intent.js";
import { TWIGSLIME_CHARACTER, isTwigslime, rollTwigslimeHp, recordTwigslimeAction } from "./monsters/twigslime-intent.js";
import { LEAFMEDIUM_CHARACTER, isLeafslimeMedium, rollLeafslimeMediumHp, recordLeafslimeMediumAction } from "./monsters/leafmedium-intent.js";
import { LEAFSLIME_CHARACTER, isLeafslime, rollLeafslimeHp, recordLeafslimeAction } from "./monsters/leafslime-intent.js";
import { STRANGLER_CHARACTER, isStrangler, rollStranglerHp, recordStranglerAction } from "./monsters/strangler-intent.js";
import { JAXFRUIT_CHARACTER, isJaxfruit, rollJaxfruitHp, recordJaxfruitAction } from "./monsters/jaxfruit-intent.js";
import { CRAWLER_CHARACTER, isCrawler, rollCrawlerHp, recordCrawlerAction } from "./monsters/crawler-intent.js";
import { INKLET_CHARACTER, isInklet, rollInkletHp, recordInkletAction, capInkletHpLoss } from "./monsters/inklet-intent.js";
import { mountEnemyIntent } from "./ui/intent-display.js"; import { mountEnemyIntent } from "./ui/intent-display.js";
import { createFogmogBattle } from "./monsters/fogmog-battle.js";
import { createPhrogBattle } from "./monsters/phrog-battle.js";
import { PHROG_CHARACTER, WRIGGLER_CHARACTER, INFESTED_COUNT, isPhrogActor, isWriggler, rollPhrogHp, rollWrigglerHp, recordPhrogAction, initializePhrog, skipSpawnedWriggler } from "./monsters/phrog-intent.js";
import { infectionHand, resolveInfection } from "./cards/infection-card.js";
import { isFogmog, isToothedEye, isFogmogActor, isEncounterEnemy,
recordFogmogAction } from "./monsters/fogmog-intent.js";
import { mountEnergy } from "./ui/energy-display.js"; import { mountEnergy } from "./ui/energy-display.js";
import { mountHandUI } from "./ui/hand/index.js"; import { mountHandUI } from "./ui/hand/index.js";
import { import {
@@ -105,13 +139,26 @@ const chooseRun = async savedRun => {
], "单一自动存档 · 完成节点后记录进度", { eyebrow: "水墨行军 · 存档选择" }); ], "单一自动存档 · 完成节点后记录进度", { eyebrow: "水墨行军 · 存档选择" });
}; };
const chooseCharacter = async () => { const chooseCharacter = async (backLabel = "返回存档选择") => {
const choices = config.characters.filter(name => lib.character[name]).map(name => ({ const choices = config.characters.filter(name => lib.character[name]).map(name => ({
id: name, id: name,
name: get.translation(name), name: get.translation(name),
description: lib.translate[`${name}_title`] || "作为本次征程的角色", description: lib.translate[`${name}_title`] || "作为本次征程的角色",
})); }));
return chooseButtons("选择出征武将", [...(choices.length ? choices : [{ id: "mengsan_liubei_shuying", name: "界刘备" }]), { id: "back", name: "返回存档选择", description: "不会修改现有征程" }], "此武将将陪伴你完成本次征程。", { back: "back", eyebrow: "出征准备" }); return chooseButtons("选择出征武将", [...(choices.length ? choices : [{ id: "mengsan_liubei_shuying", name: "界刘备" }]), { id: "back", name: backLabel, description: "不会修改现有征程" }], "此武将将陪伴你完成本次征程。", { back: "back", eyebrow: "出征准备" });
};
const chooseDefeatAction = async () => {
while (true) {
const action = await chooseButtons("战斗失败", [
{id:"retry", name:"重新尝试", description:"恢复战前生命、牌组和征程状态,重新挑战当前战斗。"},
{id:"new", name:"新开征程", danger:true, description:"选择出征武将,确认选择后替换本次征程。"},
], "本次失败不会发放战斗奖励。请选择接下来要做什么。", {eyebrow:"败而不馁"});
if (action === "retry") return "retry";
if (action !== "new") throw new Error("未知的失败流程选择");
const character = await chooseCharacter("返回失败选择");
if (character !== "back") return {action:"new", character};
}
}; };
const createCardInstance = (run, name = "sha", upgrade = 0, affixes = []) => const createCardInstance = (run, name = "sha", upgrade = 0, affixes = []) =>
@@ -288,7 +335,35 @@ const setBattleCamp = (player, camp) => {
player.classList.add("mengsan-hide-identity-shuying"); player.classList.add("mengsan-hide-identity-shuying");
}; };
const initBattleUnit = (player, spec) => { const initBattleUnit = (player, spec) => {
const monster = spec.camp === "enemy" ? createMonster(spec) : null; // 只在实际入场且未指定生命时,用征程随机源抽取各怪物的普通生命范围;资料列表不消耗随机数。
const monsterSpec = spec.camp === "enemy" && spec.hp == null ?
(spec.character === CRAWLER_CHARACTER ?
{ ...spec, hp: rollCrawlerHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === JAXFRUIT_CHARACTER ?
{ ...spec, hp: rollJaxfruitHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === STRANGLER_CHARACTER ?
{ ...spec, hp: rollStranglerHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === LEAFSLIME_CHARACTER ?
{ ...spec, hp: rollLeafslimeHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === LEAFMEDIUM_CHARACTER ?
{ ...spec, hp: rollLeafslimeMediumHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === TWIGSLIME_CHARACTER ?
{ ...spec, hp: rollTwigslimeHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === TWIGMEDIUM_CHARACTER ?
{ ...spec, hp: rollTwigmediumHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === SHRINKER_CHARACTER ?
{ ...spec, hp: rollShrinkerHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === PHROG_CHARACTER ?
{ ...spec, hp: rollPhrogHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === WRIGGLER_CHARACTER ?
{ ...spec, hp: rollWrigglerHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === BYRDONIS_CHARACTER ?
{ ...spec, hp: rollByrdonisHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === NIBBIT_CHARACTER ?
{ ...spec, hp: rollNibbitHp(() => nextRandom(_status.mengsanRun_shuying)) } :
spec.character === INKLET_CHARACTER ?
{ ...spec, hp: rollInkletHp(() => nextRandom(_status.mengsanRun_shuying)) } : spec) : spec;
const monster = spec.camp === "enemy" ? createMonster(monsterSpec) : null;
player.storage.mengsanUnitId_shuying = spec.id; player.storage.mengsanUnitId_shuying = spec.id;
setBattleCamp(player, spec.camp); setBattleCamp(player, spec.camp);
player.init(spec.character, null, spec.inheritSkills !== false); player.init(spec.character, null, spec.inheritSkills !== false);
@@ -301,6 +376,19 @@ const initBattleUnit = (player, spec) => {
} }
if (spec.maxHp != null || monster) player.maxHp = spec.maxHp ?? monster.hp; if (spec.maxHp != null || monster) player.maxHp = spec.maxHp ?? monster.hp;
player.hp = Math.min(spec.hp ?? monster?.hp ?? player.hp, player.maxHp); player.hp = Math.min(spec.hp ?? monster?.hp ?? player.hp, player.maxHp);
initializeCubex(player, activeBattle);
initializeByrdonis(player, activeBattle);
initializeEffigy(player, activeBattle);
initializePhrog(player, spec, activeBattle);
if (isInklet(player)) {
const position = spec.inkletPosition ?? 1;
if (!Number.isInteger(position) || position < 1 || position > 7) throw new RangeError("墨宝站位必须为1~7");
player.storage.mengsanInkletState_shuying = { position, stage: "opening", turnsTaken: 0 };
player.storage.mengsanInkletIntent_shuying = null;
player.storage.mengsanSlippery_shuying = 1;
player.addSkill("mengsan_slippery_shuying");
player.markSkill("mengsan_slippery_shuying");
}
player.storage.mengsanInitialHand_shuying = spec.hand ?? 4; player.storage.mengsanInitialHand_shuying = spec.hand ?? 4;
for (const skill of spec.skills || []) player.addSkill(skill); for (const skill of spec.skills || []) player.addSkill(skill);
player.update(); player.update();
@@ -315,7 +403,7 @@ const installMonsterPile = (player, monster, current) => {
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, {
game, ui, get, _status, document, MutationObserver, shuffle: shuffleBattlePile, game, ui, get, lib, _status, document, MutationObserver, shuffle: shuffleBattlePile,
strict: false, refresh() {}, strict: false, refresh() {},
})); }));
}; };
@@ -324,6 +412,23 @@ const {
planEnemyIntent, planEnemyIntent,
executeFlyconidIntent, executeFlyconidIntent,
executeRaiderIntent, executeRaiderIntent,
executeFogmogIntent,
executeMawlerIntent,
executeVineIntent,
executeCubexIntent,
executeByrdonisIntent,
executeEffigyIntent,
executePhrogIntent,
executeNibbitIntent,
executeShrinkerIntent,
executeTwigmediumIntent,
executeTwigslimeIntent,
executeLeafslimeMediumIntent,
executeLeafslimeIntent,
executeStranglerIntent,
executeJaxfruitIntent,
executeCrawlerIntent,
executeInkletIntent,
} = createMonsterIntentActions(game, () => activeBattle); } = createMonsterIntentActions(game, () => activeBattle);
const { planBondIntent, executeBondIntent } = const { planBondIntent, executeBondIntent } =
createBondIntentActions(game, () => activeBattle); createBondIntentActions(game, () => activeBattle);
@@ -333,41 +438,43 @@ const equipBattleUnit = async (player, spec, resources) => {
await player.equip(card); await player.equip(card);
} }
}; };
const spawnBattleUnit = async (current, spec, anchor) => {
const player = game.addPlayer(anchor ? Number(anchor.dataset.position) + 1 : game.players.length + game.dead.length);
registerPlayers(current.session, [player]);
current.resources.bindPlayers([player]);
const join = game.createEvent("mengsanJoinBattle", false);
join.player = player;
join.setContent(async () => {
const monster = initBattleUnit(player, spec);
current.fogmog?.register(player, spec);
installMonsterPile(player, monster, current);
if (monster) {
current.energyUI.set(player,
mountEnergy(player, current.session, document));
current.intentUI.set(player, mountEnemyIntent(player,
current.session, STYLE_PATH, document,
() => getIntentHostiles(game, player)));
}
for (const participant of [...game.players, ...game.dead]) participant.setSeatNum(Number(participant.dataset.position) + 1);
await equipBattleUnit(player, spec, current.resources);
await player.draw(spec.hand ?? 4);
await game.triggerEnter(player);
if (monster && current.session.active) {
planEnemyIntent(player, _status.mengsanRun_shuying);
planBondIntent(player);
}
});
await join;
game.log(player, "作为", spec.camp === "ally" ? "友方支援" : "敌方援军", "加入战斗");
return player;
};
const createScenario = (plan, current) => { const createScenario = (plan, current) => {
const director = createBattleDirector(plan, { const director = createBattleDirector(plan, {
active: () => current.session.active, active: () => current.session.active,
state: player => ({ hp: player.hp, hand: player.countCards("h"), alive: player.isAlive(), state: player => ({ hp: player.hp, hand: player.countCards("h"), alive: player.isAlive(),
camp: player.storage.mengsanCamp_shuying, linked: player.isLinked(), turnedOver: player.isTurnedOver() }), camp: player.storage.mengsanCamp_shuying, linked: player.isLinked(), turnedOver: player.isTurnedOver() }),
dialogue: lines => playDialogue(lines, { run: _status.mengsanRun_shuying, title: "关卡剧情" }), dialogue: lines => playDialogue(lines, { run: _status.mengsanRun_shuying, title: "关卡剧情" }),
async spawn(spec, anchor) { spawn: (spec, anchor) => spawnBattleUnit(current, spec, anchor),
const player = game.addPlayer(anchor ? Number(anchor.dataset.position) + 1 : game.players.length + game.dead.length);
registerPlayers(current.session, [player]);
current.resources.bindPlayers([player]);
const join = game.createEvent("mengsanJoinBattle", false);
join.player = player;
join.setContent(async () => {
const monster = initBattleUnit(player, spec);
installMonsterPile(player, monster, current);
if (monster) {
current.energyUI.set(player,
mountEnergy(player, current.session, document));
current.intentUI.set(player, mountEnemyIntent(player,
current.session, STYLE_PATH, document,
() => getIntentHostiles(game, player)));
}
for (const participant of [...game.players, ...game.dead]) participant.setSeatNum(Number(participant.dataset.position) + 1);
await equipBattleUnit(player, spec, current.resources);
await player.draw(spec.hand ?? 4);
await game.triggerEnter(player);
if (monster && current.session.active) {
planEnemyIntent(player, _status.mengsanRun_shuying);
planBondIntent(player);
}
});
await join;
game.log(player, "作为", spec.camp === "ally" ? "友方支援" : "敌方援军", "加入战斗");
return player;
},
async effect(player, effect) { async effect(player, effect) {
if (effect.type === "draw") await player.draw(effect.amount); if (effect.type === "draw") await player.draw(effect.amount);
else if (effect.type === "recover") await player.recover(effect.amount); else if (effect.type === "recover") await player.recover(effect.amount);
@@ -389,8 +496,13 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
// Old saves keep their deck instances, but the retired affix no longer exists. // 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"); 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 bondUnit = prepareBondBattle(run, () => nextRandom(run)); const bondUnit = prepareBondBattle(run, () => nextRandom(run), node);
const plan = buildBattlePlan(encounter, run, bondUnit); const plan = buildBattlePlan(encounter, run, bondUnit);
const scheduledUnits = [...plan.units, ...plan.rules.flatMap(rule =>
rule.effects.filter(effect => effect.type === "spawn").map(effect => effect.unit))];
const fogmogCount = scheduledUnits.filter(spec => spec.character === "mengsan_fogmog_shuying").length;
const phrogCount = scheduledUnits.filter(spec => spec.character === PHROG_CHARACTER).length;
if (scheduledUnits.length + fogmogCount + phrogCount * INFESTED_COUNT > 7) throw new Error("召唤需要预留空席位;请减少本场支援数量");
if (run.bondBattle) { if (run.bondBattle) {
game.log(`羁绊助战:${bondDefinitions[run.bondBattle.id].name},${ game.log(`羁绊助战:${bondDefinitions[run.bondBattle.id].name},${
bondUnit ? "本场将到场" : "本场未到场"}`); bondUnit ? "本场将到场" : "本场未到场"}`);
@@ -429,12 +541,16 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
registerPlayers(session, game.players.slice()); registerPlayers(session, game.players.slice());
resources.bindPlayers(game.players); resources.bindPlayers(game.players);
const currentBattle = activeBattle; const currentBattle = activeBattle;
currentBattle.phrog = createPhrogBattle(game, currentBattle,
(spec, anchor) => spawnBattleUnit(currentBattle, spec, anchor));
currentBattle.fogmog = createFogmogBattle(game, currentBattle,
(spec, anchor) => spawnBattleUnit(currentBattle, spec, anchor));
let teardown; let teardown;
activeBattle.releaseSkills = () => (teardown ||= createPlayerTeardown([...currentBattle.players], {lib,game,get,_status}))(); activeBattle.releaseSkills = () => (teardown ||= createPlayerTeardown([...currentBattle.players], {lib,game,get,_status}))();
const me = game.me; const me = game.me;
activeBattle.personalPiles = installPersonalPiles(session, me, _status.mengsanBattle_shuying, run, resources, { activeBattle.personalPiles = installPersonalPiles(session, me, _status.mengsanBattle_shuying, run, resources, {
game, ui, get, _status, document, MutationObserver, shuffle: shuffleBattlePile, game, ui, get, lib, _status, document, MutationObserver, shuffle: shuffleBattlePile,
refresh: () => activeBattle?.session === session && activeBattle.pilesUI?.refresh(), refresh: () => activeBattle?.session === session && activeBattle.pilesUI?.refresh(),
}); });
activeBattle.personalPiles.withOwner(() => me.init(run.player.character)); activeBattle.personalPiles.withOwner(() => me.init(run.player.character));
@@ -442,6 +558,7 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
setBattleCamp(me, "ally"); setBattleCamp(me, "ally");
const participants = game.players.filter(player => player !== me); const participants = game.players.filter(player => player !== me);
const monsters = plan.units.map((spec, index) => initBattleUnit(participants[index], spec)); const monsters = plan.units.map((spec, index) => initBattleUnit(participants[index], spec));
bindNibbitOpenings(participants);
plan.units.forEach((spec, index) => installMonsterPile(participants[index], monsters[index], currentBattle)); plan.units.forEach((spec, index) => installMonsterPile(participants[index], monsters[index], currentBattle));
participants.forEach((player, index) => { participants.forEach((player, index) => {
if (monsters[index]) currentBattle.intentUI.set(player, if (monsters[index]) currentBattle.intentUI.set(player,
@@ -462,7 +579,8 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
me.storage.mengsanEnergy_shuying = PLAYER_ENERGY; me.storage.mengsanEnergy_shuying = PLAYER_ENERGY;
currentBattle.energyUI.set(me, mountEnergy(me, session, document)); currentBattle.energyUI.set(me, mountEnergy(me, session, document));
currentBattle.handUI = await mountHandUI(me, session, { currentBattle.handUI = await mountHandUI(me, session, {
game, ui, document, window, cardCost, game, ui, get, lib, _status, document, window, cardCost,
canPayCard, isActiveCardUse,
styleURL: `${STYLE_PATH}/ui/hand/style.css`, styleURL: `${STYLE_PATH}/ui/hand/style.css`,
log: message => game.log(message), log: message => game.log(message),
}); });
@@ -494,6 +612,16 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
"mengsan_card_affixes_shuying", "mengsan_scenario_shuying", "mengsan_card_affixes_shuying", "mengsan_scenario_shuying",
"mengsan_card_payment_shuying", "mengsan_monster_draw_shuying", "mengsan_card_payment_shuying", "mengsan_monster_draw_shuying",
"mengsan_flyconid_action_shuying", "mengsan_raider_action_shuying", "mengsan_flyconid_action_shuying", "mengsan_raider_action_shuying",
"mengsan_fogmog_action_shuying", "mengsan_mawler_action_shuying",
"mengsan_phrog_action_shuying", "mengsan_wriggler_spawned_shuying", "mengsan_infection_damage_shuying", "mengsan_effigy_action_shuying", "mengsan_byrdonis_action_shuying", "mengsan_cubex_action_shuying", "mengsan_vine_action_shuying", "mengsan_nibbit_action_shuying",
"mengsan_shrinker_action_shuying",
"mengsan_twigmedium_action_shuying",
"mengsan_twigslime_action_shuying",
"mengsan_leafmedium_action_shuying",
"mengsan_leafslime_action_shuying",
"mengsan_strangler_action_shuying",
"mengsan_jaxfruit_action_shuying",
"mengsan_crawler_action_shuying", "mengsan_inklet_action_shuying",
"mengsan_bond_action_shuying", "mengsan_bond_action_shuying",
"mengsan_raider_card_strength_shuying", "mengsan_raider_card_strength_shuying",
"mengsan_death_blow_finish_shuying", "mengsan_death_blow_finish_shuying",
@@ -543,7 +671,9 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
player.storage.mengsanEnergy_shuying = player.storage.mengsanMaxEnergy_shuying; player.storage.mengsanEnergy_shuying = player.storage.mengsanMaxEnergy_shuying;
currentBattle.energyUI.get(player)?.(); currentBattle.energyUI.get(player)?.();
} }
if (player === me) currentBattle.fogmog.beforeRound();
await currentBattle.director.beforeTurn(player); await currentBattle.director.beforeTurn(player);
if (isFogmogActor(player) || isPhrogActor(player)) planEnemyIntent(player, run);
if (player === me && session.active) { if (player === me && session.active) {
for (const participant of game.players) { for (const participant of game.players) {
planEnemyIntent(participant, run); planEnemyIntent(participant, run);
@@ -585,12 +715,12 @@ const makeFlow = (releaseSkills = async () => {}) => createBattleFlow({
{ run: pending.base, title: "战后剧情" }); { run: pending.base, title: "战后剧情" });
}, },
settlement:createBattleSettlement({store:modeStorage, config, settlement:createBattleSettlement({store:modeStorage, config,
getRandomRewardChoices, applyReward, completeNode, enterNextAct, getRandomRewardChoices, applyReward, completeNode, enterNextAct, createRun,
logRewardPackage: (pack, selection) => { logRewardPackage: (pack, selection) => {
const card = pack.upgradeChoices.find(card => const card = pack.upgradeChoices.find(card =>
card.id === selection.cardId); card.id === selection.cardId);
game.log(`梦三:过关奖励 ${pack.gold} 金币,` + game.log(`梦三:过关奖励 ${pack.gold} 金币,` +
(card ? `强化【${get.translation(card.name)}】` : "无可强化牌")); (card ? `强化【${get.translation(card.name)}】` : "未强化卡牌"));
}, },
}), }),
chooseReward: (choices, options) => (options.rewardPackage chooseReward: (choices, options) => (options.rewardPackage
@@ -600,6 +730,7 @@ const makeFlow = (releaseSkills = async () => {}) => createBattleFlow({
...config.rewards[id], id, ...config.rewards[id], id,
})), })),
}), }),
chooseDefeat:chooseDefeatAction,
showMap, showMap,
showEnding:async route => { game.over(route === "victory"); }, showEnding:async route => { game.over(route === "victory"); },
quiesce:() => quiesceEngine(_status), quiesce:() => quiesceEngine(_status),
@@ -638,7 +769,7 @@ const awaitSettlement = async (flow, initial = flow.wait()) => {
} }
} }
}; };
const setupBattle = async (run, node, encounter = getNodeEncounter(run, node)) => { const playBattle = async (run, node, encounter) => {
// Wait for engine boot callbacks BEFORE arena creation; cardsAsync then builds synchronously. // Wait for engine boot callbacks BEFORE arena creation; cardsAsync then builds synchronously.
if (lib.onfree) await new Promise(resolve => lib.onfree.push(resolve)); if (lib.onfree) await new Promise(resolve => lib.onfree.push(resolve));
await quiesceEngine(_status); await quiesceEngine(_status);
@@ -670,7 +801,23 @@ const setupBattle = async (run, node, encounter = getNodeEncounter(run, node)) =
} }
const result = await awaitSettlement(flow); const result = await awaitSettlement(flow);
if (activeBattle === current) activeBattle = null; if (activeBattle === current) activeBattle = null;
if (result.route === "map") await routeRun(result.run, result.nextNode, true); return result;
};
const setupBattle = async (run, node, encounter = getNodeEncounter(run, node)) => {
// Retry iteratively: repeated defeats must not grow a recursive battle promise chain.
while (true) {
const result = await playBattle(run, node, encounter);
if (!result) return;
if (result.route === "retry") {
run = result.run;
node = result.retryNode;
encounter = result.retryEncounter || getNodeEncounter(run, node);
continue;
}
if (result.route === "map") await routeRun(result.run, result.nextNode, true);
else if (result.route === "new") await routeRun(result.run);
return;
}
}; };
const routeRun = async (run, selected = null, supplied = false) => { const routeRun = async (run, selected = null, supplied = false) => {
while (true) { while (true) {
@@ -711,6 +858,14 @@ const startJourney = async () => {
const flow = makeFlow(); const flow = makeFlow();
const result = await awaitSettlement(flow, flow.resumePending({session:createBattleSession(`recovery-${++battleSequence}`)})); const result = await awaitSettlement(flow, flow.resumePending({session:createBattleSession(`recovery-${++battleSequence}`)}));
if (result.route === "map") await routeRun(result.run, result.nextNode, true); if (result.route === "map") await routeRun(result.run, result.nextNode, true);
else if (result.route === "retry") await setupBattle(result.run, result.retryNode,
result.retryEncounter || getNodeEncounter(result.run, result.retryNode));
else if (result.route === "new") await routeRun(result.run);
return;
}
if (run.battleFlow?.retry) {
const retry = run.battleFlow.retry;
await setupBattle(run, retry.node, retry.encounter || getNodeEncounter(run, retry.node));
return; return;
} }
const canRefreshOldMap = config.characters.includes(run.player.character) && !run.map?.completedNodeIds?.length && run.map?.layoutVersion != config.mapLayoutVersion; const canRefreshOldMap = config.characters.includes(run.player.character) && !run.map?.completedNodeIds?.length && run.map?.layoutVersion != config.mapLayoutVersion;
@@ -725,7 +880,20 @@ const createMode = identityMode => {
const identityElement = identityMode.element || {}; const identityElement = identityMode.element || {};
const identityPlayer = identityElement.player || {}; const identityPlayer = identityElement.player || {};
const nativeChangeHujia = identityPlayer.changeHujia || lib.element.Player.prototype.changeHujia; const nativeChangeHujia = identityPlayer.changeHujia || lib.element.Player.prototype.changeHujia;
const modeCards = createMengsanCards(); const nativeChangeHp = identityPlayer.changeHp || lib.element.Player.prototype.changeHp;
const modeCards = createMengsanCards({
copyToDiscard: createBattleCardCopier(game, () => activeBattle),
reclaimFromDiscard: createBattleCardReclaimer(game, () => activeBattle, get),
temporaryStrength: createTemporaryStrength(game, () => activeBattle),
countStrikeCards: createStrikeCounter(game, () => activeBattle, lib),
blockDraw: createBlockDrawer(game, () => activeBattle),
playDrawTop: createHavocPlayer(game, () => activeBattle, _status),
handExhaust: createHandExhauster(game, () => activeBattle, get,
createRandomHandExhauster(game, () => activeBattle, () => _status.mengsanRun_shuying, nextRandom)),
battleEnergy: createBattleEnergy(game, () => activeBattle),
randomHandExhaust: createRandomHandExhauster(game, () => activeBattle, () => _status.mengsanRun_shuying, nextRandom),
isBattleActive: () => Boolean(activeBattle?.session.active),
});
return { return {
...identityMode, ...identityMode,
name: MODE_ID, name: MODE_ID,
@@ -742,7 +910,11 @@ const createMode = identityMode => {
...identityElement, ...identityElement,
player: { player: {
...identityPlayer, ...identityPlayer,
dieAfter() { async dieAfter() {
clearConstrictSource(activeBattle, this);
clearShrinkSource(activeBattle, this);
activeBattle?.fogmog?.onDeath(this);
if (activeBattle?.phrog) await activeBattle.phrog.onDeath(this);
recordBondDeath(this, _status.mengsanRun_shuying, recordBondDeath(this, _status.mengsanRun_shuying,
message => game.log(message)); message => game.log(message));
game.checkResult(); game.checkResult();
@@ -752,6 +924,11 @@ const createMode = identityMode => {
isEnemyOf(player) { return Boolean(player && this.storage.mengsanCamp_shuying !== player.storage?.mengsanCamp_shuying); }, isEnemyOf(player) { return Boolean(player && this.storage.mengsanCamp_shuying !== player.storage?.mengsanCamp_shuying); },
getEnemies(filter, includeDie) { return game[includeDie ? "filterPlayer2" : "filterPlayer"](p => this.isEnemyOf(p) && (!filter || filter(p))); }, getEnemies(filter, includeDie) { return game[includeDie ? "filterPlayer2" : "filterPlayer"](p => this.isEnemyOf(p) && (!filter || filter(p))); },
getFriends(filter, includeDie) { const self = filter === true; return game[includeDie ? "filterPlayer2" : "filterPlayer"](p => (p !== this || self) && this.isFriendOf(p) && (typeof filter !== "function" || filter(p))); }, getFriends(filter, includeDie) { const self = filter === true; return game[includeDie ? "filterPlayer2" : "filterPlayer"](p => (p !== this || self) && this.isFriendOf(p) && (typeof filter !== "function" || filter(p))); },
changeHp(num, popup) {
const next = nativeChangeHp.call(this, num, popup);
if (activeBattle?.session.active) capInkletHpLoss(this, next);
return next;
},
changeHujia(num, type) { changeHujia(num, type) {
// 脆弱只削减正向获得的护甲,不改变受伤时消耗护甲的数值。 // 脆弱只削减正向获得的护甲,不改变受伤时消耗护甲的数值。
if (this.storage?.mengsanFrail_shuying > 0 && (num == null || num > 0) && type !== "damage") { if (this.storage?.mengsanFrail_shuying > 0 && (num == null || num > 0) && type !== "damage") {
@@ -796,9 +973,43 @@ const createMode = identityMode => {
const { resume = "advance", recover } = options; const { resume = "advance", recover } = options;
const flyconid = isFlyconid(player); const flyconid = isFlyconid(player);
const raider = isRaider(player); const raider = isRaider(player);
const fogmog = isFogmogActor(player);
const mawler = isMawler(player);
const vine = isVine(player);
const cubex = isCubex(player);
const byrdonis = isByrdonis(player);
const effigy = isEffigy(player);
const phrog = isPhrogActor(player);
const nibbit = isNibbit(player);
const shrinker = isShrinker(player);
const twigmedium = isTwigmedium(player);
const twigslime = isTwigslime(player);
const leafmedium = isLeafslimeMedium(player);
const leafslime = isLeafslime(player);
const strangler = isStrangler(player);
const jaxfruit = isJaxfruit(player);
const crawler = isCrawler(player);
const inklet = isInklet(player);
const intent = options.intent ?? (flyconid ? const intent = options.intent ?? (flyconid ?
player.storage.mengsanFlyconidIntent_shuying : raider ? player.storage.mengsanFlyconidIntent_shuying : raider ?
player.storage.mengsanRaiderIntent_shuying : null); player.storage.mengsanRaiderIntent_shuying : fogmog ?
player.storage.mengsanFogmogIntent_shuying : mawler ?
player.storage.mengsanMawlerIntent_shuying : crawler ?
player.storage.mengsanCrawlerIntent_shuying : inklet ?
player.storage.mengsanInkletIntent_shuying : jaxfruit ?
player.storage.mengsanJaxfruitIntent_shuying : strangler ?
player.storage.mengsanStranglerIntent_shuying : leafslime ?
player.storage.mengsanLeafslimeIntent_shuying : leafmedium ?
player.storage.mengsanLeafslimeMediumIntent_shuying : twigslime ?
player.storage.mengsanTwigslimeIntent_shuying : twigmedium ?
player.storage.mengsanTwigmediumIntent_shuying : shrinker ?
player.storage.mengsanShrinkerIntent_shuying : vine ?
player.storage.mengsanVineIntent_shuying : nibbit ?
player.storage.mengsanNibbitIntent_shuying : cubex ?
player.storage.mengsanCubexIntent_shuying : byrdonis ?
player.storage.mengsanByrdonisIntent_shuying : effigy ?
player.storage.mengsanEffigyIntent_shuying : phrog ?
player.storage.mengsanPhrogIntent_shuying : null);
const restore = recover || (flyconid ? const restore = recover || (flyconid ?
(choice, original) => { (choice, original) => {
const state = const state =
@@ -813,6 +1024,108 @@ const createMode = identityMode => {
const next = choice === "retry" ? original : null; const next = choice === "retry" ? original : null;
player.storage.mengsanRaiderIntent_shuying = next; player.storage.mengsanRaiderIntent_shuying = next;
return next; return next;
} : fogmog ? (choice, original) => {
if (isFogmog(player)) player.storage.mengsanFogmogState_shuying =
recordFogmogAction(player.storage.mengsanFogmogState_shuying || {}, null, false);
const next = choice === "retry" ? original : null;
player.storage.mengsanFogmogIntent_shuying = next;
return next;
} : mawler ? (choice, original) => {
player.storage.mengsanMawlerState_shuying =
recordMawlerAction(player.storage.mengsanMawlerState_shuying || {}, null, false);
const next = choice === "retry" ? original : null;
player.storage.mengsanMawlerIntent_shuying = next;
return next;
} : crawler ? (choice, original) => {
player.storage.mengsanCrawlerState_shuying =
recordCrawlerAction(player.storage.mengsanCrawlerState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanCrawlerIntent_shuying = next;
return next;
} : inklet ? (choice, original) => {
player.storage.mengsanInkletState_shuying =
recordInkletAction(player.storage.mengsanInkletState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanInkletIntent_shuying = next;
return next;
} : jaxfruit ? (choice, original) => {
player.storage.mengsanJaxfruitState_shuying =
recordJaxfruitAction(player.storage.mengsanJaxfruitState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanJaxfruitIntent_shuying = next;
return next;
} : strangler ? (choice, original) => {
player.storage.mengsanStranglerState_shuying =
recordStranglerAction(player.storage.mengsanStranglerState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanStranglerIntent_shuying = next;
return next;
} : leafslime ? (choice, original) => {
player.storage.mengsanLeafslimeState_shuying =
recordLeafslimeAction(player.storage.mengsanLeafslimeState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanLeafslimeIntent_shuying = next;
return next;
} : leafmedium ? (choice, original) => {
player.storage.mengsanLeafslimeMediumState_shuying =
recordLeafslimeMediumAction(player.storage.mengsanLeafslimeMediumState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanLeafslimeMediumIntent_shuying = next;
return next;
} : twigslime ? (choice, original) => {
player.storage.mengsanTwigslimeState_shuying =
recordTwigslimeAction(player.storage.mengsanTwigslimeState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanTwigslimeIntent_shuying = next;
return next;
} : twigmedium ? (choice, original) => {
player.storage.mengsanTwigmediumState_shuying =
recordTwigmediumAction(player.storage.mengsanTwigmediumState_shuying || {}, original, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanTwigmediumIntent_shuying = next;
return next;
} : shrinker ? (choice, original) => {
player.storage.mengsanShrinkerState_shuying =
recordShrinkerAction(player.storage.mengsanShrinkerState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanShrinkerIntent_shuying = next;
return next;
} : vine ? (choice, original) => {
player.storage.mengsanVineState_shuying =
recordVineAction(player.storage.mengsanVineState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanVineIntent_shuying = next;
return next;
} : nibbit ? (choice, original) => {
player.storage.mengsanNibbitState_shuying =
recordNibbitAction(player.storage.mengsanNibbitState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanNibbitIntent_shuying = next;
return next;
} : cubex ? (choice, original) => {
player.storage.mengsanCubexState_shuying =
recordCubexAction(player.storage.mengsanCubexState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanCubexIntent_shuying = next;
return next;
} : byrdonis ? (choice, original) => {
player.storage.mengsanByrdonisState_shuying =
recordByrdonisAction(player.storage.mengsanByrdonisState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanByrdonisIntent_shuying = next;
return next;
} : phrog ? (choice, original) => {
player.storage.mengsanPhrogState_shuying =
recordPhrogAction(player.storage.mengsanPhrogState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanPhrogIntent_shuying = next;
return next;
} : effigy ? (choice, original) => {
player.storage.mengsanEffigyState_shuying =
recordEffigyAction(player.storage.mengsanEffigyState_shuying || {}, choice !== "retry");
const next = choice === "retry" ? original : null;
player.storage.mengsanEffigyIntent_shuying = next;
return next;
} : null); } : null);
const onRecover = async (choice, original) => { const onRecover = async (choice, original) => {
const next = await restore(choice, original); const next = await restore(choice, original);
@@ -858,7 +1171,7 @@ const createMode = identityMode => {
}, },
checkResult() { checkResult() {
const current = activeBattle; const current = activeBattle;
if (!current?.session.active || !current.director?.ready || current.director.busy || current.resultCheckQueued) return; if (!current?.session.active || !current.director?.ready || current.director.busy || current.phrog?.pending || current.resultCheckQueued) return;
current.resultCheckQueued = true; current.resultCheckQueued = true;
const next = game.createEvent("mengsanScenarioResult", false); const next = game.createEvent("mengsanScenarioResult", false);
next.setContent(async () => { next.setContent(async () => {
@@ -866,7 +1179,7 @@ const createMode = identityMode => {
await current.director.evaluate("state"); await current.director.evaluate("state");
if (!current.session.active) return; if (!current.session.active) return;
if (!game.me || game.me.isDead()) game.mengsanFailRun_shuying("角色死亡,征程结束"); if (!game.me || game.me.isDead()) game.mengsanFailRun_shuying("角色死亡,征程结束");
else if (!game.players.some(p => p.storage.mengsanCamp_shuying === "enemy" && p.isAlive()) && !current.director.pendingEnemies) game.mengsanFinishBattle_shuying(); else if (!game.players.some(isEncounterEnemy) && !current.director.pendingEnemies && !current.phrog?.pending) game.mengsanFinishBattle_shuying();
} finally { current.resultCheckQueued = false; } } finally { current.resultCheckQueued = false; }
}); });
}, },
@@ -891,7 +1204,7 @@ const createMode = identityMode => {
lib.card[event.card?.name]?.mengsanCost_shuying != null && isActiveCardUse(event, event.player); }, lib.card[event.card?.name]?.mengsanCost_shuying != null && isActiveCardUse(event, event.player); },
async content(event, trigger) { async content(event, trigger) {
if (trigger.mengsanPaid_shuying) return; if (trigger.mengsanPaid_shuying) return;
if (!payCard(trigger.player, trigger.card)) { if (!payCard(trigger.player, trigger.card, trigger)) {
trigger.cancel(); trigger.cancel();
game.log(trigger.player, "费用不足,不能使用", trigger.card); game.log(trigger.player, "费用不足,不能使用", trigger.card);
return; return;
@@ -908,7 +1221,7 @@ const createMode = identityMode => {
player.storage?.mengsanBond_shuying); player.storage?.mengsanBond_shuying);
}, },
async content(event, trigger, player) { async content(event, trigger, player) {
trigger.num = isStunned(player) ? 0 : trigger.num = isStunned(player) || isToothedEye(player) ? 0 :
(player.storage.mengsanMonster_shuying || (player.storage.mengsanMonster_shuying ||
player.storage.mengsanBond_shuying).draw; player.storage.mengsanBond_shuying).draw;
}, },
@@ -929,6 +1242,162 @@ const createMode = identityMode => {
event.player.storage.mengsanRaiderIntent_shuying); }, event.player.storage.mengsanRaiderIntent_shuying); },
async content(event, trigger) { await executeRaiderIntent(trigger.player); }, async content(event, trigger) { await executeRaiderIntent(trigger.player); },
}, },
mengsan_inklet_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isInklet(event.player) &&
event.player.storage.mengsanInkletIntent_shuying); },
async content(event, trigger) { await executeInkletIntent(trigger.player); },
},
mengsan_slippery_shuying: {
mark: true, marktext: "滑",
onremove(player) { delete player.storage.mengsanSlippery_shuying; },
intro: { content(storage, player) { return `滑溜${player.storage.mengsanSlippery_shuying || 0}:下一次实际生命损失限制为1,完全格挡不消耗`; } },
trigger: { player: "changeHp" },
forced: true, silent: true, popup: false,
filter(event, player) { return Boolean(activeBattle?.session.active && isInklet(player) &&
player.storage.mengsanSlippery_shuying > 0 && event.num < 0 && event.changedHp < 0); },
async content(event, trigger, player) {
player.storage.mengsanSlippery_shuying = Math.max(0, player.storage.mengsanSlippery_shuying - 1);
if (player.storage.mengsanSlippery_shuying) player.markSkill("mengsan_slippery_shuying");
else player.removeSkill("mengsan_slippery_shuying");
},
},
mengsan_crawler_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isCrawler(event.player) &&
event.player.storage.mengsanCrawlerIntent_shuying); },
async content(event, trigger) { await executeCrawlerIntent(trigger.player); },
},
mengsan_jaxfruit_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isJaxfruit(event.player) &&
event.player.storage.mengsanJaxfruitIntent_shuying); },
async content(event, trigger) { await executeJaxfruitIntent(trigger.player); },
},
mengsan_strangler_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isStrangler(event.player) &&
event.player.storage.mengsanStranglerIntent_shuying); },
async content(event, trigger) { await executeStranglerIntent(trigger.player); },
},
mengsan_leafslime_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isLeafslime(event.player) &&
event.player.storage.mengsanLeafslimeIntent_shuying); },
async content(event, trigger) { await executeLeafslimeIntent(trigger.player); },
},
mengsan_leafmedium_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isLeafslimeMedium(event.player) &&
event.player.storage.mengsanLeafslimeMediumIntent_shuying); },
async content(event, trigger) { await executeLeafslimeMediumIntent(trigger.player); },
},
mengsan_twigslime_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isTwigslime(event.player) &&
event.player.storage.mengsanTwigslimeIntent_shuying); },
async content(event, trigger) { await executeTwigslimeIntent(trigger.player); },
},
mengsan_twigmedium_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isTwigmedium(event.player) &&
event.player.storage.mengsanTwigmediumIntent_shuying); },
async content(event, trigger) { await executeTwigmediumIntent(trigger.player); },
},
mengsan_shrinker_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isShrinker(event.player) &&
event.player.storage.mengsanShrinkerIntent_shuying); },
async content(event, trigger) { await executeShrinkerIntent(trigger.player); },
},
mengsan_vine_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isVine(event.player) &&
event.player.storage.mengsanVineIntent_shuying); },
async content(event, trigger) { await executeVineIntent(trigger.player); },
},
mengsan_cubex_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isCubex(event.player) &&
event.player.storage.mengsanCubexIntent_shuying); },
async content(event, trigger) { await executeCubexIntent(trigger.player); },
},
mengsan_byrdonis_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isByrdonis(event.player) &&
event.player.storage.mengsanByrdonisIntent_shuying); },
async content(event, trigger) { await executeByrdonisIntent(trigger.player); },
},
mengsan_phrog_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && !event.player?.storage?.mengsanPhrogState_shuying?.spawned && isPhrogActor(event.player) &&
event.player.storage.mengsanPhrogIntent_shuying); },
async content(event, trigger) { await executePhrogIntent(trigger.player); },
},
mengsan_effigy_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isEffigy(event.player) &&
event.player.storage.mengsanEffigyIntent_shuying); },
async content(event, trigger) { await executeEffigyIntent(trigger.player, trigger); },
},
mengsan_nibbit_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isNibbit(event.player) &&
event.player.storage.mengsanNibbitIntent_shuying); },
async content(event, trigger) { await executeNibbitIntent(trigger.player); },
},
mengsan_mawler_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
event.player?.isAlive() && !isStunned(event.player) && isMawler(event.player) &&
event.player.storage.mengsanMawlerIntent_shuying); },
async content(event, trigger) { await executeMawlerIntent(trigger.player); },
},
mengsan_fogmog_action_shuying: {
trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100,
filter(event) { return Boolean(activeBattle?.session.active &&
!isStunned(event.player) && isFogmogActor(event.player)); },
async content(event, trigger) {
await executeFogmogIntent(trigger.player);
if (isToothedEye(trigger.player)) trigger.cancel();
},
},
mengsan_fogmog_illusion_shuying: {
mark: true, marktext: "幻",
intro: { content: "爪牙:雾菇存活时,死亡后的下一轮以6点生命复活;不单独阻止战斗胜利。" },
},
mengsan_bond_action_shuying: { mengsan_bond_action_shuying: {
trigger: { global: "phaseUseBefore" }, trigger: { global: "phaseUseBefore" },
forced: true, silent: true, popup: false, priority: 100, forced: true, silent: true, popup: false, priority: 100,
@@ -941,13 +1410,36 @@ const createMode = identityMode => {
await executeBondIntent(trigger.player); await executeBondIntent(trigger.player);
}, },
}, },
mengsan_setup_strength_shuying: {
trigger: { source: "damageBegin1", global: ["phaseAfter", "dieAfter"] },
forced: true, silent: true, popup: false, priority: 85, forceDie: true, forceOut: true,
mark: true, marktext: "力",
intro: { content(storage, player) { return `本回合攻击伤害增加${player.storage.mengsanSetupStrength_shuying || 0}点`; } },
onremove(player) { forgetTemporaryStrength(player); },
filter(event, player) {
if (event.name === "damage") {
return Boolean(temporaryStrengthAmount(player, activeBattle) > 0 &&
(event.card || event.mengsanAttack_shuying) &&
!event.mengsanScriptedSkill_shuying && !event.mengsanSetupStrengthApplied_shuying);
}
return event.name === "die" ? event.player === player &&
Boolean(player.storage.mengsanSetupStrength_shuying) : temporaryStrengthExpires(player, event);
},
async content(event, trigger, player) {
if (event.triggername === "damageBegin1") {
trigger.num += temporaryStrengthAmount(player, activeBattle);
trigger.mengsanSetupStrengthApplied_shuying = true;
} else clearTemporaryStrength(player);
},
},
mengsan_raider_card_strength_shuying: { mengsan_raider_card_strength_shuying: {
trigger: { source: "damageBegin1" }, trigger: { source: "damageBegin1" },
forced: true, silent: true, popup: false, priority: 90, forced: true, silent: true, popup: false, priority: 90,
filter(event, player) { return Boolean(activeBattle?.session.active && filter(event, player) { return Boolean(activeBattle?.session.active &&
(isRaider(player) || player.storage.mengsanBond_shuying) && (isRaider(player) || isFogmog(player) || isCrawler(player) || isInklet(player) || isJaxfruit(player) || isStrangler(player) || isLeafslime(player) || isLeafslimeMedium(player) || isTwigslime(player) || isTwigmedium(player) || isShrinker(player) || isVine(player) || isNibbit(player) || isCubex(player) || isByrdonis(player) || isEffigy(player) || isPhrogActor(player) || player.storage.mengsanBond_shuying) &&
player.storage.mengsanStrength_shuying > 0 && player.storage.mengsanStrength_shuying > 0 &&
event.card && !event.mengsanScriptedSkill_shuying); }, event.card && !event.mengsanScriptedSkill_shuying &&
!event.mengsanCardStrengthApplied_shuying); },
async content(event, trigger, player) { async content(event, trigger, player) {
trigger.num += player.storage.mengsanStrength_shuying; trigger.num += player.storage.mengsanStrength_shuying;
}, },
@@ -1011,6 +1503,78 @@ const createMode = identityMode => {
else player.removeSkill("mengsan_frail_shuying"); else player.removeSkill("mengsan_frail_shuying");
}, },
}, },
mengsan_constrict_shuying: {
mark: true, marktext: "缠",
intro: { content(storage, player) { return `紧缠${player.storage.mengsanConstrict_shuying || 0}层:自身回合结束时受到等量非攻击伤害,来源死亡解除。`; } },
onremove(player) { delete player.storage.mengsanConstrict_shuying; activeBattle?.stranglerConstrict?.delete(player); },
trigger: { player: "phaseJieshuBegin" },
forced: true, silent: true, popup: false,
filter(event, player) { return constrictTotal(activeBattle, player) > 0; },
async content(event, trigger, player) { await resolveConstrict(activeBattle, player); },
},
mengsan_slow_shuying: {
mark: true, marktext: "缓",
intro: {
markcount(storage, player) { return slowPercent(player); },
content(storage, player) { return `缓慢${player.storage.mengsanSlow_shuying || 0}层:本回合攻击伤害增加${slowPercent(player)}%。友方每使用一张牌增加10%,换回合清零。`; },
},
onremove(player) { delete player.storage.mengsanSlow_shuying; delete player.storage.mengsanSlowCards_shuying; },
trigger: { player: "damageBegin3", global: ["useCardAfter", "phaseBefore", "phaseAfter"] },
forced: true, silent: true, popup: false, priority: 20,
filter(event, player) {
if (!activeBattle?.session.active || !isEffigy(player) || !player.isAlive() || !player.storage.mengsanSlow_shuying) return false;
if (event.name === "damage") return slowApplies(player, event);
if (event.name === "useCard") return canCountSlowCard(player, activeBattle, event, _status.currentPhase);
return event.name === "phase";
},
async content(event, trigger, player) {
if (event.triggername === "damageBegin3") {
trigger.num = slowAttackDamage(trigger.num, player);
trigger.mengsanSlowApplied_shuying = true;
} else if (event.triggername === "useCardAfter") recordSlowCard(player, activeBattle, trigger, _status.currentPhase);
else resetSlow(player);
},
},
mengsan_territorial_shuying: {
mark: true, marktext: "领",
intro: { content(storage, player) { return `多尼斯异鸟的领地意识${player.storage.mengsanTerritorial_shuying || 0}层:自身回合结束时获得等量力量。`; } },
onremove(player) { delete player.storage.mengsanTerritorial_shuying; },
trigger: { player: "phaseAfter" },
forced: true, silent: true, popup: false,
filter(event, player) { return Boolean(activeBattle?.session.active && isByrdonis(player) &&
event.player === player && player.isAlive() && player.storage.mengsanTerritorial_shuying > 0); },
async content(event, trigger, player) { resolveTerritorial(player, activeBattle); },
},
mengsan_artifact_shuying: {
mark: true, marktext: "制",
intro: { content(storage, player) { return `人工制品${player.storage.mengsanArtifact_shuying || 0}层:每层抵消一次梦三负面状态施加,不抵消伤害。`; } },
onremove(player) { delete player.storage.mengsanArtifact_shuying; },
},
mengsan_tangled_shuying: {
mark: true, marktext: "藤",
intro: { content(storage, player) { return `剩余${player.storage.mengsanTangled_shuying || 0}回合:攻击牌费用增加1(持续回合可叠加,加费不叠加)。`; } },
onremove(player) { delete player.storage.mengsanTangled_shuying; activeBattle?.tangledTargets?.delete(player); if (activeBattle?.session.active) activeBattle.handUI?.refresh(); },
trigger: { player: "phaseAfter" },
forced: true, silent: true, popup: false,
filter(event, player) { return Boolean(activeBattle?.session.active && player.storage.mengsanTangled_shuying > 0); },
async content(event, trigger, player) { advanceTangled(activeBattle, player); },
},
mengsan_shrink_shuying: {
mark: true, marktext: "缩",
intro: { content: "攻击伤害减少30%(向下取整),不重复叠加;来源死亡解除。" },
onremove(player) { delete player.storage.mengsanShrink_shuying; activeBattle?.shrinkerSources?.delete(player); },
trigger: { source: "damageBegin1" },
forced: true, silent: true, popup: false, priority: 70,
filter(event, player) {
return Boolean(activeBattle?.session.active && !event.mengsanScriptedSkill_shuying &&
!event.mengsanShrinkApplied_shuying && (event.card || event.mengsanAttack_shuying) &&
isShrunk(activeBattle, player));
},
async content(event, trigger, player) {
trigger.num = shrinkAttackDamage(trigger.num, player);
trigger.mengsanShrinkApplied_shuying = true;
},
},
mengsan_vulnerable_shuying: { mengsan_vulnerable_shuying: {
mark: true, marktext: "易", mark: true, marktext: "易",
onremove(player) { delete player.storage.mengsanVulnerable_shuying; }, onremove(player) { delete player.storage.mengsanVulnerable_shuying; },
@@ -1071,6 +1635,30 @@ const createMode = identityMode => {
}); });
}, },
}, },
mengsan_infested_shuying: {
mark: true, marktext: "寄",
intro: { markcount(storage, player) { return player.storage.mengsanInfested_shuying || 0; },
content: "死亡后立即召唤4只扭动虫,扭动虫出场的首回合跳过;须消灭扭动虫才能获胜。" },
onremove(player) { delete player.storage.mengsanInfested_shuying; },
},
mengsan_wriggler_spawned_shuying: {
trigger: { global: "phaseBefore" },
forced: true, silent: true, popup: false, priority: 1000,
filter(event) { return Boolean(activeBattle?.session.active && isWriggler(event.player) && event.player.storage.mengsanPhrogState_shuying?.spawned); },
async content(event, trigger) {
if (skipSpawnedWriggler(trigger.player, trigger)) {
clearStun(trigger.player);
game.mengsanSetEnemyIntent_shuying(trigger.player, null);
game.log(trigger.player, "生成眩晕,跳过首回合");
}
},
},
mengsan_infection_damage_shuying: {
trigger: { global: "phaseDiscardBegin" },
forced: true, silent: true, popup: false, priority: 200,
filter(event) { return Boolean(activeBattle?.session.active && event.player?.isAlive() && infectionHand(event.player).length); },
async content(event, trigger) { await resolveInfection(activeBattle, trigger.player, trigger); },
},
mengsan_card_affixes_shuying: { mengsan_card_affixes_shuying: {
forced: true, silent: true, popup: false, priority: 100, forced: true, silent: true, popup: false, priority: 100,
trigger: { global: ["phaseDiscardBegin", "cardsDiscardAfter"] }, trigger: { global: ["phaseDiscardBegin", "cardsDiscardAfter"] },
@@ -1128,15 +1716,52 @@ const createMode = identityMode => {
mengsan_yingyong_shuying_info: "锁定技,每回合限一次,你于自己的回合内使用牌造成的伤害+1。", mengsan_yingyong_shuying_info: "锁定技,每回合限一次,你于自己的回合内使用牌造成的伤害+1。",
mengsan_flyconid_action_shuying: "孢子行动", mengsan_flyconid_action_shuying: "孢子行动",
mengsan_raider_action_shuying: "劫掠行动", mengsan_raider_action_shuying: "劫掠行动",
mengsan_fogmog_action_shuying: "雾菇行动",
mengsan_mawler_action_shuying: "蛮兽行动",
mengsan_vine_action_shuying: "藤蔓蹒跚者行动",
mengsan_nibbit_action_shuying: "小啃兽行动",
mengsan_cubex_action_shuying: "立柱构造体行动",
mengsan_byrdonis_action_shuying: "多尼斯异鸟行动",
mengsan_effigy_action_shuying: "旧日雕像行动",
mengsan_phrog_action_shuying: "异蛙寄生行动",
mengsan_wriggler_spawned_shuying: "生成眩晕",
mengsan_infection_damage_shuying: "感染结算",
mengsan_infested_shuying: "寄生物",
mengsan_slow_shuying: "缓慢",
mengsan_slow_shuying_info: "本回合友方每使用一张牌,旧日雕像受到的攻击伤害增加10%,回合结束清零。",
mengsan_territorial_shuying: "领地意识",
mengsan_territorial_shuying_info: "自身回合结束时,每层领地意识获得1点力量。",
mengsan_shrinker_action_shuying: "缩小甲虫行动",
mengsan_twigmedium_action_shuying: "树枝史莱姆(中)行动",
mengsan_twigslime_action_shuying: "树枝史莱姆(小)行动",
mengsan_leafmedium_action_shuying: "树叶史莱姆(中)行动",
mengsan_leafslime_action_shuying: "树叶史莱姆(小)行动",
mengsan_strangler_action_shuying: "蛇行扼杀者行动",
mengsan_jaxfruit_action_shuying: "闪光贾克斯果行动",
mengsan_crawler_action_shuying: "毛绒伏地虫行动",
mengsan_inklet_action_shuying: "墨宝行动",
mengsan_slippery_shuying: "滑溜",
mengsan_slippery_shuying_info: "下一次实际失去生命时只失去1点生命;完全格挡不消耗。",
mengsan_fogmog_illusion_shuying: "幻象·爪牙",
mengsan_weak_shuying: "虚弱", mengsan_weak_shuying: "虚弱",
mengsan_weak_shuying_info: mengsan_weak_shuying_info:
"攻击伤害减少25%(向下取整),自身回合结束减少1层。", "攻击伤害减少25%(向下取整),自身回合结束减少1层。",
mengsan_relic_attack_shuying: "遗物攻击修正", mengsan_relic_attack_shuying: "遗物攻击修正",
mengsan_setup_strength_shuying: "临时力量",
mengsan_setup_strength_shuying_info: "本回合内增加攻击伤害,回合结束时移除。",
mengsan_raider_card_strength_shuying: "力量加成", mengsan_raider_card_strength_shuying: "力量加成",
mengsan_raider_strength_shuying: "力量", mengsan_raider_strength_shuying: "力量",
mengsan_raider_strength_shuying_info: "攻击造成的伤害按力量层数增加。", mengsan_raider_strength_shuying_info: "攻击造成的伤害按力量层数增加。",
mengsan_frail_shuying: "脆弱", mengsan_frail_shuying: "脆弱",
mengsan_frail_shuying_info: "接下来相应回合获得护甲减少25%(向下取整)。", mengsan_frail_shuying_info: "接下来相应回合获得护甲减少25%(向下取整)。",
mengsan_constrict_shuying: "紧缠",
mengsan_constrict_shuying_info: "自身回合结束受到层数对应的非攻击伤害;可叠加,来源死亡解除。",
mengsan_artifact_shuying: "人工制品",
mengsan_artifact_shuying_info: "每层抵消一次梦三负面状态施加,不抵消伤害、正面增益或既有状态的自然结算。",
mengsan_tangled_shuying: "缠结",
mengsan_tangled_shuying_info: "攻击牌费用增加1,持续对应回合,自身回合结束减少1回合;不影响响应。",
mengsan_shrink_shuying: "缩小",
mengsan_shrink_shuying_info: "攻击伤害减少30%(向下取整),不重复叠加;来源死亡解除。",
mengsan_vulnerable_shuying: "易伤", mengsan_vulnerable_shuying: "易伤",
mengsan_vulnerable_shuying_info: "接下来相应回合受到的攻击伤害增加50%(向上取整)。", mengsan_vulnerable_shuying_info: "接下来相应回合受到的攻击伤害增加50%(向上取整)。",
}, },
+445
View File
@@ -1,14 +1,37 @@
import { addConstrict } from "./strangler-constrict.js";
import { applyShrink } from "./shrinker-status.js";
import { getIntentHostiles } from "../battle/intent-effects.js";
import { applyLeafslimeGoop } from "./leafslime-goop.js";
import { createIntentExecutor } from "../battle/intent-effects.js"; import { createIntentExecutor } from "../battle/intent-effects.js";
export { getIntentHostiles } from "../battle/intent-effects.js"; export { getIntentHostiles } from "../battle/intent-effects.js";
import { isStunned } from "../battle/stun-intent.js"; import { isStunned } from "../battle/stun-intent.js";
import { nextRandom } from "../progression/state.js"; import { nextRandom } from "../progression/state.js";
import { selectFlyconidMove, recordFlyconidAction } from "./flyconid-intent.js"; import { selectFlyconidMove, recordFlyconidAction } from "./flyconid-intent.js";
import { isMawler, selectMawlerMove, recordMawlerAction } from "./mawler-intent.js";
import { isVine, selectVineMove, recordVineAction } from "./vine-intent.js";
import { isCubex, selectCubexMove, recordCubexAction } from "./cubex-intent.js";
import { isByrdonis, selectByrdonisMove, recordByrdonisAction } from "./byrdonis-intent.js";
import { isEffigy, selectEffigyMove, recordEffigyAction } from "./effigy-intent.js";
import { isPhrog, isPhrogActor, selectPhrogMove, selectWrigglerMove, recordPhrogAction } from "./phrog-intent.js";
import { applyInfection } from "../cards/infection-card.js";
import { isNibbit, selectNibbitMove, recordNibbitAction, bindNibbitOpenings } from "./nibbit-intent.js";
import { isShrinker, selectShrinkerMove, recordShrinkerAction } from "./shrinker-intent.js";
import { isTwigmedium, selectTwigmediumMove, recordTwigmediumAction } from "./twigmedium-intent.js";
import { isTwigslime, selectTwigslimeMove, recordTwigslimeAction } from "./twigslime-intent.js";
import { isLeafslimeMedium, selectLeafslimeMediumMove, recordLeafslimeMediumAction } from "./leafmedium-intent.js";
import { isLeafslime, selectLeafslimeMove, recordLeafslimeAction } from "./leafslime-intent.js";
import { isStrangler, selectStranglerMove, recordStranglerAction } from "./strangler-intent.js";
import { isJaxfruit, selectJaxfruitMove, recordJaxfruitAction } from "./jaxfruit-intent.js";
import { isCrawler, selectCrawlerMove, recordCrawlerAction } from "./crawler-intent.js";
import { isInklet, selectInkletMove, recordInkletAction } from "./inklet-intent.js";
import { import {
isRaiderCharacter, isRaiderCharacter,
selectRaiderMove, selectRaiderMove,
recordRaiderAction, recordRaiderAction,
} from "./raider-intent.js"; } from "./raider-intent.js";
import { isFogmog, isFogmogActor, selectFogmogMove, recordFogmogAction, EYE_INTENT } from "./fogmog-intent.js";
export const isFlyconid = player => export const isFlyconid = player =>
player?.name === "mengsan_flyconid_shuying" && player?.name === "mengsan_flyconid_shuying" &&
player.storage?.mengsanCamp_shuying === "enemy"; player.storage?.mengsanCamp_shuying === "enemy";
@@ -28,6 +51,94 @@ export function createMonsterIntentActions(game, getActiveBattle) {
() => nextRandom(run)); () => nextRandom(run));
player.storage.mengsanFlyconidIntent_shuying = move; player.storage.mengsanFlyconidIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move); game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isInklet(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanInkletIntent_shuying) return;
const move = selectInkletMove(player.storage.mengsanInkletState_shuying || {}, () => nextRandom(run));
player.storage.mengsanInkletIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isCrawler(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanCrawlerIntent_shuying) return;
const move = selectCrawlerMove(player.storage.mengsanCrawlerState_shuying || {});
player.storage.mengsanCrawlerIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isJaxfruit(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanJaxfruitIntent_shuying) return;
const move = selectJaxfruitMove(player.storage.mengsanJaxfruitState_shuying || {});
player.storage.mengsanJaxfruitIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isStrangler(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanStranglerIntent_shuying) return;
const move = selectStranglerMove(player.storage.mengsanStranglerState_shuying || {}, () => nextRandom(run));
player.storage.mengsanStranglerIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isLeafslime(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanLeafslimeIntent_shuying) return;
const move = selectLeafslimeMove(player.storage.mengsanLeafslimeState_shuying || {}, () => nextRandom(run));
player.storage.mengsanLeafslimeIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isLeafslimeMedium(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanLeafslimeMediumIntent_shuying) return;
const move = selectLeafslimeMediumMove(player.storage.mengsanLeafslimeMediumState_shuying || {});
player.storage.mengsanLeafslimeMediumIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isTwigslime(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanTwigslimeIntent_shuying) return;
const move = selectTwigslimeMove(player.storage.mengsanTwigslimeState_shuying || {});
player.storage.mengsanTwigslimeIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isTwigmedium(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanTwigmediumIntent_shuying) return;
const move = selectTwigmediumMove(player.storage.mengsanTwigmediumState_shuying || {}, () => nextRandom(run));
player.storage.mengsanTwigmediumIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isShrinker(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanShrinkerIntent_shuying) return;
const move = selectShrinkerMove(player.storage.mengsanShrinkerState_shuying || {});
player.storage.mengsanShrinkerIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isNibbit(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanNibbitIntent_shuying) return;
bindNibbitOpenings(game.players);
const move = selectNibbitMove(player.storage.mengsanNibbitState_shuying || {});
player.storage.mengsanNibbitIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isEffigy(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanEffigyIntent_shuying) return;
const move = selectEffigyMove(player.storage.mengsanEffigyState_shuying || {});
player.storage.mengsanEffigyIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isPhrogActor(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanPhrogIntent_shuying) return;
const move = (isPhrog(player) ? selectPhrogMove : selectWrigglerMove)(player.storage.mengsanPhrogState_shuying || {});
player.storage.mengsanPhrogIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isByrdonis(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanByrdonisIntent_shuying) return;
const move = selectByrdonisMove(player.storage.mengsanByrdonisState_shuying || {});
player.storage.mengsanByrdonisIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isCubex(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanCubexIntent_shuying) return;
const move = selectCubexMove(player.storage.mengsanCubexState_shuying || {});
player.storage.mengsanCubexIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isVine(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanVineIntent_shuying) return;
const move = selectVineMove(player.storage.mengsanVineState_shuying || {});
player.storage.mengsanVineIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isMawler(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanMawlerIntent_shuying) return;
const move = selectMawlerMove(player.storage.mengsanMawlerState_shuying || {},
() => nextRandom(run));
player.storage.mengsanMawlerIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isFogmogActor(player)) {
if (player.storage.mengsanFogmogIntent_shuying || !getActiveBattle()?.fogmog?.canAct(player)) return;
const move = isFogmog(player) ? selectFogmogMove(
player.storage.mengsanFogmogState_shuying || {}, () => nextRandom(run)) : EYE_INTENT;
player.storage.mengsanFogmogIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isRaider(player)) { } else if (isRaider(player)) {
if (player.storage.mengsanRaiderIntent_shuying) return; if (player.storage.mengsanRaiderIntent_shuying) return;
const move = selectRaiderMove(player.name, const move = selectRaiderMove(player.name,
@@ -74,7 +185,341 @@ export function createMonsterIntentActions(game, getActiveBattle) {
await executeEffects(player, move); await executeEffects(player, move);
}; };
const executeInkletIntent = async player => {
const current = getActiveBattle();
if (!isInklet(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanInkletIntent_shuying;
if (!move) return;
player.storage.mengsanInkletIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanInkletState_shuying = recordInkletAction(player.storage.mengsanInkletState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeCrawlerIntent = async player => {
const current = getActiveBattle();
if (!isCrawler(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanCrawlerIntent_shuying;
if (!move) return;
player.storage.mengsanCrawlerIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanCrawlerState_shuying =
recordCrawlerAction(player.storage.mengsanCrawlerState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeJaxfruitIntent = async player => {
const current = getActiveBattle();
if (!isJaxfruit(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanJaxfruitIntent_shuying;
if (!move) return;
player.storage.mengsanJaxfruitIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanJaxfruitState_shuying =
recordJaxfruitAction(player.storage.mengsanJaxfruitState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeStranglerIntent = async player => {
const current = getActiveBattle();
if (!isStrangler(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanStranglerIntent_shuying;
if (!move) return;
player.storage.mengsanStranglerIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanStranglerState_shuying =
recordStranglerAction(player.storage.mengsanStranglerState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.constrict) {
for (const target of getIntentHostiles(game, player)) addConstrict(current, player, target, move.constrict);
} else await executeEffects(player, move);
};
const executeLeafslimeIntent = async player => {
const current = getActiveBattle();
if (!isLeafslime(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanLeafslimeIntent_shuying;
if (!move) return;
player.storage.mengsanLeafslimeIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanLeafslimeState_shuying =
recordLeafslimeAction(player.storage.mengsanLeafslimeState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.slimed) applyLeafslimeGoop(game, current, player);
else await executeEffects(player, move);
};
const executeLeafslimeMediumIntent = async player => {
const current = getActiveBattle();
if (!isLeafslimeMedium(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanLeafslimeMediumIntent_shuying;
if (!move) return;
player.storage.mengsanLeafslimeMediumIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanLeafslimeMediumState_shuying =
recordLeafslimeMediumAction(player.storage.mengsanLeafslimeMediumState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.slimed) applyLeafslimeGoop(game, current, player, move.slimed);
else await executeEffects(player, move);
};
const executeTwigslimeIntent = async player => {
const current = getActiveBattle();
if (!isTwigslime(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanTwigslimeIntent_shuying;
if (!move) return;
player.storage.mengsanTwigslimeIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanTwigslimeState_shuying =
recordTwigslimeAction(player.storage.mengsanTwigslimeState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeTwigmediumIntent = async player => {
const current = getActiveBattle();
if (!isTwigmedium(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanTwigmediumIntent_shuying;
if (!move) return;
player.storage.mengsanTwigmediumIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanTwigmediumState_shuying =
recordTwigmediumAction(player.storage.mengsanTwigmediumState_shuying || {}, move);
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.slimed) applyLeafslimeGoop(game, current, player, move.slimed);
else await executeEffects(player, move);
};
const executeShrinkerIntent = async player => {
const current = getActiveBattle();
if (!isShrinker(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanShrinkerIntent_shuying;
if (!move) return;
player.storage.mengsanShrinkerIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanShrinkerState_shuying =
recordShrinkerAction(player.storage.mengsanShrinkerState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.shrink) {
for (const target of getIntentHostiles(game, player)) applyShrink(current, player, target);
} else await executeEffects(player, move);
};
const executeNibbitIntent = async player => {
const current = getActiveBattle();
if (!isNibbit(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanNibbitIntent_shuying;
if (!move) return;
player.storage.mengsanNibbitIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanNibbitState_shuying =
recordNibbitAction(player.storage.mengsanNibbitState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeEffigyIntent = async (player, phase) => {
const current = getActiveBattle();
if (!isEffigy(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanEffigyIntent_shuying;
if (!move) return;
player.storage.mengsanEffigyIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanEffigyState_shuying =
recordEffigyAction(player.storage.mengsanEffigyState_shuying || {});
if (move.sleep) {
phase?.cancel();
game.log(player, "沉睡,本回合不行动"); return;
}
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executePhrogIntent = async player => {
const current = getActiveBattle();
if (!isPhrogActor(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanPhrogIntent_shuying;
if (!move || move.id === "spawned") return;
player.storage.mengsanPhrogIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanPhrogState_shuying =
recordPhrogAction(player.storage.mengsanPhrogState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.infection) applyInfection(game, current, player, move.infection);
await executeEffects(player, move);
};
const executeByrdonisIntent = async player => {
const current = getActiveBattle();
if (!isByrdonis(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanByrdonisIntent_shuying;
if (!move) return;
player.storage.mengsanByrdonisIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanByrdonisState_shuying =
recordByrdonisAction(player.storage.mengsanByrdonisState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeCubexIntent = async player => {
const current = getActiveBattle();
if (!isCubex(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanCubexIntent_shuying;
if (!move) return;
player.storage.mengsanCubexIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanCubexState_shuying =
recordCubexAction(player.storage.mengsanCubexState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeVineIntent = async player => {
const current = getActiveBattle();
if (!isVine(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanVineIntent_shuying;
if (!move) return;
player.storage.mengsanVineIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanVineState_shuying =
recordVineAction(player.storage.mengsanVineState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeMawlerIntent = async player => {
const current = getActiveBattle();
if (!isMawler(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanMawlerIntent_shuying;
if (!move) return;
player.storage.mengsanMawlerIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
const paid = player.storage.mengsanEnergy_shuying >= 1;
player.storage.mengsanMawlerState_shuying =
recordMawlerAction(player.storage.mengsanMawlerState_shuying || {}, move, paid);
if (!paid) { game.log(player, "费用不足,未发动", move.name); return; }
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeFogmogIntent = async player => {
const current = getActiveBattle();
if (!isFogmogActor(player) || isStunned(player) || !current?.fogmog?.canAct(player)) return;
const move = player.storage.mengsanFogmogIntent_shuying;
if (!move) return;
player.storage.mengsanFogmogIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
const paid = player.storage.mengsanEnergy_shuying >= 1;
if (isFogmog(player)) player.storage.mengsanFogmogState_shuying =
recordFogmogAction(player.storage.mengsanFogmogState_shuying || {}, move, paid);
if (!paid) { game.log(player, "费用不足,未发动", move.name); return; }
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.summon) await current.fogmog.summon(player);
else if (move.dazed) current.fogmog.distract(player);
else await executeEffects(player, move);
};
return { return {
executeFogmogIntent,
executeMawlerIntent,
executeVineIntent,
executeCubexIntent,
executeByrdonisIntent,
executeEffigyIntent,
executePhrogIntent,
executeNibbitIntent,
executeShrinkerIntent,
executeTwigmediumIntent,
executeTwigslimeIntent,
executeLeafslimeMediumIntent,
executeLeafslimeIntent,
executeStranglerIntent,
executeJaxfruitIntent,
executeCrawlerIntent,
executeInkletIntent,
planEnemyIntent, planEnemyIntent,
executeFlyconidIntent, executeFlyconidIntent,
executeRaiderIntent, executeRaiderIntent,
+39
View File
@@ -0,0 +1,39 @@
const KEY = "mengsanArtifact_shuying", SKILL = "mengsan_artifact_shuying";
const bindings = new WeakMap();
const DEBUFFS = Object.freeze({
vulnerable: ["mengsanVulnerable_shuying", "mengsan_vulnerable_shuying"],
weak: ["mengsanWeak_shuying", "mengsan_weak_shuying"],
frail: ["mengsanFrail_shuying", "mengsan_frail_shuying"],
});
export const clearArtifact = player => { delete player.storage[KEY]; player.removeSkill(SKILL); };
export function setArtifact(player, layers, current = null) {
if (!Number.isSafeInteger(layers) || layers < 0) throw new RangeError("人工制品层数无效");
if (!layers) { clearArtifact(player); return; }
if (current?.session.active && bindings.get(player)?.battle !== current) {
const entry = { battle: current };
current.session.ownResource(entry, () => {
if (bindings.get(player) !== entry) return;
bindings.delete(player); clearArtifact(player);
});
bindings.set(player, entry);
}
player.storage[KEY] = layers; player.addSkill(SKILL); player.markSkill(SKILL);
}
export function consumeArtifact(player) {
if (!player?.isAlive() || !player.storage) return false;
const layers = player.storage[KEY] ?? 0;
if (!Number.isSafeInteger(layers) || layers < 0) throw new RangeError("人工制品层数无效");
if (!layers) return false;
if (layers === 1) clearArtifact(player);
else { player.storage[KEY] = layers - 1; player.markSkill(SKILL); }
return true;
}
// 一次施加抵消一次,不按负面层数逐层消耗;伤害和正面增益不走此入口。
export function applyMengsanDebuff(player, kind, amount) {
if (!Object.hasOwn(DEBUFFS, kind) || !Number.isSafeInteger(amount) || amount < 0) throw new RangeError("负面状态参数无效");
if (!amount || !player?.isAlive()) return false;
const [key, skill] = DEBUFFS[kind], old = player.storage[key] ?? 0;
if (!Number.isSafeInteger(old) || old < 0 || amount > Number.MAX_SAFE_INTEGER - old) throw new RangeError("负面状态层数越界");
if (consumeArtifact(player)) return false;
player.storage[key] = old + amount; player.addSkill(skill); player.markSkill(skill); return true;
}
+57
View File
@@ -0,0 +1,57 @@
// 普通数值;网页SVG:开始→俯冲→啄击→俯冲。领地意识在回合结束结算。
export const BYRDONIS_CHARACTER = "mengsan_byrdonis_shuying";
export const BYRDONIS_MOVES = Object.freeze({
swoop: Object.freeze({ id: "swoop", name: "俯冲", damage: 17 }),
peck: Object.freeze({ id: "peck", name: "啄击", damage: 3, hits: 3 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("多尼斯异鸟行动记录无效");
return turns;
};
export const selectByrdonisMove = (state = {}) => turnsOf(state) % 2 ? BYRDONIS_MOVES.peck : BYRDONIS_MOVES.swoop;
export function recordByrdonisAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("多尼斯异鸟行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export function rollByrdonisHp(random) {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("多尼斯异鸟随机数必须在[0,1)内");
return 81 + Math.floor(value * 4);
}
export const isByrdonis = player => player?.name === BYRDONIS_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
const bindings = new WeakMap();
export function initializeByrdonis(player, current = null) {
if (!isByrdonis(player)) return false;
if (current?.session.active && bindings.get(player)?.battle !== current) {
const entry = { battle: current };
current.session.ownResource(entry, () => {
if (bindings.get(player) !== entry) return;
bindings.delete(player);
delete player.storage.mengsanByrdonisState_shuying;
delete player.storage.mengsanByrdonisIntent_shuying;
player.removeSkill("mengsan_territorial_shuying");
player.removeSkill("mengsan_raider_strength_shuying");
});
bindings.set(player, entry);
}
player.storage.mengsanByrdonisState_shuying = { turnsTaken: 0 };
player.storage.mengsanByrdonisIntent_shuying = null;
player.storage.mengsanTerritorial_shuying = 1;
player.addSkill("mengsan_territorial_shuying");
player.markSkill("mengsan_territorial_shuying");
return true;
}
export function resolveTerritorial(player, current) {
if (!isByrdonis(player) || !player.isAlive() || !current?.session.active) return false;
const layers = player.storage.mengsanTerritorial_shuying ?? 0;
const strength = player.storage.mengsanStrength_shuying ?? 0;
if (!Number.isSafeInteger(layers) || layers < 0 || !Number.isSafeInteger(strength) ||
!Number.isSafeInteger(strength + layers)) throw new RangeError("领地意识或力量数值无效");
if (!layers) return false;
player.storage.mengsanStrength_shuying = strength + layers;
player.addSkill("mengsan_raider_strength_shuying");
player.markSkill("mengsan_raider_strength_shuying");
return true;
}
+32
View File
@@ -0,0 +1,32 @@
// 毛绒伏地虫:酸液 -> 吸入 -> 酸液 -> 回到开头,不是两招交替。
export const CRAWLER_CHARACTER = "mengsan_fuzzy_wurm_crawler_shuying";
export const CRAWLER_MOVES = Object.freeze({
acid: Object.freeze({ id: "acid", name: "酸液黏球", damage: 4 }),
inhale: Object.freeze({ id: "inhale", name: "吸入", strength: 7 }),
});
export const CRAWLER_CYCLE = Object.freeze([
CRAWLER_MOVES.acid, CRAWLER_MOVES.inhale, CRAWLER_MOVES.acid,
]);
const cycleIndex = state => {
const index = state.cycleIndex ?? 0;
if (!Number.isInteger(index) || index < 0 || index >= CRAWLER_CYCLE.length) {
throw new RangeError("毛绒伏地虫循环位置无效");
}
return index;
};
export const selectCrawlerMove = (state = {}) => CRAWLER_CYCLE[cycleIndex(state)];
export function recordCrawlerAction(state = {}, advance = true) {
const index = cycleIndex(state), turn = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turn) || turn < 0 || turn === Number.MAX_SAFE_INTEGER || typeof advance !== "boolean") {
throw new RangeError("毛绒伏地虫行动记录无效");
}
return { turnsTaken: turn + 1,
cycleIndex: (index + (advance ? 1 : 0)) % CRAWLER_CYCLE.length };
}
export function rollCrawlerHp(random) {
const roll = random();
if (!Number.isFinite(roll) || roll < 0 || roll >= 1) throw new RangeError("怪物随机数必须在 [0,1) 内");
return 55 + Math.floor(roll * 3);
}
export const isCrawler = player => player?.name === CRAWLER_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
+43
View File
@@ -0,0 +1,43 @@
import { setArtifact } from "./artifact-status.js";
// SVG明确:蓄能只在开场一次,排出回到重复轰击,不回到蓄能。
export const CUBEX_CHARACTER = "mengsan_cubex_construct_shuying";
export const CUBEX_MOVES = Object.freeze({
charge: Object.freeze({ id: "charge", name: "蓄能", strength: 2 }),
repeater: Object.freeze({ id: "repeater", name: "重复轰击", damage: 7, strength: 2 }),
expel: Object.freeze({ id: "expel", name: "排出", damage: 5, hits: 2 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("立柱构造体行动记录无效");
return turns;
};
const CYCLE = Object.freeze([CUBEX_MOVES.repeater, CUBEX_MOVES.repeater, CUBEX_MOVES.expel]);
export const selectCubexMove = (state = {}) => {
const turns = turnsOf(state);
return turns === 0 ? CUBEX_MOVES.charge : CYCLE[(turns - 1) % CYCLE.length];
};
export function recordCubexAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("立柱构造体行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export const isCubex = player => player?.name === CUBEX_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
const bindings = new WeakMap();
export function initializeCubex(player, current = null) {
if (!isCubex(player)) return false;
if (current?.session.active && bindings.get(player)?.battle !== current) {
const entry = { battle: current };
current.session.ownResource(entry, () => {
if (bindings.get(player) !== entry) return;
bindings.delete(player);
delete player.storage.mengsanCubexState_shuying;
delete player.storage.mengsanCubexIntent_shuying;
});
bindings.set(player, entry);
}
player.hujia = 13;
player.storage.mengsanCubexState_shuying = { turnsTaken: 0 };
player.storage.mengsanCubexIntent_shuying = null;
setArtifact(player, 1, current);
return true;
}
+29
View File
@@ -0,0 +1,29 @@
import { initializeSlow } from "./effigy-slow.js";
export const EFFIGY_CHARACTER = "mengsan_bygone_effigy_shuying";
export const EFFIGY_MOVES = Object.freeze({
sleep: Object.freeze({ id: "sleep", name: "沉睡", sleep: true }),
wake: Object.freeze({ id: "wake", name: "苏醒", strength: 10 }),
slashes: Object.freeze({ id: "slashes", name: "斩击", damage: 13 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("旧日雕像行动记录无效");
return turns;
};
export const selectEffigyMove = (state = {}) => {
const turns = turnsOf(state);
return turns === 0 ? EFFIGY_MOVES.sleep : turns === 1 ? EFFIGY_MOVES.wake : EFFIGY_MOVES.slashes;
};
export function recordEffigyAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("旧日雕像行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export const isEffigy = player => player?.name === EFFIGY_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
export function initializeEffigy(player, current = null) {
if (!isEffigy(player)) return false;
initializeSlow(player, current);
player.storage.mengsanEffigyState_shuying = { turnsTaken: 0 };
player.storage.mengsanEffigyIntent_shuying = null;
return true;
}
+62
View File
@@ -0,0 +1,62 @@
import { isAttackCard } from "./vine-tangled.js";
const bindings = new WeakMap();
const layersOf = player => {
const value = player?.storage?.mengsanSlow_shuying ?? 0;
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("缓慢层数无效");
return value;
};
export function slowPercent(player) {
const count = player?.storage?.mengsanSlowCards_shuying ?? 0;
const percent = count * layersOf(player) * 10;
if (!Number.isSafeInteger(count) || count < 0 || !Number.isSafeInteger(percent)) throw new RangeError("缓慢计数溢出");
return percent;
}
export function initializeSlow(player, current) {
if (current?.session.active && bindings.get(player)?.battle !== current) {
const entry = { battle: current, seen: new WeakSet() };
current.session.ownResource(entry, () => {
if (bindings.get(player) !== entry) return;
bindings.delete(player);
delete player.storage.mengsanEffigyState_shuying;
delete player.storage.mengsanEffigyIntent_shuying;
player.removeSkill("mengsan_slow_shuying");
player.removeSkill("mengsan_raider_strength_shuying");
});
bindings.set(player, entry);
}
player.storage.mengsanSlow_shuying = 1;
player.storage.mengsanSlowCards_shuying = 0;
player.addSkill("mengsan_slow_shuying");
player.markSkill("mengsan_slow_shuying");
}
export function canCountSlowCard(player, current, event, phasePlayer) {
const binding = bindings.get(player);
return Boolean(current?.session.active && binding?.battle === current && player.isAlive() &&
layersOf(player) > 0 && event?.name === "useCard" && event.card &&
!event.cancelled && !event._cancelled && event.player?.storage?.mengsanCamp_shuying === "ally" &&
phasePlayer?.storage?.mengsanCamp_shuying === "ally" && current.players?.has(event.player) &&
!binding.seen.has(event));
}
// AfterCardPlayed: the attack being resolved does not count itself. A virtual card is one play.
export function recordSlowCard(player, current, event, phasePlayer) {
if (!canCountSlowCard(player, current, event, phasePlayer)) return false;
const count = (player.storage.mengsanSlowCards_shuying ?? 0) + 1;
if (!Number.isSafeInteger(count) || !Number.isSafeInteger(count * layersOf(player) * 10)) throw new RangeError("缓慢计数溢出");
bindings.get(player).seen.add(event);
player.storage.mengsanSlowCards_shuying = count;
player.markSkill("mengsan_slow_shuying");
return true;
}
export function resetSlow(player) {
player.storage.mengsanSlowCards_shuying = 0;
player.markSkill("mengsan_slow_shuying");
}
export const slowApplies = (player, event) => Boolean(layersOf(player) > 0 && slowPercent(player) > 0 &&
(event?.mengsanAttack_shuying || isAttackCard(event?.card)) && !event.mengsanSlowApplied_shuying);
// 整数伤害适配:力量/虚弱/易伤之后乘(1+10%×已使用牌数),向下取整,护甲之后由引擎处理。
export function slowAttackDamage(damage, target) {
if (!Number.isSafeInteger(damage) || damage < 0) throw new RangeError("缓慢攻击伤害无效");
const value = damage * (100 + slowPercent(target));
if (!Number.isSafeInteger(value)) throw new RangeError("缓慢攻击伤害溢出");
return Math.floor(value / 100);
}
+3 -2
View File
@@ -8,10 +8,11 @@ export const FLYCONID_MOVES = Object.freeze({
export function eligibleFlyconidMoves(state = {}) { export function eligibleFlyconidMoves(state = {}) {
const upcoming = (state.turnsTaken || 0) + 1; const upcoming = (state.turnsTaken || 0) + 1;
const lastUsed = state.lastUsed || {}; const lastUsed = state.lastUsed || {};
// 行动间隔达到冷却值即恢复;额外等待一轮会使三招同时不可用。
const moves = []; const moves = [];
if (state.lastMove !== "smash") moves.push(FLYCONID_MOVES.smash); if (state.lastMove !== "smash") moves.push(FLYCONID_MOVES.smash);
if (lastUsed.frail == null || upcoming - lastUsed.frail > 2) moves.push(FLYCONID_MOVES.frail); if (lastUsed.frail == null || upcoming - lastUsed.frail >= 2) moves.push(FLYCONID_MOVES.frail);
if (upcoming >= 2 && (lastUsed.vulnerable == null || upcoming - lastUsed.vulnerable > 3)) moves.push(FLYCONID_MOVES.vulnerable); if (upcoming >= 2 && (lastUsed.vulnerable == null || upcoming - lastUsed.vulnerable >= 3)) moves.push(FLYCONID_MOVES.vulnerable);
return moves; return moves;
} }
+75
View File
@@ -0,0 +1,75 @@
import { EYE_CHARACTER, isFogmog, isToothedEye } from "./fogmog-intent.js";
import { createDazedData } from "../cards/status-cards.js";
import { getIntentHostiles } from "../battle/intent-effects.js";
// 生命周期属于单场战斗;复活复用原席位,不追加玩家或定时器。
export function createFogmogBattle(game, current, spawn) {
const owners = new Map(), eyes = new Map(), pending = new Map();
let round = 0, sequence = 0, cardSequence = 0;
const active = () => current.session.active;
const canAct = player => active() && player.isAlive() &&
(isFogmog(player) || isToothedEye(player) && eyes.get(player)?.isAlive());
current.session.ownResource(owners, () => { owners.clear(); eyes.clear(); pending.clear(); });
return {
canAct,
register(player, spec) {
if (!isToothedEye(player)) return;
const owner = game.players.find(p => p.playerid === spec.fogmogOwner && isFogmog(p));
if (!owner || owners.has(owner)) throw new Error("利齿之眼缺少唯一雾菇召唤者");
owners.set(owner, player); eyes.set(player, owner);
player.storage.mengsanFogmogOwner_shuying = owner.playerid;
player.addSkill("mengsan_fogmog_illusion_shuying");
player.markSkill("mengsan_fogmog_illusion_shuying");
},
async summon(owner) {
if (!canAct(owner) || !isFogmog(owner) || owners.has(owner)) return;
if (game.players.length + game.dead.length >= 8) throw new Error("雾菇召唤需要一个空席位");
await spawn({ id: `fogmog_eye_${++sequence}`, character: EYE_CHARACTER,
camp: "enemy", tier: "normal", hp: 6, maxHp: 6, hand: 0,
inheritSkills: false, fogmogOwner: owner.playerid }, owner);
},
distract(source) {
if (!canAct(source)) return;
for (const target of getIntentHostiles(game, source)) {
const pile = target === game.me ? current.personalPiles : current.monsterPiles.get(target);
if (!pile) continue; // 无个人牌堆的剧情援军不向公共牌堆塞入状态牌。
for (let index = 0; index < 3 && canAct(source); index++) {
pile.addToDiscard(createDazedData(`dazed_${source.playerid}_${++cardSequence}`));
}
game.log(target, "的个人弃牌堆加入3张【晕眩】");
}
},
onDeath(player) {
if (eyes.has(player)) {
if (eyes.get(player).isAlive()) pending.set(player, round + 1);
player.storage.mengsanFogmogIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
}
if (owners.has(player)) {
const eye = owners.get(player);
pending.delete(eye);
eye.storage.mengsanFogmogIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(eye, null);
}
},
beforeRound() {
if (!active()) return;
round++;
for (const [eye, due] of pending) {
const owner = eyes.get(eye);
if (!owner?.isAlive()) { pending.delete(eye); continue; }
if (round < due) continue;
pending.delete(eye);
if (!eye.isAlive()) {
eye.revive(6, false);
eye.hujia = 0;
eye.storage.mengsanEnergy_shuying = eye.storage.mengsanMaxEnergy_shuying;
eye.addSkill("mengsan_fogmog_illusion_shuying");
eye.markSkill("mengsan_fogmog_illusion_shuying");
eye.update();
game.log(eye, "以6点生命复活");
}
}
},
};
}
+35
View File
@@ -0,0 +1,35 @@
// 灰机 Wiki 雾菇页的普通难度:74生命,重击8、头槌14。
export const FOGMOG_CHARACTER = "mengsan_fogmog_shuying";
export const EYE_CHARACTER = "mengsan_eye_with_teeth_shuying";
export const FOGMOG_MOVES = Object.freeze({
spores: Object.freeze({ id: "spores", name: "虚幻孢子", summon: EYE_CHARACTER }),
thwack: Object.freeze({ id: "thwack", name: "重击", damage: 8, strength: 1 }),
headbutt: Object.freeze({ id: "headbutt", name: "头槌", damage: 14 }),
});
export const EYE_INTENT = Object.freeze({ id: "distract", name: "牵制", dazed: 3 });
export function selectFogmogMove(state = {}, random) {
const turn = state.turnsTaken || 0;
if (!Number.isSafeInteger(turn) || turn < 0) throw new RangeError("雾菇行动次数无效");
if (!turn) return FOGMOG_MOVES.spores;
if (state.lastMove !== "thwack") return FOGMOG_MOVES.thwack;
if ((state.thwacks || 0) >= 2) return FOGMOG_MOVES.headbutt;
const roll = random();
if (!Number.isFinite(roll) || roll < 0 || roll >= 1) throw new RangeError("怪物随机数必须在 [0,1) 内");
return roll < 0.4 ? FOGMOG_MOVES.thwack : FOGMOG_MOVES.headbutt;
}
export function recordFogmogAction(state = {}, move, paid) {
return {
turnsTaken: (state.turnsTaken || 0) + 1,
lastMove: paid && move ? move.id : state.lastMove || null,
thwacks: paid && move ? (move.id === "thwack" ?
(state.lastMove === "thwack" ? (state.thwacks || 0) + 1 : 1) : 0) : state.thwacks || 0,
};
}
export const isFogmog = player => player?.name === FOGMOG_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
export const isToothedEye = player => player?.name === EYE_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
export const isFogmogActor = player => isFogmog(player) || isToothedEye(player);
// 爪牙不单独构成胜利目标;雾菇死亡后无需继续杀死或等待其幻象。
export const isEncounterEnemy = player => player?.isAlive() &&
player.storage?.mengsanCamp_shuying === "enemy" && !isToothedEye(player);
+52
View File
@@ -0,0 +1,52 @@
// 灰机 Wiki 墨宝:奇数位刺击起手;偶数位旋风起手后刺击;以后随机招与刺击交替。
export const INKLET_CHARACTER = "mengsan_inklet_shuying";
export const INKLET_MOVES = Object.freeze({
jab: Object.freeze({ id: "jab", name: "刺击", damage: 3 }),
whirlwind: Object.freeze({ id: "whirlwind", name: "旋风斩", damage: 2, hits: 3 }),
gaze: Object.freeze({ id: "gaze", name: "锐利凝视", damage: 10 }),
});
const roll = random => {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("墨宝随机数必须在[0,1)内");
return value;
};
const positionOf = state => {
const position = state.position ?? 1;
if (!Number.isInteger(position) || position < 1 || position > 7) throw new RangeError("墨宝站位必须为1~7");
return position;
};
const stageOf = state => {
const stage = state.stage ?? "opening";
if (!["opening", "jab", "random"].includes(stage)) throw new RangeError("墨宝行动节点无效");
return stage;
};
export function selectInkletMove(state = {}, random) {
const position = positionOf(state), stage = stageOf(state);
if (stage === "opening") return position % 2 ? INKLET_MOVES.jab : INKLET_MOVES.whirlwind;
if (stage === "jab") return INKLET_MOVES.jab;
return roll(random) < 0.5 ? INKLET_MOVES.gaze : INKLET_MOVES.whirlwind;
}
export function recordInkletAction(state = {}, advance = true) {
const position = positionOf(state), stage = stageOf(state), turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0 || turns === Number.MAX_SAFE_INTEGER || typeof advance !== "boolean") {
throw new RangeError("墨宝行动记录无效");
}
const next = stage === "opening" ? (position % 2 ? "random" : "jab") : stage === "jab" ? "random" : "jab";
return { position, stage: advance ? next : stage, turnsTaken: turns + 1 };
}
export const rollInkletHp = random => 11 + Math.floor(roll(random) * 7);
export const isInklet = player => player?.name === INKLET_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
// 在梦三的原生changeHp事件创建后限幅;保留护甲消耗量,只将穿透后的生命损失限制为1。
// 层数由实际changeHp通知消耗,完全格挡、零伤害及取消事件不消耗。
export function capInkletHpLoss(player, event) {
if (!isInklet(player) || !(player.storage.mengsanSlippery_shuying > 0) ||
!Number.isFinite(event.num) || event.num >= 0) return event;
const parent = event.getParent();
if (parent?.unreal) return event;
const armor = parent?.name === "damage" && !parent.nohujia && !player.hasSkillTag("nohujia") ?
Math.max(0, player.hujia || 0) : 0;
if (-event.num > armor) event.num = -Math.min(-event.num, armor + 1);
return event;
}
+26
View File
@@ -0,0 +1,26 @@
// 闪光贾克斯果:每回合能量球,伤害结算完成后获得2力量;不随机选择行动。
export const JAXFRUIT_CHARACTER = "mengsan_snapping_jaxfruit_shuying";
export const JAXFRUIT_MOVE = Object.freeze({ id: "energyOrb", name: "能量球", damage: 3, strength: 2 });
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("贾克斯果行动记录无效");
return turns;
};
export function selectJaxfruitMove(state = {}) {
turnsOf(state);
return JAXFRUIT_MOVE;
}
export function recordJaxfruitAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) {
throw new RangeError("贾克斯果行动推进无效");
}
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export function rollJaxfruitHp(random) {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("贾克斯果随机数必须在[0,1)内");
return 31 + Math.floor(value * 3);
}
export const isJaxfruit = player => player?.name === JAXFRUIT_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";

Some files were not shown because too many files have changed in this diff Show More