fixed # 新增羁绊系统

fixed # 新增遗物系统
fixed # 梦三模式下移除护甲上限限制
This commit is contained in:
2026-09-30 14:33:17 +08:00
parent f2da9bce65
commit 82d80397b6
45 changed files with 2403 additions and 130 deletions
+2 -1
View File
@@ -87,7 +87,7 @@ export function grantSupport(run, rewardId, spec) {
supports.push({ id: `support_${number}`, rewardId, category: spec.category || "item", unit: copy(spec.unit), battles });
}
export function buildBattlePlan(encounter, run) {
export function buildBattlePlan(encounter, run, bondUnit = null) {
const plan = copy(encounter.battlePlan || {
units: [{ id: "enemy_1", character: encounter.enemy, camp: "enemy", tier: encounter.tier || "normal", hand: 4 }], rules: [],
});
@@ -98,6 +98,7 @@ export function buildBattlePlan(encounter, run) {
assert(support.battles === -1 || (Number.isInteger(support.battles) && support.battles > 0), "支援持续场数无效");
plan.units.push({ ...copy(support.unit), id: support.id });
}
if (bondUnit) plan.units.unshift(copy(bondUnit));
validateBattlePlan(plan);
return plan;
}
+10 -2
View File
@@ -16,7 +16,11 @@ export function createBattleFlow({settlement, chooseReward, showMap, showEnding,
if (job.pending.state !== "chosen") {
state = "choosing";
if (!job.hasChoice) {
job.choiceId = job.pending.outcome === "defeat" || !job.pending.choices.length ? null : await chooseReward(copy(job.pending.choices), { boss: job.pending.boss });
job.choiceId = job.pending.outcome === "defeat" ? null
: await chooseReward(copy(job.pending.choices), {
boss: job.pending.boss,
fixedRewards: copy(job.pending.fixedRewards || []),
});
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");
job.hasChoice = true;
}
@@ -37,7 +41,11 @@ export function createBattleFlow({settlement, chooseReward, showMap, showEnding,
await quiesce();
if (!job.delivered) {
state = "presenting";
if (job.committed.route === "map") job.nextNode = await showMap(copy(job.committed.run));
if (job.committed.route === "map") {
const mapRun = copy(job.committed.run);
job.nextNode = await showMap(mapRun);
job.committed.run = mapRun;
}
else await showEnding(job.committed.route, copy(job.committed.run));
job.delivered = true;
}
+7 -1
View File
@@ -1,4 +1,7 @@
// Candidate settlement adapter. Dependencies are the current mode/state reward functions.
import { applyBattleEndRelics } from "../relics/battle.js";
import { settleBondBattle } from "../bonds/state.js";
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}) {
@@ -34,16 +37,19 @@ export function createBattleSettlement({store, config, getRandomRewardChoices, a
}
// Battle may have changed RNG/card state since the last checkpoint.
const base = copy(snapshot); delete base.battleFlow;
const bondGrowth = settleBondBattle(base);
if (outcome === "victory") {
base.player.hp = Math.max(1, Math.min(hp, base.player.maxHp ?? hp));
base.statistics.defeatedEnemies += encounter.defeatedEnemies ?? 1;
}
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
? [{id:"mengsan.reward.gold.shuying", kind:"gold", name:"金币", amount:encounter.gold}, ...rewards] : [];
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};
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};
return run.battleFlow.pending;
});
},
+4 -2
View File
@@ -2,9 +2,11 @@
import { _status } from "../../../../noname.js";
export const PLAYER_ENERGY = 3;
export const PLAYER_HAND_LIMIT = 3;
export const SHA_DAMAGE = 4;
export const SHA_DAMAGE = 6;
export const TRICK_DAMAGE = 4;
const COSTS = Object.freeze({ sha: 1, tao: 1, jiu: 1, shan: 0, wuxie: 0,
const COSTS = Object.freeze({ sha: 1, mengsan_zhongsha: 2,
tao: 1, jiu: 1, shan: 0, wuxie: 0,
juedou: 2, nanman: 2, wanjian: 2, taoyuan: 2 });
export const cardCost = card => COSTS[card?.name] ?? 1;
+53
View File
@@ -0,0 +1,53 @@
import { attackHitCount, outgoingAttackDamage } from "./intent-damage.js";
import { beginDeathBlow, isDeathBlowIntent } from "./death-blow.js";
export const getIntentHostiles = (game, source) =>
game.players.filter(target => target !== source && target.isAlive() &&
target.storage?.mengsanCamp_shuying !==
source.storage?.mengsanCamp_shuying);
export function createIntentExecutor(game, getActiveBattle) {
const active = source => source.isAlive() &&
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");
};
return async (source, move) => {
for (const target of getIntentHostiles(game, source)) {
if (!active(source)) break;
if (!target.isAlive()) continue;
if (isDeathBlowIntent(move)) {
await beginDeathBlow(source, target, move);
continue;
}
if (move.damage != null) {
for (let hitIndex = 0; hitIndex < attackHitCount(move); hitIndex++) {
if (!target.isAlive() || !active(source)) break;
const hit = target.damage(
outgoingAttackDamage(move, source), source);
hit.mengsanAttack_shuying = true;
hit.mengsanScriptedSkill_shuying = true;
await hit;
}
}
if (target.isAlive() && active(source)) applyDebuff(target, move);
}
if (!active(source)) return;
if (move.block) await source.changeHujia(move.block);
if (!active(source)) return;
if (move.strength) {
source.storage.mengsanStrength_shuying =
(source.storage.mengsanStrength_shuying || 0) + move.strength;
source.addSkill("mengsan_raider_strength_shuying");
source.markSkill("mengsan_raider_strength_shuying");
}
};
}