fixed # 补全梦三怪物、卡牌
fixed # 修复天书乱斗中,过关技能不会重置次数的问题
@@ -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张牌;当前牌直接显示其实际格挡值。沿用脆弱对格挡的衰减与个人牌堆抽牌/洗牌/不足失败流程;等待格挡完全结算,战斗结束、死亡、换场或个人牌堆被替换后不再抽牌,不触碰公共和其他角色牌堆,不修改永久牌组;无消耗关键词。描述只显示当前费用与效果。
|
||||
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -1,6 +1,6 @@
|
||||
// Candidate external coordinator. Finish callbacks return a boolean, NEVER a drain promise.
|
||||
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;
|
||||
async function drive() {
|
||||
try {
|
||||
@@ -13,16 +13,20 @@ export function createBattleFlow({settlement, chooseReward, showMap, showEnding,
|
||||
await presentVictory(copy(job.pending));
|
||||
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";
|
||||
if (!job.hasChoice) {
|
||||
job.choiceId = job.pending.outcome === "defeat" ? null
|
||||
job.choiceId = job.pending.outcome === "defeat" ? await chooseDefeat()
|
||||
: await chooseReward(copy(job.pending.choices), {
|
||||
boss: job.pending.boss,
|
||||
fixedRewards: copy(job.pending.fixedRewards || []),
|
||||
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.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.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;
|
||||
}
|
||||
state = job.committed.route === "map" ? "map" : "ended";
|
||||
@@ -73,7 +79,7 @@ export function createBattleFlow({settlement, chooseReward, showMap, showEnding,
|
||||
state = "starting";
|
||||
try {
|
||||
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.
|
||||
state = "battle"; start();
|
||||
} 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 (!["victory", "defeat"].includes(outcome)) throw new Error("Unknown outcome");
|
||||
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";
|
||||
active.session.requestStop();
|
||||
launch();
|
||||
|
||||
@@ -8,7 +8,7 @@ const copy = value => JSON.parse(JSON.stringify(value));
|
||||
const idFor = (run, node) => JSON.stringify([run.runId, run.actIndex, node.id]);
|
||||
export function createBattleSettlement({
|
||||
store, config, getRandomRewardChoices, applyReward, completeNode,
|
||||
enterNextAct, now = Date.now, logRewardPackage = () => {},
|
||||
enterNextAct, createRun, now = Date.now, logRewardPackage = () => {},
|
||||
}) {
|
||||
const readRun = () => store.read()[config.saveKey];
|
||||
function current(storage, runId) {
|
||||
@@ -25,9 +25,9 @@ export function createBattleSettlement({
|
||||
return Object.freeze({
|
||||
readRun,
|
||||
// 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 (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);
|
||||
return store.update(storage => {
|
||||
const run = current(storage, snapshot.runId);
|
||||
@@ -42,7 +42,7 @@ export function createBattleSettlement({
|
||||
}
|
||||
// Battle may have changed RNG/card state since the last checkpoint.
|
||||
const base = copy(snapshot); delete base.battleFlow;
|
||||
const bondGrowth = settleBondBattle(base);
|
||||
const bondGrowth = settleBondBattle(base, node);
|
||||
if (outcome === "victory") {
|
||||
base.player.hp = Math.max(1, Math.min(hp, base.player.maxHp ?? hp));
|
||||
base.statistics.defeatedEnemies += encounter.defeatedEnemies ?? 1;
|
||||
@@ -50,21 +50,68 @@ export function createBattleSettlement({
|
||||
const relicRecovery = hp > 0
|
||||
? applyBattleEndRelics(base, outcome) : 0;
|
||||
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 the configured amount as a candidate, never as automatic victory income.
|
||||
const choices = outcome === "victory" && !encounter.skipRandomReward
|
||||
// Persist guaranteed gold alongside loot; award it once in the chosen result.
|
||||
const choices = outcome === "victory" && !encounter.rewardPackage
|
||||
? [{id:"mengsan.reward.gold.shuying", kind:"gold", name:"金币", amount:encounter.gold}, ...rewards] : [];
|
||||
const rewardPackage = outcome === "victory" && encounter.rewardPackage
|
||||
? prepareRewardPackage(base, encounter.rewardPackage,
|
||||
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.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;
|
||||
});
|
||||
},
|
||||
async choose(runId, id, choiceId) {
|
||||
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") {
|
||||
const sameChoice = item.rewardPackage
|
||||
? item.choiceId?.cardId === choiceId?.cardId &&
|
||||
@@ -80,16 +127,24 @@ export function createBattleSettlement({
|
||||
applyRewardPackage(result, item.rewardPackage, choiceId,
|
||||
applyReward);
|
||||
}
|
||||
const choice = item.choices.find(c => c.id === choiceId);
|
||||
if (!item.rewardPackage && item.choices.length && choiceId !== null) {
|
||||
if (!choice) throw new Error("Reward is not a fixed candidate");
|
||||
if (choice.kind === "gold") {
|
||||
if (!Number.isSafeInteger(choice.amount) || choice.amount < 0) throw new Error("Invalid pending gold reward");
|
||||
result.player.gold += choice.amount;
|
||||
result.statistics.goldEarned += choice.amount;
|
||||
} else applyReward(result, choice.effectId || choice.id);
|
||||
} else if (!item.rewardPackage && choiceId !== null) throw new Error("Unexpected reward selection");
|
||||
for (const rewardId of item.fixedRewards || []) applyReward(result, rewardId);
|
||||
if (!item.rewardPackage) {
|
||||
const reward = item.choices.find(c => c.id === choiceId && c.kind !== "gold");
|
||||
if (choiceId !== null && !reward) {
|
||||
throw new Error("Choose one pending reward or skip");
|
||||
}
|
||||
// This result is persisted before commit; retries never credit gold twice.
|
||||
for (const gold of item.choices.filter(c => c.kind === "gold")) {
|
||||
if (!Number.isSafeInteger(gold.amount) || gold.amount < 0) throw new Error("Invalid pending gold reward");
|
||||
result.player.gold += gold.amount;
|
||||
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");
|
||||
route = item.boss && !enterNextAct(result) ? "victory" : "map";
|
||||
} else result.status = "failed";
|
||||
@@ -111,6 +166,14 @@ export function createBattleSettlement({
|
||||
const {run, item} = pending(storage, runId, id);
|
||||
if (item.state !== "chosen") throw new Error("Durable chosen result required");
|
||||
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};
|
||||
if (item.route === "map") storage[config.saveKey] = result;
|
||||
else {
|
||||
@@ -118,12 +181,13 @@ export function createBattleSettlement({
|
||||
profile.completedRuns ||= [];
|
||||
if (!profile.completedRuns.some(r => r.runId === runId)) {
|
||||
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});
|
||||
}
|
||||
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};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
// 梦三独有的出牌经济,不修改 noname 的全局牌定义。
|
||||
import { isAttackCard } from "../monsters/vine-tangled.js";
|
||||
import { _status } from "../../../../noname.js";
|
||||
import { cardUpgradeLevel, cardUpgradeRule } from "../cards/upgrades.js";
|
||||
export const PLAYER_ENERGY = 3;
|
||||
export const PLAYER_HAND_LIMIT = 3;
|
||||
export const SHA_DAMAGE = 6;
|
||||
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,
|
||||
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) => {
|
||||
if (!event || !player) return false;
|
||||
@@ -17,7 +24,7 @@ export const isActiveCardUse = (event, player) => {
|
||||
let inPhaseUse = false;
|
||||
for (let current = event; current && !seen.has(current); current = current.parent) {
|
||||
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 === "phaseUse" && current.player === player) inPhaseUse = true;
|
||||
}
|
||||
@@ -26,10 +33,35 @@ export const isActiveCardUse = (event, 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) => {
|
||||
if (!canPayCard(player, card)) return false;
|
||||
player.storage.mengsanEnergy_shuying -= cardCost(card);
|
||||
export const canPayCard = (player, card, event = _status.event) =>
|
||||
isFreeCardUse(player, card, event) || (player?.storage?.mengsanEnergy_shuying ?? Infinity) >= cardCost(card, player);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { slowAttackDamage } from "../monsters/effigy-slow.js";
|
||||
import { shrinkAttackDamage } from "../monsters/shrinker-status.js";
|
||||
|
||||
export function attackHitCount(intent) {
|
||||
const hits = intent.hits ?? 1;
|
||||
if (!Number.isInteger(hits) || hits < 1) {
|
||||
@@ -16,7 +19,7 @@ export function outgoingAttackDamage(intent, source) {
|
||||
if (source?.storage?.mengsanWeak_shuying > 0) {
|
||||
damage = Math.floor(damage * 0.75);
|
||||
}
|
||||
return damage;
|
||||
return shrinkAttackDamage(damage, source);
|
||||
}
|
||||
|
||||
export function previewAttackDamage(intent, source, target) {
|
||||
@@ -24,5 +27,5 @@ export function previewAttackDamage(intent, source, target) {
|
||||
if (target?.storage?.mengsanVulnerable_shuying > 0) {
|
||||
damage = Math.ceil(damage * 1.5);
|
||||
}
|
||||
return damage;
|
||||
return slowAttackDamage(damage, target);
|
||||
}
|
||||
|
||||
@@ -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 { beginDeathBlow, isDeathBlowIntent } from "./death-blow.js";
|
||||
|
||||
@@ -11,14 +13,8 @@ export function createIntentExecutor(game, getActiveBattle) {
|
||||
Boolean(getActiveBattle()?.session.active);
|
||||
const applyDebuff = (target, move) => {
|
||||
if (!move.debuff) return;
|
||||
const frail = move.debuff === "脆弱";
|
||||
const key = frail ? "mengsanFrail_shuying" :
|
||||
"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");
|
||||
if (move.debuff === "缠结") { applyTangled(getActiveBattle(), target, move.stacks); return; }
|
||||
applyMengsanDebuff(target, move.debuff === "脆弱" ? "frail" : "vulnerable", move.stacks);
|
||||
};
|
||||
return async (source, move) => {
|
||||
for (const target of getIntentHostiles(game, source)) {
|
||||
|
||||
@@ -43,7 +43,15 @@ export function markBondDown(run, id) {
|
||||
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 id = bonds.selected;
|
||||
const npc = bonds.npcs[id];
|
||||
@@ -54,7 +62,11 @@ export function prepareBondBattle(run, random) {
|
||||
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;
|
||||
if (!battle) return null;
|
||||
const npc = ensureBonds(run).npcs[battle.id];
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createCardData } from "./card-data.js";
|
||||
import { cardUpgradeLevel } from "./upgrades.js";
|
||||
|
||||
// Route generated copies explicitly to their actor's real pile; never infer a public pile.
|
||||
export function createBattleCardCopier(game, getActiveBattle) {
|
||||
return (player, card, originals = []) => {
|
||||
const current = getActiveBattle();
|
||||
if (!current?.session.active) return null;
|
||||
const pile = player === game.me ? current.personalPiles : current.monsterPiles.get(player);
|
||||
if (!pile) throw new Error("复制牌的角色没有个人弃牌堆");
|
||||
if (!card || typeof card.name !== "string") throw new TypeError("复制牌缺少牌名");
|
||||
const physical = Array.isArray(originals) ? originals.find(item => item?.name === card.name) : null;
|
||||
const source = card.storage?.mengsanCard_shuying || physical?.storage?.mengsanCard_shuying || {};
|
||||
const serial = (current.generatedCardSerial || 0) + 1;
|
||||
if (!Number.isSafeInteger(serial)) throw new RangeError("战斗生成牌序号越界");
|
||||
const data = createCardData({
|
||||
id: `mengsan_copy_${current.session.id}_${serial}`,
|
||||
name: card.name,
|
||||
suit: card.suit || source.suit || "spade",
|
||||
number: card.number || source.number || 1,
|
||||
nature: card.nature || source.nature || null,
|
||||
upgrade: cardUpgradeLevel({name:card.name, storage:{mengsanCard_shuying:source}}),
|
||||
affixes: (source.affixes || []).filter(key => key !== "annihilate"),
|
||||
}, { enemy: true });
|
||||
// Fresh JSON metadata means no consumed/exhausted flags or source ID are shared.
|
||||
current.generatedCardSerial = serial;
|
||||
return pile.addToDiscard(data);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Actor-local discard recovery. Do not infer ownership from the active global UI pile.
|
||||
export function createBattleCardReclaimer(game, getActiveBattle, get) {
|
||||
return async player => {
|
||||
const current = getActiveBattle();
|
||||
if (!current?.session.active || !player.isAlive()) return null;
|
||||
const getPile = () => player === game.me
|
||||
? current.personalPiles : current.monsterPiles.get(player);
|
||||
const pile = getPile();
|
||||
if (!pile) throw new Error("回收牌的角色没有个人牌堆");
|
||||
const cards = pile.discardCards();
|
||||
if (!cards.length) return null;
|
||||
let chosen = cards[0];
|
||||
if (cards.length > 1) {
|
||||
const result = await player.chooseButton({
|
||||
createDialog: ["头槌:选择一张个人弃牌,放到个人抽牌堆顶部", cards],
|
||||
forced: true, selectButton: 1,
|
||||
ai(button) { return get.value(button.link, player); },
|
||||
}).forResult();
|
||||
if (!result.bool || result.links?.length !== 1) return null;
|
||||
chosen = result.links[0];
|
||||
}
|
||||
// The selection is asynchronous: reject stale battles, piles and removed cards.
|
||||
if (getActiveBattle() !== current || !current.session.active ||
|
||||
!player.isAlive() || getPile() !== pile || !cards.includes(chosen)) return null;
|
||||
if (!pile.moveDiscardToTop(chosen)) return null;
|
||||
game.log(player, "将", chosen, "放到个人抽牌堆顶部");
|
||||
return chosen;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 只向同一场仍在进行的战斗中的存活角色增加当前费用,不改变上限。
|
||||
export function createBattleEnergy(game, getActiveBattle) {
|
||||
const valid = (player, battle) => Boolean(battle &&
|
||||
battle === getActiveBattle() && battle.session?.active &&
|
||||
battle.players?.has(player) && player?.isAlive?.());
|
||||
const validateAmount = (player, amount) => {
|
||||
const current = player.storage?.mengsanEnergy_shuying;
|
||||
if (!Number.isSafeInteger(current) || current < 0 ||
|
||||
!Number.isSafeInteger(amount) || amount <= 0 ||
|
||||
!Number.isSafeInteger(current + amount)) {
|
||||
throw new Error("梦三费用数值无效");
|
||||
}
|
||||
};
|
||||
return {
|
||||
canUse(player) { return valid(player, getActiveBattle()); },
|
||||
capture(player, amount) {
|
||||
const battle = getActiveBattle();
|
||||
if (!valid(player, battle)) return null;
|
||||
validateAmount(player, amount);
|
||||
return battle;
|
||||
},
|
||||
grant(player, amount, battle) {
|
||||
if (!valid(player, battle)) return false;
|
||||
validateAmount(player, amount);
|
||||
player.storage.mengsanEnergy_shuying += amount;
|
||||
battle.energyUI.get(player)?.();
|
||||
if (player === game.me) battle.handUI?.refresh();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 原生格挡完全结算后才抽牌,固定当前战斗和个人牌堆身份。
|
||||
export function createBlockDrawer(game, getActiveBattle) {
|
||||
const valid = (player, battle) => Boolean(battle && getActiveBattle() === battle &&
|
||||
battle.session?.active && battle.players?.has(player) && player?.isAlive?.());
|
||||
const getPile = (player, battle) => player === game.me ? battle.personalPiles : battle.monsterPiles?.get(player);
|
||||
return async function blockDraw(player, block, draw) {
|
||||
if (!Number.isSafeInteger(block) || block < 0 || !Number.isSafeInteger(draw) || draw < 0) {
|
||||
throw new TypeError("格挡抽牌数值无效");
|
||||
}
|
||||
const battle = getActiveBattle();
|
||||
if (!valid(player, battle)) return;
|
||||
const pile = getPile(player, battle);
|
||||
if (typeof pile?.take !== "function") throw new Error("格挡抽牌缺少个人牌堆接口");
|
||||
await player.changeHujia(block);
|
||||
// 格挡的触发事件中可能结束战斗、死亡或换场,不摸新场次的牌。
|
||||
if (!valid(player, battle) || getPile(player, battle) !== pile) return;
|
||||
if (draw > 0) await player.draw(draw);
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,26 @@
|
||||
// 梦三永久牌组的可获得牌注册表。这里登记牌名与归属,不复制引擎中的效果定义。
|
||||
// 新增武将专属牌时填写 owner 为该武将 ID;通用牌填写 owner: null。
|
||||
const definitions = [
|
||||
{ name: "mengsan_songjianwushi", category: "utility", owner: null },
|
||||
{ name: "mengsan_pomie", category: "utility", owner: null },
|
||||
{ name: "mengsan_jianyi", category: "utility", owner: null },
|
||||
{ name: "mengsan_fangxue", category: "utility", owner: null },
|
||||
{ name: "mengsan_yujin", category: "damage", owner: null },
|
||||
{ name: "mengsan_wanmeidaji", category: "damage", owner: null },
|
||||
{ name: "mengsan_yubeidaji", category: "damage", owner: null },
|
||||
{ name: "mengsan_tupo", category: "damage", owner: null },
|
||||
{ name: "mengsan_touchui", category: "damage", owner: null },
|
||||
{ name: "mengsan_tiezhanbo", category: "damage", owner: null },
|
||||
{ name: "mengsan_shuangchongdaji", category: "damage", owner: null },
|
||||
{ name: "mengsan_shandianpili", category: "damage", owner: null },
|
||||
{ name: "sha", category: "damage", owner: null },
|
||||
{ name: "mengsan_zhongsha", category: "damage", owner: null },
|
||||
{ name: "mengsan_fennu", category: "damage", owner: null },
|
||||
{ name: "mengsan_rongrongzhiquan", category: "damage", owner: null },
|
||||
{ name: "mengsan_quanshenzhuangji", category: "damage", owner: null },
|
||||
{ name: "mengsan_jianbingdaji", category: "damage", owner: null },
|
||||
{ name: "mengsan_feijianhuixuanbiao", category: "damage", owner: null },
|
||||
{ name: "mengsan_fangyu", category: "utility", owner: null },
|
||||
{ name: "shan", category: "utility", owner: null },
|
||||
{ name: "tao", category: "recovery", owner: null },
|
||||
{ name: "jiu", category: "recovery", owner: null },
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// 坚毅只消耗使用者当时的手牌:普通牌随机,当前选择模式走原生选牌事件。
|
||||
export function createHandExhauster(game, getActiveBattle, get, randomHandExhaust) {
|
||||
const valid = (player, battle) => Boolean(battle &&
|
||||
getActiveBattle() === battle && battle.session?.active &&
|
||||
battle.players?.has(player) && player?.isAlive?.());
|
||||
const getPile = (player, battle) => player === game.me
|
||||
? battle.personalPiles : battle.monsterPiles?.get(player);
|
||||
const handCards = (player, event) => {
|
||||
const playing = new Set(event?.cards || []);
|
||||
return player.getCards("h").filter(card =>
|
||||
!playing.has(card) && !card.storage?.mengsanExhausted_shuying);
|
||||
};
|
||||
return {
|
||||
capture(player) {
|
||||
const battle = getActiveBattle();
|
||||
if (!valid(player, battle)) return null;
|
||||
if (typeof getPile(player, battle)?.exhaustFromHand !== "function") {
|
||||
throw new Error("坚毅缺少个人消耗牌堆接口");
|
||||
}
|
||||
if (typeof randomHandExhaust?.exhaust !== "function") throw new Error("坚毅缺少随机手牌消耗接口");
|
||||
return { battle, pile: getPile(player, battle) };
|
||||
},
|
||||
async exhaust(player, event, token, choose) {
|
||||
const battle = token?.battle, pile = token?.pile;
|
||||
if (!valid(player, battle) || getPile(player, battle) !== pile) return null;
|
||||
if (typeof pile?.exhaustFromHand !== "function") throw new Error("坚毅缺少个人消耗牌堆接口");
|
||||
if (!choose) return await randomHandExhaust.exhaust(player, event, battle);
|
||||
const cards = handCards(player, event);
|
||||
if (!cards.length) return null;
|
||||
let chosen = cards[0];
|
||||
if (cards.length > 1) {
|
||||
const result = await player.chooseButton({
|
||||
createDialog: ["坚毅:选择消耗一张你的手牌", cards],
|
||||
forced: true, selectButton: 1,
|
||||
ai(button) { return -get.value(button.link, player); },
|
||||
}).forResult();
|
||||
if (!result.bool || result.links?.length !== 1) return null;
|
||||
chosen = result.links[0];
|
||||
}
|
||||
// 选牌期间可能已结束、换场、失去此牌或替换牌堆,不提交陈旧选择。
|
||||
if (!valid(player, battle) || getPile(player, battle) !== pile ||
|
||||
!cards.includes(chosen) || !handCards(player, event).includes(chosen)) return null;
|
||||
return await pile.exhaustFromHand(chosen);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// 破灭通过原生使用事件执行自己的堆顶原牌;授权不写入永久牌数据。
|
||||
import { authorizeFreeCardUse } from "../battle/combat-rules.js";
|
||||
export function createHavocPlayer(game, getActiveBattle, status) {
|
||||
const valid = (player, battle) => Boolean(battle && getActiveBattle() === battle &&
|
||||
battle.session?.active && battle.players?.has(player) && player?.isAlive?.());
|
||||
const getPile = (player, battle) => player === game.me ? battle.personalPiles : battle.monsterPiles?.get(player);
|
||||
return async function playDrawTop(player) {
|
||||
const battle = getActiveBattle();
|
||||
if (!valid(player, battle)) return null;
|
||||
const pile = getPile(player, battle);
|
||||
if (typeof pile?.beginTopPlay !== "function" || typeof pile?.finishTopPlay !== "function") {
|
||||
throw new Error("破灭缺少个人牌堆接口");
|
||||
}
|
||||
const original = pile.beginTopPlay();
|
||||
if (!original) return null;
|
||||
let release = () => {};
|
||||
try {
|
||||
const stillValid = () => valid(player, battle) && getPile(player, battle) === pile;
|
||||
const choice = player.chooseUseTarget({
|
||||
card: original, cards: [original], forced: true, addCount: false,
|
||||
prompt: "破灭:使用抽牌堆顶部的牌",
|
||||
oncard() {
|
||||
// 目标选择期间可能已死亡、结束或换场,不执行陈旧的使用。
|
||||
if (!stillValid()) status.event?.cancel?.();
|
||||
},
|
||||
});
|
||||
release = authorizeFreeCardUse(player, original, choice, stillValid);
|
||||
await choice;
|
||||
} finally {
|
||||
release();
|
||||
// 只认这一张实体原牌,不添加消耗词缀,避免愤怒的副本继承消耗。
|
||||
await pile.finishTopPlay(original);
|
||||
}
|
||||
return original;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { getIntentHostiles } from "../battle/intent-effects.js";
|
||||
import { isPhrog, isPhrogActor } from "../monsters/phrog-intent.js";
|
||||
|
||||
export const INFECTION_NAME = "mengsan_infection_shuying";
|
||||
export function createInfectionData(id) {
|
||||
if (typeof id !== "string" || !id) throw new TypeError("感染牌缺少ID");
|
||||
return { id, name: INFECTION_NAME, suit: "spade", number: 1,
|
||||
nature: null, affixes: ["unplayable"], upgrade: 0 };
|
||||
}
|
||||
export const infectionDefinition = Object.freeze({
|
||||
type: "status", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_infection_shuying.png",
|
||||
enable: false, notarget: true,
|
||||
ai: { value: -5, useful: -5, order: 0 },
|
||||
cardPrompt: () => "不能被打出。回合结束弃牌前,若此牌在你的手牌中,你受到3点非攻击伤害。",
|
||||
});
|
||||
export function applyInfection(game, current, source, count) {
|
||||
if (!current?.session.active || !source?.isAlive() || !isPhrogActor(source)) return;
|
||||
if (count !== (isPhrog(source) ? 3 : 1)) throw new RangeError("感染牌数量与怪物不符");
|
||||
if (!current.infectionCards) {
|
||||
const entry = current.infectionCards = { sequence: 0, resolved: new WeakSet() };
|
||||
current.session.ownResource(entry, () => { if (current.infectionCards === entry) delete current.infectionCards; });
|
||||
}
|
||||
for (const target of getIntentHostiles(game, source)) {
|
||||
if (!current.session.active || !source.isAlive()) break;
|
||||
const pile = target === game.me ? current.personalPiles : current.monsterPiles.get(target);
|
||||
if (!pile) continue;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (!current.session.active || !source.isAlive()) break;
|
||||
const sequence = current.infectionCards.sequence + 1;
|
||||
if (!Number.isSafeInteger(sequence)) throw new RangeError("感染牌序号溢出");
|
||||
current.infectionCards.sequence = sequence;
|
||||
pile.addToDiscard(createInfectionData(`infection_${source.playerid}_${sequence}`));
|
||||
}
|
||||
game.log(target, `的个人弃牌堆加入${count}张【感染】`);
|
||||
}
|
||||
}
|
||||
export const infectionHand = player => [...new Set(player?.getCards("h") || [])].filter(card =>
|
||||
card.name === INFECTION_NAME && card.storage?.mengsanOwnerId_shuying === player.playerid);
|
||||
const resolutions = new WeakMap();
|
||||
export async function resolveInfection(current, player, phase) {
|
||||
if (!current?.session.active || !player?.isAlive() || !current.players.has(player)) return false;
|
||||
let resolved = resolutions.get(current);
|
||||
if (!resolved) { resolved = new WeakSet(); resolutions.set(current, resolved); }
|
||||
if (resolved.has(phase)) return false;
|
||||
resolved.add(phase);
|
||||
const cards = infectionHand(player);
|
||||
for (const card of cards) {
|
||||
if (!current.session.active || !player.isAlive()) break;
|
||||
if (!infectionHand(player).includes(card)) continue;
|
||||
const damage = player.damage(3, "nosource");
|
||||
damage.mengsanInfection_shuying = true;
|
||||
await damage;
|
||||
}
|
||||
return cards.length > 0;
|
||||
}
|
||||
@@ -2,17 +2,23 @@
|
||||
// 保留原牌名,以兼容原有牌的响应、装备技能及转化牌引用。
|
||||
import standard from "../../../../card/standard.js";
|
||||
import extra from "../../../../card/extra.js";
|
||||
import { _status, game } from "../../../../noname.js";
|
||||
import { _status, game, get } from "../../../../noname.js";
|
||||
import { cardCost, canPayCard, isActiveCardUse, SHA_DAMAGE,
|
||||
TRICK_DAMAGE } from "../battle/combat-rules.js";
|
||||
import { cardUpgradeLevel, cardUpgradeRule } from "./upgrades.js";
|
||||
|
||||
import { applyMengsanDebuff } from "../monsters/artifact-status.js";
|
||||
import { isAttackCard } from "../monsters/vine-tangled.js";
|
||||
import { DAZED_NAME, dazedDefinition } from "./status-cards.js";
|
||||
import { SLIMED_NAME, slimedDefinition } from "./slimed-card.js";
|
||||
import { INFECTION_NAME, infectionDefinition } from "./infection-card.js";
|
||||
|
||||
const packs = [standard, extra];
|
||||
const names = [...new Set(packs.flatMap(pack => pack.list.map(entry => entry[2])))];
|
||||
const sourceCard = name => standard.card[name] || extra.card[name];
|
||||
const sourceText = name => extra.translate[`${name}_info`] || standard.translate[`${name}_info`];
|
||||
|
||||
export function createMengsanCards() {
|
||||
export function createMengsanCards({ copyToDiscard, reclaimFromDiscard, temporaryStrength, countStrikeCards, randomHandExhaust, battleEnergy, handExhaust, playDrawTop, blockDraw, isBattleActive = () => true } = {}) {
|
||||
const card = {};
|
||||
const translate = {};
|
||||
for (const name of names) {
|
||||
@@ -52,14 +58,12 @@ export function createMengsanCards() {
|
||||
const damage = cardUpgradeLevel(used)
|
||||
? cardUpgradeRule("sha").damage : SHA_DAMAGE;
|
||||
return `梦三:主动使用消耗1费用;目标须使用【闪】,` +
|
||||
`否则受到${damage}点伤害。每回合使用次数不限。` +
|
||||
(cardUpgradeLevel(used) ? "已强化(上限1次)。" :
|
||||
"可强化1次:伤害变为9点。");
|
||||
`否则受到${damage}点伤害。每回合使用次数不限。`;
|
||||
};
|
||||
translate.sha_info = `梦三:主动使用消耗1费用。出牌阶段,对攻击范围内的一名角色使用;其须使用【闪】,否则受到${SHA_DAMAGE}点伤害。每回合使用次数不限。`;
|
||||
card.mengsan_zhongsha = {
|
||||
type: "basic",
|
||||
fullskin: true,
|
||||
fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_zhongsha.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_zhongsha" }),
|
||||
enable(used, player, event) {
|
||||
@@ -79,12 +83,8 @@ export function createMengsanCards() {
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
if (!target.isAlive()) return;
|
||||
target.storage.mengsanVulnerable_shuying =
|
||||
(target.storage.mengsanVulnerable_shuying || 0) +
|
||||
effect.vulnerable;
|
||||
target.addSkill("mengsan_vulnerable_shuying");
|
||||
target.markSkill("mengsan_vulnerable_shuying");
|
||||
game.log(target, `获得${effect.vulnerable}层易伤`);
|
||||
if (applyMengsanDebuff(target, "vulnerable", effect.vulnerable))
|
||||
game.log(target, `获得${effect.vulnerable}层易伤`);
|
||||
},
|
||||
ai: { order: 4, result: { target: -1.5 },
|
||||
tag: { damage: 1 } },
|
||||
@@ -96,9 +96,7 @@ export function createMengsanCards() {
|
||||
`一名其他角色造成${effect.damage}点伤害,然后给予其` +
|
||||
`${effect.vulnerable}层易伤。受到攻击伤害增加50%` +
|
||||
"(向上取整),每个自身回合结束减少1层。" +
|
||||
"每回合使用次数不限。" + (cardUpgradeLevel(used)
|
||||
? "已强化(上限1次)。" :
|
||||
"可强化1次:10点伤害、3层易伤。");
|
||||
"每回合使用次数不限。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_zhongsha = "重杀";
|
||||
@@ -107,6 +105,648 @@ export function createMengsanCards() {
|
||||
"一名其他角色造成8点伤害,然后给予其2层易伤。" +
|
||||
"易伤使受到的攻击伤害增加50%(向上取整)," +
|
||||
"每个自身回合结束减少1层。每回合使用次数不限。";
|
||||
// STS2 Setup Strike: the new temporary strength does not enhance its own hit.
|
||||
// STS2 Cinder: damage first, then exhaust one random remaining hand card.
|
||||
card.mengsan_yujin = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_yujin.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_yujin" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
if (!isBattleActive() || !player.isAlive() || !target?.isAlive() ||
|
||||
target === player || !player.isEnemyOf(target)) return;
|
||||
if (!randomHandExhaust?.capture || !randomHandExhaust?.exhaust) throw new Error("余烬缺少随机消耗接口");
|
||||
const battle = randomHandExhaust.capture();
|
||||
if (!battle) return;
|
||||
const rule = cardUpgradeRule("mengsan_yujin");
|
||||
const damage = target.damage(cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
if (isBattleActive() && player.isAlive()) await randomHandExhaust.exhaust(player, event, battle);
|
||||
},
|
||||
ai: { order: 6, result: { target: -2 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_yujin");
|
||||
const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return `梦三:主动使用消耗2费用。对一名敌方角色造成${amount}点伤害,然后随机消耗一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。每回合使用次数不限。`;
|
||||
},
|
||||
};
|
||||
translate.mengsan_yujin = "余烬";
|
||||
translate.mengsan_yujin_info = card.mengsan_yujin.cardPrompt({ name: "mengsan_yujin" });
|
||||
// STS2 Perfected Strike counts current physical combat cards, including itself/exhaust.
|
||||
card.mengsan_wanmeidaji = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_wanmeidaji.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_wanmeidaji" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
if (!isBattleActive() || !player.isAlive() || !target?.isAlive() ||
|
||||
target === player || !player.isEnemyOf(target)) return;
|
||||
if (typeof countStrikeCards !== "function") throw new Error("完美打击缺少打击牌计数接口");
|
||||
const count = countStrikeCards(player, event);
|
||||
if (!Number.isSafeInteger(count) || count < 0) throw new RangeError("打击牌数量无效");
|
||||
const rule = cardUpgradeRule("mengsan_wanmeidaji");
|
||||
const amount = rule.baseDamage + count * (cardUpgradeLevel(event.card) ? rule.perStrike : rule.basePerStrike);
|
||||
if (!Number.isSafeInteger(amount) || amount < 0) throw new RangeError("完美打击伤害无效");
|
||||
const damage = target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
damage.mengsanStrikeCount_shuying = count;
|
||||
await damage;
|
||||
},
|
||||
ai: { order: 7, result: { target: -2 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_wanmeidaji");
|
||||
const bonus = cardUpgradeLevel(used) ? rule.perStrike : rule.basePerStrike;
|
||||
return `梦三:主动使用消耗2费用。对一名敌方角色造成6点伤害,本场战斗的牌中每有一张名称含“打击”的牌,伤害增加${bonus}点(包括手牌、个人抽牌堆、个人弃牌堆、消耗牌及打出区)。每回合使用次数不限。`;
|
||||
},
|
||||
};
|
||||
translate.mengsan_wanmeidaji = "完美打击";
|
||||
translate.mengsan_wanmeidaji_info = card.mengsan_wanmeidaji.cardPrompt({ name: "mengsan_wanmeidaji" });
|
||||
card.mengsan_yubeidaji = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_yubeidaji.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_yubeidaji" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
if (!isBattleActive() || !player.isAlive() || !target?.isAlive() ||
|
||||
target === player || !player.isEnemyOf(target)) return;
|
||||
if (!temporaryStrength?.capture || !temporaryStrength?.grant) throw new Error("预备打击缺少临时力量接口");
|
||||
const battle = temporaryStrength.capture();
|
||||
if (!battle) return;
|
||||
const rule = cardUpgradeRule("mengsan_yubeidaji");
|
||||
const upgraded = cardUpgradeLevel(event.card);
|
||||
const damage = target.damage(upgraded ? rule.damage : rule.baseDamage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
if (isBattleActive() && player.isAlive()) {
|
||||
temporaryStrength.grant(player, upgraded ? rule.strength : rule.baseStrength, event, battle);
|
||||
}
|
||||
},
|
||||
ai: { order: 8, result: { player: 0.8, target: -1.5 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_yubeidaji");
|
||||
const upgraded = cardUpgradeLevel(used);
|
||||
return `梦三:主动使用消耗1费用。对一名敌方角色造成${upgraded ? rule.damage : rule.baseDamage}点伤害,然后在本回合内获得${upgraded ? rule.strength : rule.baseStrength}点力量。每回合使用次数不限。`;
|
||||
},
|
||||
};
|
||||
translate.mengsan_yubeidaji = "预备打击";
|
||||
translate.mengsan_yubeidaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成7点伤害,然后在本回合内获得2点力量。每回合使用次数不限。";
|
||||
// STS2 Breakthrough: native life loss is paid once before the all-enemy attack.
|
||||
card.mengsan_tupo = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_tupo.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_tupo" }),
|
||||
notarget: true, usable: Infinity,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
if (!game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target)).length) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
if (!game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target)).length) return;
|
||||
// loseHp bypasses block and awaits the normal dying/rescue process.
|
||||
await player.loseHp(1);
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
const rule = cardUpgradeRule("mengsan_tupo");
|
||||
const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage;
|
||||
const targets = game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target));
|
||||
for (const target of targets) {
|
||||
if (!isBattleActive() || !player.isAlive()) break;
|
||||
if (!target.isAlive() || !player.isEnemyOf(target)) continue;
|
||||
player.line(target, "green");
|
||||
const damage = target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
}
|
||||
},
|
||||
ai: { order: 7, result: { player(player) { return player.hp <= 1 ? -10 : 1; } }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_tupo");
|
||||
const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return `梦三:主动使用消耗1费用。失去1点生命,然后对所有敌方角色造成${amount}点伤害。每回合使用次数不限。`;
|
||||
},
|
||||
};
|
||||
translate.mengsan_tupo = "突破";
|
||||
translate.mengsan_tupo_info = "梦三:主动使用消耗1费用。失去1点生命,然后对所有敌方角色造成9点伤害。每回合使用次数不限。";
|
||||
// STS2 Headbutt: damage first, then choose from the actor's own real discard pile.
|
||||
card.mengsan_touchui = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_touchui.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_touchui" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
if (!isBattleActive() || !player.isAlive() || !target?.isAlive() ||
|
||||
target === player || !player.isEnemyOf(target)) return;
|
||||
if (typeof reclaimFromDiscard !== "function") throw new Error("头槌缺少个人弃牌回收接口");
|
||||
const rule = cardUpgradeRule("mengsan_touchui");
|
||||
const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage;
|
||||
const damage = target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
// Defeating one enemy does not prevent recovery unless combat actually ends.
|
||||
if (isBattleActive() && player.isAlive()) await reclaimFromDiscard(player);
|
||||
},
|
||||
ai: { order: 6, result: { player: 0.4, target: -1.5 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_touchui");
|
||||
const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return '梦三:主动使用消耗1费用。对一名敌方角色造成' + amount +
|
||||
'点伤害,然后选择个人弃牌堆中的一张牌放到个人抽牌堆顶部。每回合使用次数不限。';
|
||||
},
|
||||
};
|
||||
translate.mengsan_touchui = "头槌";
|
||||
translate.mengsan_touchui_info = "梦三:主动使用消耗1费用。对一名敌方角色造成9点伤害," +
|
||||
"然后选择个人弃牌堆中的一张牌放到个人抽牌堆顶部。每回合使用次数不限。";
|
||||
// STS2 Iron Wave: obtain block before resolving the attack.
|
||||
card.mengsan_tiezhanbo = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_tiezhanbo.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_tiezhanbo" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
const canResolve = () => isBattleActive() && player.isAlive() &&
|
||||
target?.isAlive() && target !== player && player.isEnemyOf(target);
|
||||
if (!canResolve()) return;
|
||||
const rule = cardUpgradeRule("mengsan_tiezhanbo");
|
||||
const upgraded = cardUpgradeLevel(event.card) > 0;
|
||||
await player.changeHujia(upgraded ? rule.block : rule.baseBlock);
|
||||
// Block triggers may end combat or remove the acting player/target.
|
||||
if (!canResolve()) return;
|
||||
const damage = target.damage(upgraded ? rule.damage : rule.baseDamage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
},
|
||||
ai: { order: 7, result: { player: 0.6, target: -1.2 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_tiezhanbo");
|
||||
const upgraded = cardUpgradeLevel(used) > 0;
|
||||
const block = upgraded ? rule.block : rule.baseBlock;
|
||||
const damage = upgraded ? rule.damage : rule.baseDamage;
|
||||
return '梦三:主动使用消耗1费用。获得' + block +
|
||||
'点格挡(护甲),然后对一名敌方角色造成' + damage +
|
||||
'点伤害。每回合使用次数不限。';
|
||||
},
|
||||
};
|
||||
translate.mengsan_tiezhanbo = "铁斩波";
|
||||
translate.mengsan_tiezhanbo_info = "梦三:主动使用消耗1费用。获得5点格挡(护甲)," +
|
||||
"然后对一名敌方角色造成5点伤害。每回合使用次数不限。";
|
||||
// STS2 Twin Strike: each hit resolves on the same target separately.
|
||||
card.mengsan_shuangchongdaji = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_shuangchongdaji.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_shuangchongdaji" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const target = event.target;
|
||||
const rule = cardUpgradeRule("mengsan_shuangchongdaji");
|
||||
const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage;
|
||||
for (let hitIndex = 0; hitIndex < rule.hits; hitIndex++) {
|
||||
if (!isBattleActive() || !player.isAlive() || !target?.isAlive() ||
|
||||
target === player || !player.isEnemyOf(target)) break;
|
||||
const damage = target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
}
|
||||
},
|
||||
ai: { order: 6, result: { target: -1.5 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_shuangchongdaji");
|
||||
const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return '梦三:主动使用消耗1费用。对一名敌方角色造成' +
|
||||
amount + '点伤害两次,两次伤害分别结算。每回合使用次数不限。';
|
||||
},
|
||||
};
|
||||
translate.mengsan_shuangchongdaji = "双重打击";
|
||||
translate.mengsan_shuangchongdaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成5点伤害两次," +
|
||||
"两次伤害分别结算。每回合使用次数不限。";
|
||||
// STS2 Thunderclap: hit each living enemy before adding new vulnerable.
|
||||
card.mengsan_shandianpili = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_shandianpili.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_shandianpili" }),
|
||||
notarget: true, usable: Infinity,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
if (!game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target)).length) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
const rule = cardUpgradeRule("mengsan_shandianpili");
|
||||
const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage;
|
||||
const targets = game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target));
|
||||
for (const target of targets) {
|
||||
if (!isBattleActive() || !player.isAlive()) break;
|
||||
if (!target.isAlive() || !player.isEnemyOf(target)) continue;
|
||||
player.line(target, "green");
|
||||
const damage = target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
}
|
||||
// 伤害结束后重新取得存活敌人,包含异蛙亡语刚生成的扭动虫。
|
||||
for (const target of game.filterPlayer(target => target !== player && target.isAlive() && player.isEnemyOf(target))) {
|
||||
if (!isBattleActive() || !player.isAlive()) break;
|
||||
if (applyMengsanDebuff(target, "vulnerable", rule.vulnerable))
|
||||
game.log(target, `获得${rule.vulnerable}层易伤`);
|
||||
}
|
||||
},
|
||||
ai: { order: 7, result: { player: 1 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_shandianpili");
|
||||
const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return `梦三:主动使用消耗1费用。对所有敌方角色造成${amount}点伤害,然后给予其1层易伤。` +
|
||||
"易伤使受到的攻击伤害增加50%(向上取整),每个自身回合结束减少1层。每回合使用次数不限。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_shandianpili = "闪电霹雳";
|
||||
translate.mengsan_shandianpili_info = "梦三:主动使用消耗1费用。对所有敌方角色造成4点伤害,然后给予其1层易伤。" +
|
||||
"易伤使受到的攻击伤害增加50%(向上取整),每个自身回合结束减少1层。每回合使用次数不限。";
|
||||
// Each throw resolves separately and samples the still-living enemies again.
|
||||
card.mengsan_feijianhuixuanbiao = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_feijianhuixuanbiao.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_feijianhuixuanbiao" }),
|
||||
notarget: true, usable: Infinity,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
if (!game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target)).length) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
const rule = cardUpgradeRule("mengsan_feijianhuixuanbiao");
|
||||
const count = cardUpgradeLevel(event.card) ? rule.hits : rule.baseHits;
|
||||
for (let hitIndex = 0; hitIndex < count; hitIndex++) {
|
||||
if (!isBattleActive() || !player.isAlive()) break;
|
||||
const enemies = game.filterPlayer(target => target !== player &&
|
||||
target.isAlive() && player.isEnemyOf(target));
|
||||
if (!enemies.length) break;
|
||||
// Use the engine helper with an inclusive index range.
|
||||
const target = enemies[get.rand(0, enemies.length - 1)];
|
||||
player.line(target, "green");
|
||||
const damage = target.damage(rule.damage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
}
|
||||
},
|
||||
ai: { order: 6, result: { player: 1 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_feijianhuixuanbiao");
|
||||
const count = cardUpgradeLevel(used) ? rule.hits : rule.baseHits;
|
||||
return `梦三:主动使用消耗1费用。每次对随机一名敌方角色造成${rule.damage}点伤害,共${count}次。` +
|
||||
"每次攻击重新随机选择存活的敌人,可以重复命中同一敌人。" +
|
||||
"每回合使用次数不限。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_feijianhuixuanbiao = "飞剑回旋镖";
|
||||
translate.mengsan_feijianhuixuanbiao_info = "梦三:主动使用消耗1费用。随机对敌人造成3点伤害3次。" +
|
||||
"每次攻击重新随机选择存活的敌人,可以重复命中同一敌人。每回合使用次数不限。";
|
||||
// Resolve damage before drawing from the acting player's mode-owned pile.
|
||||
card.mengsan_jianbingdaji = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_jianbingdaji.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_jianbingdaji" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
const rule = cardUpgradeRule("mengsan_jianbingdaji");
|
||||
const upgraded = cardUpgradeLevel(event.card) > 0;
|
||||
const damage = event.target.damage(upgraded ? rule.damage : rule.baseDamage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
// Defeating the last enemy may dispose the personal pile during damage.
|
||||
if (isBattleActive() && player.isAlive()) {
|
||||
await player.draw(upgraded ? rule.draw : rule.baseDraw);
|
||||
}
|
||||
},
|
||||
ai: { order: 6, result: { player: 0.6, target: -1.5 }, tag: { damage: 1, draw: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_jianbingdaji");
|
||||
const upgraded = cardUpgradeLevel(used) > 0;
|
||||
const damage = upgraded ? rule.damage : rule.baseDamage;
|
||||
const draw = upgraded ? rule.draw : rule.baseDraw;
|
||||
return `梦三:主动使用消耗1费用。对一名敌方角色造成${damage}点伤害,然后抽${draw}张牌。` +
|
||||
"每回合使用次数不限。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_jianbingdaji = "剑柄打击";
|
||||
translate.mengsan_jianbingdaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成9点伤害,然后抽1张牌。" +
|
||||
"每回合使用次数不限。";
|
||||
// Block determines base damage without spending the acting player's block.
|
||||
card.mengsan_quanshenzhuangji = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_quanshenzhuangji.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_quanshenzhuangji" }),
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
const block = Number.isFinite(player.hujia) ? Math.max(0, player.hujia) : 0;
|
||||
const storedStrength = player.storage?.mengsanStrength_shuying;
|
||||
const strength = Number.isInteger(storedStrength) ? storedStrength : 0;
|
||||
// Native damage(0) stops before damageBegin1, so strength must be included here.
|
||||
const temporary = temporaryStrength?.amount(player) || 0;
|
||||
const damage = event.target.damage(Math.max(0, block + strength + temporary), player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
damage.mengsanCardStrengthApplied_shuying = true;
|
||||
damage.mengsanSetupStrengthApplied_shuying = true;
|
||||
await damage;
|
||||
},
|
||||
ai: {
|
||||
order: 5,
|
||||
result: { target(used, player) { return player.hujia > 0 ? -1.5 : 0; } },
|
||||
tag: { damage: 1 },
|
||||
},
|
||||
cardPrompt(used) {
|
||||
const cost = cardCost(used || { name: "mengsan_quanshenzhuangji" });
|
||||
return `梦三:主动使用消耗${cost}费用。对一名敌方角色造成等同于你当前格挡(护甲)值的伤害。` +
|
||||
"不消耗格挡,可以受到力量加成。" +
|
||||
"每回合使用次数不限。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_quanshenzhuangji = "全身撞击";
|
||||
translate.mengsan_quanshenzhuangji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成等同于你当前格挡(护甲)值的伤害。" +
|
||||
"不消耗格挡,可以受到力量加成。每回合使用次数不限。";
|
||||
// Double only the surviving target's existing vulnerable after damage resolves.
|
||||
card.mengsan_rongrongzhiquan = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_rongrongzhiquan.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_rongrongzhiquan" }),
|
||||
mengsanExhaust_shuying: true,
|
||||
usable: Infinity, selectTarget: 1,
|
||||
enable(used, player, event) {
|
||||
if (!isBattleActive() || !player.isAlive()) return false;
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && target.isAlive() && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (!isBattleActive() || !player.isAlive()) return;
|
||||
const rule = cardUpgradeRule("mengsan_rongrongzhiquan");
|
||||
const target = event.target;
|
||||
const damage = target.damage(cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
if (!target.isAlive() || !isBattleActive()) return;
|
||||
const stacks = target.storage?.mengsanVulnerable_shuying;
|
||||
if (!Number.isSafeInteger(stacks) || stacks <= 0) return;
|
||||
const doubled = stacks * 2;
|
||||
if (!Number.isSafeInteger(doubled)) throw new RangeError("易伤层数越界");
|
||||
if (applyMengsanDebuff(target, "vulnerable", stacks))
|
||||
game.log(target, `易伤层数从${stacks}翻倍为${doubled}`);
|
||||
},
|
||||
ai: { order: 6, result: { target: -1.5 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_rongrongzhiquan");
|
||||
const damage = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage;
|
||||
return `梦三:主动使用消耗1费用。对一名敌方角色造成${damage}点伤害,然后将该目标已有的易伤层数翻倍。` +
|
||||
"没有易伤时不会新增易伤。消耗:使用后退出本场战斗,不进入弃牌堆或洗牌。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_rongrongzhiquan = "熔融之拳";
|
||||
translate.mengsan_rongrongzhiquan_info = "梦三:主动使用消耗1费用。对一名敌方角色造成10点伤害,然后将该目标已有的易伤层数翻倍。" +
|
||||
"消耗:使用后退出本场战斗。";
|
||||
// STS2【愤怒】:攻击后生成独立的战斗复制品,不扩充永久牌组。
|
||||
card.mengsan_fennu = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_fennu.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_fennu" }),
|
||||
enable(used, player, event) {
|
||||
return !isActiveCardUse(event || _status.event, player) ||
|
||||
canPayCard(player, used);
|
||||
},
|
||||
usable: Infinity, selectTarget: 1,
|
||||
filterTarget(used, player, target) {
|
||||
return target !== player && player.isEnemyOf(target);
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
if (typeof copyToDiscard !== "function") throw new Error("愤怒缺少战斗复制接口");
|
||||
const amount = cardUpgradeLevel(event.card)
|
||||
? cardUpgradeRule("mengsan_fennu").damage : 6;
|
||||
const damage = event.target.damage(amount, player);
|
||||
damage.mengsanAttack_shuying = true;
|
||||
await damage;
|
||||
if (copyToDiscard(player, event.card, event.cards)) {
|
||||
game.log(player, "将一张【愤怒】的复制品加入个人弃牌堆");
|
||||
}
|
||||
},
|
||||
ai: { order: 6, result: { target: -1 }, tag: { damage: 1 } },
|
||||
cardPrompt(used) {
|
||||
const amount = cardUpgradeLevel(used)
|
||||
? cardUpgradeRule("mengsan_fennu").damage : 6;
|
||||
return `梦三:主动使用消耗0费用。对一名敌方角色造成${amount}点伤害,` +
|
||||
"然后将一张此牌的复制品加入你的个人弃牌堆。" +
|
||||
"复制品保留原牌状态和词缀,仅存在于本场战斗。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_fennu = "愤怒";
|
||||
translate.mengsan_fennu_info = "梦三:主动使用消耗0费用。对一名敌方角色造成6点伤害," +
|
||||
"然后将一张此牌的复制品加入你的个人弃牌堆。复制品保留原牌状态和词缀,仅存在于本场战斗。" +
|
||||
"每回合使用次数不限。";
|
||||
// STS2【放血】:先等待原生生命流失及濒死救援,再增加当前战斗费用。
|
||||
card.mengsan_fangxue = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_fangxue.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_fangxue" }),
|
||||
enable(used, player, event) {
|
||||
return isBattleActive() && Boolean(battleEnergy?.canUse(player)) &&
|
||||
(!isActiveCardUse(event || _status.event, player) || canPayCard(player, used));
|
||||
},
|
||||
usable: Infinity, toself: true, selectTarget: -1,
|
||||
filterTarget(used, player, target) { return target === player; },
|
||||
async content(event, trigger, player) {
|
||||
if (typeof battleEnergy?.capture !== "function" || typeof battleEnergy?.grant !== "function") {
|
||||
throw new Error("放血缺少战斗费用接口");
|
||||
}
|
||||
const rule = cardUpgradeRule("mengsan_fangxue");
|
||||
const amount = cardUpgradeLevel(event.card) ? rule.energy : rule.baseEnergy;
|
||||
const battle = battleEnergy.capture(player, amount);
|
||||
if (!battle) return;
|
||||
await player.loseHp(rule.loseHp);
|
||||
battleEnergy.grant(player, amount, battle);
|
||||
},
|
||||
ai: { order: 9, result: { player: player => player.hp > 3 ? 1 : -1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_fangxue");
|
||||
const amount = cardUpgradeLevel(used) ? rule.energy : rule.baseEnergy;
|
||||
return "梦三:主动使用消耗0费用。失去" + rule.loseHp + "点生命,然后获得" + amount + "点费用。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_fangxue = "放血";
|
||||
translate.mengsan_fangxue_info = "梦三:主动使用消耗0费用。失去3点生命,然后获得2点费用。";
|
||||
// STS2【坚毅】:先获得格挡,再消耗一张自身当时的手牌。
|
||||
card.mengsan_jianyi = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_jianyi.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_jianyi" }),
|
||||
enable(used, player, event) {
|
||||
return isBattleActive() && Boolean(player?.isAlive?.()) &&
|
||||
(!isActiveCardUse(event || _status.event, player) || canPayCard(player, used));
|
||||
},
|
||||
usable: Infinity, toself: true, selectTarget: -1,
|
||||
filterTarget(used, player, target) { return target === player; },
|
||||
async content(event, trigger, player) {
|
||||
if (typeof handExhaust?.capture !== "function" || typeof handExhaust?.exhaust !== "function") {
|
||||
throw new Error("坚毅缺少手牌消耗接口");
|
||||
}
|
||||
const battle = handExhaust.capture(player);
|
||||
if (!battle) return;
|
||||
const rule = cardUpgradeRule("mengsan_jianyi");
|
||||
const current = cardUpgradeLevel(event.card);
|
||||
await player.changeHujia(current ? rule.block : rule.baseBlock);
|
||||
await handExhaust.exhaust(player, event, battle, Boolean(current && rule.chooseExhaust));
|
||||
},
|
||||
ai: { order: 7, result: { player: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_jianyi");
|
||||
const current = cardUpgradeLevel(used);
|
||||
const block = current ? rule.block : rule.baseBlock;
|
||||
return "梦三:主动使用消耗1费用。获得" + block + "点格挡(护甲),然后" +
|
||||
(current && rule.chooseExhaust ? "选择消耗" : "随机消耗") +
|
||||
"一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_jianyi = "坚毅";
|
||||
translate.mengsan_jianyi_info = "梦三:主动使用消耗1费用。获得7点格挡(护甲),然后随机消耗一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。";
|
||||
// STS2【破灭】:免费使用自己的堆顶原牌,再将这一原牌消耗。
|
||||
card.mengsan_pomie = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_pomie.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_pomie" }),
|
||||
enable(used, player, event) {
|
||||
return isBattleActive() && Boolean(player?.isAlive?.()) &&
|
||||
(!isActiveCardUse(event || _status.event, player) || canPayCard(player, used));
|
||||
},
|
||||
usable: Infinity, toself: true, selectTarget: -1,
|
||||
filterTarget(used, player, target) { return target === player; },
|
||||
async content(event, trigger, player) {
|
||||
if (typeof playDrawTop !== "function") throw new Error("破灭缺少堆顶使用接口");
|
||||
await playDrawTop(player);
|
||||
},
|
||||
ai: { order: 6, result: { player: 1 } },
|
||||
cardPrompt(used) {
|
||||
return "梦三:主动使用消耗" + cardCost(used) +
|
||||
"费用。免费使用你抽牌堆顶部的牌,然后将该原牌消耗(退出本场战斗,不进入弃牌堆或洗牌)。抽牌堆为空时不生效。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_pomie = "破灭";
|
||||
translate.mengsan_pomie_info = "梦三:主动使用消耗1费用。免费使用你抽牌堆顶部的牌,然后将该原牌消耗(退出本场战斗,不进入弃牌堆或洗牌)。抽牌堆为空时不生效。";
|
||||
// STS2【耸肩无视】:先获得格挡,再抽一张牌。
|
||||
card.mengsan_songjianwushi = {
|
||||
type: "basic", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_songjianwushi.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_songjianwushi" }),
|
||||
enable(used, player, event) {
|
||||
return isBattleActive() && Boolean(player?.isAlive?.()) &&
|
||||
(!isActiveCardUse(event || _status.event, player) || canPayCard(player, used));
|
||||
},
|
||||
usable: Infinity, toself: true, selectTarget: -1,
|
||||
filterTarget(used, player, target) { return target === player; },
|
||||
async content(event, trigger, player) {
|
||||
if (typeof blockDraw !== "function") throw new Error("耸肩无视缺少格挡抽牌接口");
|
||||
const rule = cardUpgradeRule("mengsan_songjianwushi");
|
||||
await blockDraw(player, cardUpgradeLevel(event.card) ? rule.block : rule.baseBlock, rule.draw);
|
||||
},
|
||||
ai: { order: 7, result: { player: 1 }, tag: { draw: 1 } },
|
||||
cardPrompt(used) {
|
||||
const rule = cardUpgradeRule("mengsan_songjianwushi");
|
||||
const block = cardUpgradeLevel(used) ? rule.block : rule.baseBlock;
|
||||
return "梦三:主动使用消耗1费用。获得" + block + "点格挡(护甲),然后抽" + rule.draw + "张牌。";
|
||||
},
|
||||
};
|
||||
translate.mengsan_songjianwushi = "耸肩无视";
|
||||
translate.mengsan_songjianwushi_info = "梦三:主动使用消耗1费用。获得8点格挡(护甲),然后抽1张牌。";
|
||||
// 【防御】沿用梦三护甲事件,保留脆弱与伤害抵扣等既有规则。
|
||||
card.mengsan_fangyu = {
|
||||
type: "basic",
|
||||
fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_fangyu.png",
|
||||
mengsanCost_shuying: cardCost({ name: "mengsan_fangyu" }),
|
||||
enable(used, player, event) {
|
||||
return !isActiveCardUse(event || _status.event, player) ||
|
||||
canPayCard(player, used);
|
||||
},
|
||||
usable: Infinity,
|
||||
toself: true,
|
||||
selectTarget: -1,
|
||||
filterTarget(used, player, target) { return target === player; },
|
||||
async content(event, trigger, player) {
|
||||
await player.changeHujia(5);
|
||||
},
|
||||
ai: { order: 7, result: { target: 1 } },
|
||||
cardPrompt() { return translate.mengsan_fangyu_info; },
|
||||
};
|
||||
translate.mengsan_fangyu = "防御";
|
||||
translate.mengsan_fangyu_info =
|
||||
"梦三:主动使用消耗1费用。出牌阶段,对自己使用,获得5点格挡(护甲)。" +
|
||||
"每回合使用次数不限。";
|
||||
// 【桃】的回复量也由梦三牌本体提供;救援仍不消耗费用。
|
||||
card.tao.content = async function (event) { await event.target.recover(8); };
|
||||
translate.tao_info = "梦三:主动使用消耗1费用,回复8点生命;濒死救援不消耗费用,同样回复8点生命。";
|
||||
@@ -139,5 +779,24 @@ export function createMengsanCards() {
|
||||
};
|
||||
translate.shandian_info = "梦三:主动使用消耗1费用。若判定为黑桃2~9,目标受到12点雷电伤害;否则移至下家的判定区。";
|
||||
card.shandian.cardPrompt = () => translate.shandian_info;
|
||||
return { card, translate, names: [...names, "mengsan_zhongsha"] };
|
||||
card[INFECTION_NAME] = { ...infectionDefinition };
|
||||
translate[INFECTION_NAME] = "感染";
|
||||
translate[`${INFECTION_NAME}_info`] = infectionDefinition.cardPrompt();
|
||||
card[SLIMED_NAME] = { ...slimedDefinition };
|
||||
translate[SLIMED_NAME] = "黏液";
|
||||
translate[`${SLIMED_NAME}_info`] = slimedDefinition.cardPrompt();
|
||||
card[DAZED_NAME] = { ...dazedDefinition };
|
||||
translate[DAZED_NAME] = "晕眩";
|
||||
translate[`${DAZED_NAME}_info`] = dazedDefinition.cardPrompt();
|
||||
for (const [name, definition] of Object.entries(card)) {
|
||||
if (!isAttackCard({ name }) || typeof definition.cardPrompt !== "function") continue;
|
||||
const prompt = definition.cardPrompt;
|
||||
definition.cardPrompt = function (used) {
|
||||
const text = prompt.call(this, used);
|
||||
const owner = used && typeof get.owner === "function" ? get.owner(used) : null;
|
||||
if (!isBattleActive() || !owner?.storage?.mengsanTangled_shuying || typeof text !== "string") return text;
|
||||
return text.replace(/主动使用消耗\d+费用/, `主动使用消耗${cardCost(used || { name }, owner)}费用`);
|
||||
};
|
||||
}
|
||||
return { card, translate, names: [...names, "mengsan_zhongsha", "mengsan_fangyu", "mengsan_fennu", "mengsan_feijianhuixuanbiao", "mengsan_jianbingdaji", "mengsan_quanshenzhuangji", "mengsan_rongrongzhiquan", "mengsan_shandianpili", "mengsan_shuangchongdaji", "mengsan_tiezhanbo", "mengsan_touchui", "mengsan_tupo", "mengsan_yubeidaji", "mengsan_wanmeidaji", "mengsan_yujin", "mengsan_fangxue", "mengsan_jianyi", "mengsan_pomie", "mengsan_songjianwushi"] };
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
if (!descriptor || !descriptor.configurable || (!Object.hasOwn(descriptor, "value") && typeof descriptor.get !== "function")) throw new Error("Unsupported pile property: " + key);
|
||||
}
|
||||
const draw = document.createElement("div"), discard = document.createElement("div");
|
||||
const exhausted = document.createElement("div"); exhausted.id = "special";
|
||||
// Detached nodes: engine get.position recognises these IDs; no duplicate document IDs.
|
||||
draw.id = "cardPile"; discard.id = "discardPile";
|
||||
const owned = new Set(), restores = [];
|
||||
const owned = new Set(), topPlaying = new Set(), restores = [];
|
||||
const strict = env.strict !== false;
|
||||
let released = false, forcedOwner = false;
|
||||
const personal = () => {
|
||||
@@ -41,7 +42,17 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
card.destroyLog = false;
|
||||
card.destroyed = (current, position) => {
|
||||
if (released || !["discardPile", "equip", "judge"].includes(position)) return false;
|
||||
if (current.storage.mengsanCard_shuying.affixes.includes("exhaust") && current.storage.mengsanConsumed_shuying) {
|
||||
if (position === "discardPile" && topPlaying.has(current)) {
|
||||
if (!current.storage.mengsanExhausted_shuying) {
|
||||
current.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(current));
|
||||
refresh();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const exhaust = env.lib?.card?.[current.name]?.mengsanExhaust_shuying === true ||
|
||||
current.storage.mengsanCard_shuying.affixes.includes("exhaust");
|
||||
if (exhaust && current.storage.mengsanConsumed_shuying) {
|
||||
if (current.storage.mengsanExhausted_shuying) return true;
|
||||
current.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(current));
|
||||
@@ -51,7 +62,7 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
return false; // Actual discarded cards remain available to native skills.
|
||||
};
|
||||
for (const key of card.storage.mengsanCard_shuying.affixes) card.addGaintag(AFFIX_INFO[key]?.name || key);
|
||||
if (data.upgrade) card.addGaintag(`强化+${data.upgrade}`);
|
||||
// 强化仅用手牌 +X 角标显示;数值仍保存在 mengsanCard_shuying.upgrade。
|
||||
}
|
||||
function snapshot(card) {
|
||||
const data = card.storage?.mengsanCard_shuying || {};
|
||||
@@ -124,10 +135,95 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
observer.disconnect(); released = true;
|
||||
for (const restore of restores.reverse()) restore();
|
||||
for (const card of owned) card.remove();
|
||||
owned.clear(); draw.remove(); discard.remove();
|
||||
owned.clear(); topPlaying.clear(); draw.remove(); discard.remove(); exhausted.remove();
|
||||
});
|
||||
return {
|
||||
take,
|
||||
// 取出当前堆顶,不算抽牌、不洗牌,也不触发不足牌堆失败。
|
||||
beginTopPlay() {
|
||||
if (released || !session.active || !owner.isAlive() || !draw.firstChild) return null;
|
||||
const card = draw.firstChild;
|
||||
adopt(card); card.original = "c"; card.fix?.();
|
||||
exhausted.appendChild(card); topPlaying.add(card); refresh();
|
||||
return card;
|
||||
},
|
||||
async finishTopPlay(card) {
|
||||
if (!topPlaying.has(card)) return null;
|
||||
try {
|
||||
if (released || !session.active) return null;
|
||||
if (card.storage?.mengsanExhausted_shuying) { card.remove(); return card; }
|
||||
if (owner.getCards("hej").includes(card)) {
|
||||
const loss = owner.lose(card, exhausted); loss.type = "mengsanExhaust";
|
||||
await loss;
|
||||
if (released || !session.active || card.parentNode !== exhausted) return null;
|
||||
}
|
||||
// 被使用后在排序区/弃牌堆,或无法使用而仍在暂存区,均消耗原牌一次。
|
||||
if (!owned.has(card)) return null;
|
||||
card.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(card)); card.remove(); refresh();
|
||||
return card;
|
||||
} finally { topPlaying.delete(card); }
|
||||
},
|
||||
// Explicit exhaustion is not a discard and must not touch the public piles.
|
||||
async exhaustFromHand(card) {
|
||||
if (released || !session.active || !owner.isAlive() ||
|
||||
!owner.getCards("h").includes(card) || card.storage?.mengsanExhausted_shuying) return null;
|
||||
adopt(card);
|
||||
const loss = owner.lose(card, exhausted);
|
||||
loss.type = "mengsanExhaust";
|
||||
await loss;
|
||||
// A cancelled/redirection event must not falsely consume its original.
|
||||
if (released || !session.active || card.parentNode !== exhausted) return null;
|
||||
if (!card.storage.mengsanExhausted_shuying) {
|
||||
card.storage.mengsanExhausted_shuying = true;
|
||||
battle.exhaustPile.push(snapshot(card));
|
||||
}
|
||||
card.remove();
|
||||
refresh();
|
||||
return card;
|
||||
},
|
||||
// Current physical combat cards only. Exhausted originals remain in owned;
|
||||
// never add the serialized exhaust snapshots a second time.
|
||||
battleCards(playing = []) {
|
||||
if (released || !session.active) return [];
|
||||
const cards = new Set([...owner.getCards("h"), ...draw.childNodes, ...discard.childNodes, ...exhausted.childNodes]);
|
||||
for (const card of owned) {
|
||||
if (card.storage?.mengsanOwnerId_shuying === owner.playerid &&
|
||||
card.storage?.mengsanExhausted_shuying) cards.add(card);
|
||||
}
|
||||
// The caller supplies this actor's native use-card originals, not virtual cards.
|
||||
for (const card of playing) if (ui.ordering && card?.parentNode === ui.ordering) cards.add(card);
|
||||
return [...cards];
|
||||
},
|
||||
addToDiscard(data) {
|
||||
if (released || !session.active) return null;
|
||||
if (!data || typeof data.id !== "string" || !data.id ||
|
||||
typeof data.name !== "string" || !/^[a-z][a-z0-9_]*$/.test(data.name) ||
|
||||
!["spade", "heart", "club", "diamond"].includes(data.suit) ||
|
||||
!Number.isInteger(data.number) || data.number < 1 || data.number > 13 ||
|
||||
!Array.isArray(data.affixes) || data.affixes.some(key => !Object.hasOwn(AFFIX_INFO, key))) {
|
||||
throw new TypeError("战斗生成牌数据无效");
|
||||
}
|
||||
const card = game.createCard(data.name, data.suit, data.number, data.nature);
|
||||
adopt(card, data);
|
||||
discard.appendChild(card);
|
||||
refresh();
|
||||
return card;
|
||||
},
|
||||
// Return references only to cards currently in this actor's discard node.
|
||||
discardCards() {
|
||||
if (released || !session.active) return [];
|
||||
return Array.from(discard.childNodes).filter(card => owned.has(card) &&
|
||||
!card.storage?.mengsanExhausted_shuying);
|
||||
},
|
||||
moveDiscardToTop(card) {
|
||||
if (released || !session.active || !owned.has(card) ||
|
||||
card.parentNode !== discard || card.storage?.mengsanExhausted_shuying) return false;
|
||||
card.fix?.();
|
||||
draw.insertBefore(card, draw.firstChild);
|
||||
refresh();
|
||||
return true;
|
||||
},
|
||||
addAffix(card, key) {
|
||||
if (released || !Object.hasOwn(AFFIX_INFO, key) || !owned.has(card) || !owner.getCards("h").includes(card)) return false;
|
||||
const affixes = card.storage?.mengsanCard_shuying?.affixes;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// STS2 Cinder selects from the actor's hand after damage, never from the draw pile.
|
||||
export function createRandomHandExhauster(game, getActiveBattle, getRun, nextRandom) {
|
||||
return {
|
||||
capture() { const battle = getActiveBattle(); return battle?.session.active ? battle : null; },
|
||||
async exhaust(player, event, battle) {
|
||||
if (!battle || getActiveBattle() !== battle || !battle.session.active || !player?.isAlive()) return null;
|
||||
const piles = player === game.me ? battle.personalPiles : battle.monsterPiles?.get(player);
|
||||
if (typeof piles?.exhaustFromHand !== "function") throw new Error("余烬缺少个人消耗牌堆接口");
|
||||
const playing = new Set(event?.cards || []);
|
||||
const hand = player.getCards("h").filter(card => !playing.has(card) && !card.storage?.mengsanExhausted_shuying);
|
||||
if (!hand.length) return null;
|
||||
const run = getRun();
|
||||
if (!run) throw new Error("余烬缺少征程随机状态");
|
||||
const value = nextRandom(run);
|
||||
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("余烬随机值无效");
|
||||
return await piles.exhaustFromHand(hand[Math.floor(value * hand.length)]);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { _status } from "../../../../noname.js";
|
||||
import { canPayCard, isActiveCardUse } from "../battle/combat-rules.js";
|
||||
export const SLIMED_NAME = "mengsan_slimed_shuying";
|
||||
export function createSlimedData(id) {
|
||||
if (typeof id !== "string" || !id) throw new TypeError("黏液牌缺少ID");
|
||||
return { id, name: SLIMED_NAME, suit: "spade", number: 1, nature: null, affixes: ["exhaust"], upgrade: 0 };
|
||||
}
|
||||
export const slimedDefinition = Object.freeze({
|
||||
type: "status", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_slimed_shuying.png",
|
||||
mengsanCost_shuying: 1, mengsanExhaust_shuying: true,
|
||||
enable(used, player, event) {
|
||||
return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used);
|
||||
},
|
||||
notarget: true,
|
||||
async content(event, trigger, player) { await player.draw(1); },
|
||||
ai: { value: -1, useful: 0, order: 1, result: { player: 1 } },
|
||||
cardPrompt: () => "梦三:主动使用消耗1费用。抽1张牌。消耗:使用后退出此次战斗。",
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
// 战斗生成的状态牌不登记到永久可获得牌表,也不写入征程牌组。
|
||||
export const DAZED_NAME = "mengsan_dazed_shuying";
|
||||
export function createDazedData(id) {
|
||||
if (typeof id !== "string" || !id) throw new TypeError("晕眩牌缺少ID");
|
||||
return { id, name: DAZED_NAME, suit: "spade", number: 1,
|
||||
nature: null, affixes: ["unplayable", "void"], upgrade: 0 };
|
||||
}
|
||||
export const dazedDefinition = Object.freeze({
|
||||
type: "status", fullimage: true,
|
||||
image: "ext:术樱包/mengsan/assets/cards/mengsan_dazed_shuying.png",
|
||||
enable: false, notarget: true,
|
||||
ai: { value: -1, useful: -1, order: 0 },
|
||||
cardPrompt: () => "不能被打出。虚无:弃牌阶段开始时移除此牌,退出此次战斗。",
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// Count current actor-owned combat cards; permanent save data is deliberately absent.
|
||||
export function createStrikeCounter(game, getActiveBattle, lib) {
|
||||
return (player, event) => {
|
||||
const battle = getActiveBattle();
|
||||
if (!battle?.session.active || !player?.isAlive()) return 0;
|
||||
const piles = player === game.me ? battle.personalPiles : battle.monsterPiles?.get(player);
|
||||
if (typeof piles?.battleCards !== "function") throw new Error("完美打击缺少个人战斗牌堆接口");
|
||||
const playing = new Set(event?.cards || []);
|
||||
const seen = new Set();
|
||||
for (let parent = event?.parent; parent && !seen.has(parent); parent = parent.parent) {
|
||||
seen.add(parent);
|
||||
if (parent.name === "useCard" && parent.player === player) {
|
||||
for (const card of parent.cards || []) playing.add(card);
|
||||
}
|
||||
}
|
||||
// Nested native attacks may leave earlier originals in the playing zone.
|
||||
for (const used of player.getHistory?.("useCard") || []) {
|
||||
for (const card of used.cards || []) playing.add(card);
|
||||
}
|
||||
let count = 0;
|
||||
for (const card of piles.battleCards(playing)) {
|
||||
const name = lib.translate[card.name];
|
||||
if (typeof name === "string" && name.includes("打击")) count++;
|
||||
}
|
||||
return count;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Temporary combat-only strength. Never alter permanent strength or the run/deck.
|
||||
const records = new WeakMap();
|
||||
const SKILL = "mengsan_setup_strength_shuying";
|
||||
|
||||
export function forgetTemporaryStrength(player) {
|
||||
const existed = records.delete(player);
|
||||
delete player.storage.mengsanSetupStrength_shuying;
|
||||
return existed;
|
||||
}
|
||||
|
||||
export function clearTemporaryStrength(player) {
|
||||
if (!records.has(player)) return false;
|
||||
forgetTemporaryStrength(player);
|
||||
player.removeSkill(SKILL);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function temporaryStrengthAmount(player, battle) {
|
||||
const entry = records.get(player);
|
||||
return entry?.battle === battle && battle?.session.active ? entry.amount : 0;
|
||||
}
|
||||
|
||||
export function temporaryStrengthExpires(player, phase) {
|
||||
const entry = records.get(player);
|
||||
return Boolean(entry && phase.name === "phase" && (!entry.turn || entry.turn === phase));
|
||||
}
|
||||
|
||||
export function createTemporaryStrength(game, getActiveBattle) {
|
||||
return {
|
||||
amount(player) { return temporaryStrengthAmount(player, getActiveBattle()); },
|
||||
capture() {
|
||||
const battle = getActiveBattle();
|
||||
return battle?.session.active ? battle : null;
|
||||
},
|
||||
grant(player, amount, event, battle) {
|
||||
if (!battle?.session.active || getActiveBattle() !== battle || !player.isAlive()) return false;
|
||||
if (!Number.isSafeInteger(amount) || amount <= 0) throw new RangeError("临时力量必须是正安全整数");
|
||||
const ancestor = event.getParent?.("phase");
|
||||
const turn = ancestor?.name === "phase" ? ancestor : null;
|
||||
let entry = records.get(player);
|
||||
if (entry && (entry.battle !== battle || entry.turn !== turn)) {
|
||||
clearTemporaryStrength(player);
|
||||
entry = null;
|
||||
}
|
||||
if (amount > Number.MAX_SAFE_INTEGER - (entry?.amount || 0)) throw new RangeError("临时力量层数越界");
|
||||
if (!entry) {
|
||||
entry = { battle, turn, amount: 0 };
|
||||
// Register cleanup before publishing state. A stale release cannot clear a later battle.
|
||||
battle.session.ownResource(entry, () => {
|
||||
if (records.get(player) === entry) forgetTemporaryStrength(player);
|
||||
});
|
||||
records.set(player, entry);
|
||||
}
|
||||
entry.amount += amount;
|
||||
player.storage.mengsanSetupStrength_shuying = entry.amount;
|
||||
player.addSkill(SKILL);
|
||||
player.markSkill(SKILL);
|
||||
game.log(player, `在本回合内获得${amount}点力量`);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,21 @@
|
||||
const upgrades = Object.freeze({
|
||||
mengsan_songjianwushi: Object.freeze({ maxLevel: 1, baseBlock: 8, block: 11, draw: 1 }),
|
||||
mengsan_pomie: Object.freeze({ maxLevel: 1, baseCost: 1, cost: 0 }),
|
||||
mengsan_jianyi: Object.freeze({ maxLevel: 1, baseBlock: 7, block: 9, chooseExhaust: true }),
|
||||
mengsan_fangxue: Object.freeze({ maxLevel: 1, loseHp: 3, baseEnergy: 2, energy: 3 }),
|
||||
mengsan_yujin: Object.freeze({ maxLevel: 1, baseDamage: 18, damage: 24, exhaust: 1 }),
|
||||
mengsan_wanmeidaji: Object.freeze({ maxLevel: 1, baseDamage: 6, damage: 6, basePerStrike: 2, perStrike: 3 }),
|
||||
mengsan_yubeidaji: Object.freeze({ maxLevel: 1, baseDamage: 7, damage: 9, baseStrength: 2, strength: 3 }),
|
||||
mengsan_tupo: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 13, loseHp: 1 }),
|
||||
mengsan_touchui: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 12 }),
|
||||
mengsan_tiezhanbo: Object.freeze({ maxLevel: 1, baseBlock: 5, block: 7, baseDamage: 5, damage: 7 }),
|
||||
mengsan_shuangchongdaji: Object.freeze({ maxLevel: 1, baseDamage: 5, damage: 7, hits: 2 }),
|
||||
mengsan_shandianpili: Object.freeze({ maxLevel: 1, baseDamage: 4, damage: 7, vulnerable: 1 }),
|
||||
mengsan_rongrongzhiquan: Object.freeze({ maxLevel: 1, baseDamage: 10, damage: 14, exhaust: true }),
|
||||
mengsan_quanshenzhuangji: Object.freeze({ maxLevel: 1, baseCost: 1, cost: 0, damageFromBlock: true }),
|
||||
mengsan_jianbingdaji: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 10, baseDraw: 1, draw: 2 }),
|
||||
mengsan_feijianhuixuanbiao: Object.freeze({ maxLevel: 1, damage: 3, baseHits: 3, hits: 4 }),
|
||||
mengsan_fennu: Object.freeze({ maxLevel: 1, baseDamage: 6, damage: 8 }),
|
||||
sha: Object.freeze({ maxLevel: 1, damage: 9 }),
|
||||
mengsan_zhongsha: Object.freeze({ maxLevel: 1, damage: 10,
|
||||
vulnerable: 3 }),
|
||||
|
||||
@@ -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,7 +9,7 @@ const act1Map = {
|
||||
1, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
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" },
|
||||
chestRewardPool: "act1.pool.chest",
|
||||
fixedNodes: [
|
||||
@@ -20,8 +20,14 @@ const act1Map = {
|
||||
contentId: "act1.battle.liubeiOpening",
|
||||
},
|
||||
],
|
||||
enemies: ["mengsan_flyconid_shuying", "mengsan_raider_trio_shuying"],
|
||||
eliteEnemies: ["re_xiahoudun", "re_zhangliao", "re_xuzhu"],
|
||||
enemies: [
|
||||
"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",
|
||||
baseGold: 20,
|
||||
};
|
||||
|
||||
@@ -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: [] };
|
||||
}
|
||||
@@ -9,6 +9,44 @@ export function createScenarioCharacters(Character) {
|
||||
skills: [], img: "image/character/shibing1.jpg" }),
|
||||
mengsan_flyconid_shuying: new Character({ sex: "none", group: "qun", hp: 48, maxHp: 48,
|
||||
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,
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/portraits/raider-brute-portrait.png" }),
|
||||
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_soldier_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_assassin_shuying: "劫掠者刺客",
|
||||
mengsan_raider_axe_shuying: "劫掠者斧手",
|
||||
|
||||
@@ -11,6 +11,78 @@ export const rewards = {
|
||||
card: { name: "mengsan_zhongsha" }, name: "获得一张【重杀】",
|
||||
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.wuzhong": { card: { name: "wuzhong" }, name: "获得一张【无中生有】", description: "加入独立永久牌组" },
|
||||
"shared.reward.card.upgrade": { effectId: "upgrade", name: "随机强化", description: "随机强化一张尚未满级的可强化牌" },
|
||||
@@ -26,6 +98,22 @@ export const rewardPools = {
|
||||
"shared.pool.battle.normal": [
|
||||
"shared.reward.card.sha",
|
||||
"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.wuzhong",
|
||||
],
|
||||
|
||||
@@ -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。隔离测试不是实机验收。
|
||||
@@ -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。隔离检查不代表游戏实机验收。
|
||||
@@ -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
|
||||
隔离测试不代表实机验收。
|
||||
@@ -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实机验收。
|
||||
@@ -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视觉验收。
|
||||
@@ -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验收。
|
||||
@@ -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所示许可,保留来源。
|
||||
|
||||
验证为隔离执行真实项目模块及引擎边界夹具,不是实机验收。
|
||||
@@ -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验收。
|
||||
@@ -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视觉验收。
|
||||
@@ -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
|
||||
验证属于隔离运行与实际文件检查,不代表实机验收。
|
||||
@@ -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;隔离验证不等于实机验收。
|
||||
@@ -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许可提示。生成提示保存于验证记录。
|
||||
|
||||
模块隔离与边界夹具检查不是实机验收。
|
||||