fixed # 补全部分卡牌

fixed # 修复部分已知BUG
This commit is contained in:
2026-10-04 19:32:38 +08:00
parent c448da39c2
commit 94a2a6724b
163 changed files with 14008 additions and 1537 deletions
+31
View File
@@ -0,0 +1,31 @@
export const WEAK_BATTLE_COUNT = 3;
// 与结算事务一起提交;事件、精英、Boss、固定专属关卡、失败和重试均不增加此计数。
export function normalBattleCount(run) {
const saved = run.statistics?.normalBattles;
if (saved != null) {
if (!Number.isSafeInteger(saved) || saved < 0) throw new Error("普通战斗计数无效");
return saved;
}
// 旧存档按第一章当前地图已完成的普通节点恢复;第二章起不再使用本次弱池阶段。
const completed = (run.map?.nodes || []).filter(node => node.completed && node.type === "battle" && !node.contentId).length;
return run.actIndex > 0 ? Math.max(WEAK_BATTLE_COUNT, completed) : completed;
}
// 已生成的地图也受保护;转换后的节点按普通战斗结算,未来楼层不提前转换。
export function protectEarlyBattleNodes(run, nodes) {
if (run.actIndex !== 0 || normalBattleCount(run) >= WEAK_BATTLE_COUNT) return;
for (const node of nodes) {
if (node && !node.completed && !node.contentId && node.type === "elite") {
node.type = "battle";
}
}
}
export function recordNormalBattleVictory(run, node, encounter) {
if (run.actIndex !== 0 || node.type !== "battle" || node.contentId || encounter.requiredCharacter ||
encounter.boss || encounter.tier !== "normal") return;
const next = normalBattleCount(run) + 1;
if (!Number.isSafeInteger(next)) throw new RangeError("普通战斗计数越界");
run.statistics.normalBattles = next;
}
+8
View File
@@ -0,0 +1,8 @@
// 输入为存档副本;只保留征程成长和地图进度,不恢复战斗或未确认的结算。
export function stripBattleProgress(run) {
if (!run || typeof run !== "object") return run;
delete run.battleFlow;
delete run.bondBattle;
delete run.sharedBattleCardEffects;
return run;
}
+12 -3
View File
@@ -1,9 +1,17 @@
// Candidate factory. Storage writer body reused from batch2 MODIFIED_STORAGE.js.
import { migrateRetiredCards } from "../cards/retired-cards.js";
import { stripBattleProgress } from "./map-checkpoint.js";
export function createModeStorage({lib, game, config, localStorage, alert = () => {}, console = globalThis.console}) {
const MODE_ID = config.modeId;
const copy = value => JSON.parse(JSON.stringify(value));
const copyStorage = () => {
const storage = copy(lib.storage);
stripBattleProgress(storage[config.saveKey]);
migrateRetiredCards(storage[config.saveKey]);
return storage;
};
// Keep one mode-level writer; a failed write must not poison later retries.
let modeStorageQueue = Promise.resolve();
@@ -42,8 +50,9 @@ const persistModeStorage = (update = () => {}) => {
// reload2 also drains the engine's queued database operations on release.
lib.status.reload++;
const operation = modeStorageQueue.then(async () => {
const storage = copy(lib.storage);
const storage = copyStorage();
update(storage);
stripBattleProgress(storage[config.saveKey]);
storage.version = lib.version;
await writeModeStorage(storage);
lib.storage = storage;
@@ -55,7 +64,7 @@ const persistModeStorage = (update = () => {}) => {
const saveRun = async run => {
try {
// Capture before enqueue: subsequent gameplay mutations cannot alter this save.
const snapshot = copy(run);
const snapshot = migrateRetiredCards(stripBattleProgress(copy(run)));
await persistModeStorage(storage => {
storage[config.saveKey] = snapshot;
});
@@ -77,7 +86,7 @@ const clearRun = async () => {
return Object.freeze({
read: () => copy(lib.storage),
read: copyStorage,
saveRun, clearRun,
async update(mutator) {
let value;
+34 -3
View File
@@ -1,6 +1,6 @@
import config from "../config.js";
import { nextRandom } from "./state.js";
import { canAcquireCard } from "../cards/card-definitions.js";
import { canAcquireCard, cardDefinitions } from "../cards/card-definitions.js";
import { hasUpgradeableCard } from "../cards/upgrades.js";
import { canAcquireRelic } from "../relics/definitions.js";
@@ -17,10 +17,41 @@ export const canAcquireReward = (run, reward) => {
return !name || canAcquireCard(run?.player?.character, name);
};
export const getRandomRewardChoices = (run, poolId, count = 3) => {
// 精英先抽稀有度再抽牌,避免牌池中各等级数量不同影响概率。
// 每个候选:普通20%、罕见60%、稀有20%;同一组选项不重复。
const eliteRarityWeights = Object.freeze({ common: 20, uncommon: 60, rare: 20 });
const rewardRarity = id => cardDefinitions[rewardCardName(config.rewards[id])]?.rarity;
export const getRandomRewardChoices = (run, poolId, count = 3, { allowSharedCards = false } = {}) => {
if (!Number.isSafeInteger(count) || count < 0) throw new RangeError("奖励候选数量无效");
const fallbackId = "shared.pool.battle.normal";
const boss = poolId === "shared.pool.boss.premium";
const pool = (config.rewardPools[poolId] || config.rewardPools[fallbackId])
.filter(id => canAcquireReward(run, config.rewards[id])).slice();
.filter(id => {
const reward = config.rewards[id];
if (!canAcquireReward(run, reward)) return false;
const name = rewardCardName(reward);
// 普通随机卡牌奖励仅来自当前角色;特殊剧情可显式允许通用牌。
if (name && !allowSharedCards && cardDefinitions[name]?.owner !== run.player.character) return false;
return !boss || name && cardDefinitions[name]?.rarity === "rare";
});
if (boss && pool.length < count) throw new Error("角色专属稀有卡奖励不足,拒绝降级奖励");
if (poolId === "shared.pool.battle.elite") {
const groups = Object.entries(eliteRarityWeights).map(([rarity, weight]) => ({
weight, ids: pool.filter(id => rewardRarity(id) === rarity),
}));
const choices = [];
while (choices.length < count) {
const available = groups.filter(group => group.ids.length);
if (!available.length) break;
let cursor = nextRandom(run) * available.reduce((sum, group) => sum + group.weight, 0);
const group = available.find(group => (cursor -= group.weight) < 0) || available[available.length - 1];
const index = Math.floor(nextRandom(run) * group.ids.length);
const [id] = group.ids.splice(index, 1);
choices.push({ id, ...config.rewards[id] });
}
return choices;
}
for (let index = pool.length - 1; index > 0; index--) {
const target = Math.floor(nextRandom(run) * (index + 1));
[pool[index], pool[target]] = [pool[target], pool[index]];
+47 -7
View File
@@ -1,3 +1,5 @@
import { KIN_PRIEST_CHARACTER } from "../monsters/kin-intent.js";
import { createKinBattlePlan } from "../content/acts/act1/kin-group.js";
import { createStartingDeck } from "../cards/starting-deck.js";
import config from "../config.js";
import { initializeInnateBonds } from "../bonds/state.js";
@@ -5,6 +7,8 @@ import { RAIDER_TRIO_ENCOUNTER, createRaiderTrioBattlePlan } from "../content/ac
import { INKLET_CHARACTER } from "../monsters/inklet-intent.js";
import { createInkletBattlePlan } from "../content/acts/act1/inklet-group.js";
import { NIBBIT_PAIR_ENCOUNTER, createNibbitPairBattlePlan } from "../content/acts/act1/nibbit-pair.js";
import { chooseEncounterEntry, createEncounterPlan } from "../content/encounters.js";
import { normalBattleCount, protectEarlyBattleNodes, WEAK_BATTLE_COUNT } from "./encounter-progress.js";
const hashText = text => {
let value = 2166136261;
@@ -120,6 +124,10 @@ export const generateActMap = (run, actIndex) => {
}
else if (floor == 0) type = "battle";
else type = weightedType(run, act.nodeWeights);
// 专属开场后的前三层不生成随机精英;跳过战斗的路线还需按胜场继续保护。
if (actIndex === 0 && floor <= WEAK_BATTLE_COUNT && !fixedNode && type === "elite") {
type = "battle";
}
const id = `${act.id}_f${floor}_n${index}_${Math.floor(nextRandom(run) * 1e6)}`;
const node = {
id,
@@ -175,6 +183,7 @@ export const createRun = character => {
handLimitBonus: 0,
},
statistics: {
normalBattles: 0,
completedNodes: 0,
defeatedEnemies: 0,
goldEarned: 0,
@@ -188,11 +197,12 @@ export const createRun = character => {
export const getSelectableNodes = run => {
const map = run.map;
if (!map.currentNodeId) {
return map.nodes.filter(node => node.floor == 0 && !node.completed);
}
const ids = map.edges.filter(edge => edge[0] == map.currentNodeId).map(edge => edge[1]);
return map.nodes.filter(node => ids.includes(node.id) && !node.completed);
const ids = new Set(map.currentNodeId
? map.edges.filter(edge => edge[0] == map.currentNodeId).map(edge => edge[1])
: map.nodes.filter(node => node.floor == 0).map(node => node.id));
// 同时修正旧存档的早期楼层和当前可走节点,使地图图标与实际遭遇一致。
protectEarlyBattleNodes(run, map.nodes.filter(node => node.floor <= WEAK_BATTLE_COUNT || ids.has(node.id)));
return map.nodes.filter(node => ids.has(node.id) && !node.completed);
};
export const completeNode = (run, nodeId) => {
@@ -231,6 +241,11 @@ export const insertStoryNode = (run, sourceId) => {
};
export const enterNextAct = run => {
if (config.acts[run.actIndex]?.endsCurrentContent) {
run.status = "completed";
run.revision++;
return false;
}
run.actIndex++;
if (run.actIndex >= config.acts.length) {
run.status = "completed";
@@ -245,11 +260,34 @@ export const getNodeEncounter = (run, node, override = null) => {
const act = config.acts[run.actIndex];
const content = node.contentId ? config.nodeContents?.[node.contentId] : null;
const encounter = override || content;
if (act.encounterPools && !encounter?.enemies?.length && !encounter?.battlePlan &&
["battle", "elite", "boss"].includes(node.type)) {
// 直接进入节点时也检查;交接传入的节点可能是副本,须同步地图中的结算类型。
protectEarlyBattleNodes(run, [node, run.map?.nodes.find(current => current.id === node.id)]);
const count = normalBattleCount(run);
const poolName = node.type === "battle" ? (count < WEAK_BATTLE_COUNT ? "weak" : "strong") : node.type;
const id = chooseEncounterEntry(act.encounterPools[poolName], () => nextRandom(run));
const definition = config.encounters[id];
const battlePlan = createEncounterPlan(definition, () => nextRandom(run));
const boss = node.type === "boss", elite = node.type === "elite";
return {
name: definition.name, encounterId: id, encounterPool: poolName,
normalBattleNumber: node.type === "battle" ? count + 1 : null,
enemy: battlePlan.units.find(unit => unit.camp === "enemy").character,
battlePlan, tier: definition.tier, boss,
gold: act.baseGold * (boss ? 3 : elite ? 2 : 1),
rewardPool: boss ? "shared.pool.boss.premium" : elite ? "shared.pool.battle.elite" : "shared.pool.battle.normal",
guaranteedRelicPool: elite ? "shared.pool.elite.relics" : null,
rewardTitle: "战斗奖励(三选一)",
openingDialogue: [], victoryDialogue: [], fixedRewards: [], contentId: null,
};
}
let pool = encounter?.enemies || act.enemies;
if (node.type == "elite" || node.type == "story") pool = act.eliteEnemies?.length ? act.eliteEnemies : act.enemies;
if (encounter?.enemies?.length) pool = encounter.enemies;
if (node.type == "boss") {
return { battlePlan: encounter?.battlePlan || act.bossBattlePlan || null, enemy: act.boss, tier: "boss", gold: act.baseGold * 3, boss: true, rewardPool: "shared.pool.boss.premium" };
const boss = act.bossEnemies?.length ? randomGet(run, act.bossEnemies.filter(Boolean)) : act.boss;
return { name: boss === KIN_PRIEST_CHARACTER ? "同族小队" : "", battlePlan: encounter?.battlePlan || (boss === KIN_PRIEST_CHARACTER ? createKinBattlePlan() : act.bossBattlePlan || null), enemy: boss, tier: "boss", gold: act.baseGold * 3, boss: true, rewardPool: "shared.pool.boss.premium" };
}
const enemy = randomGet(run, pool.filter(Boolean));
return {
@@ -269,7 +307,9 @@ export const getNodeEncounter = (run, node, override = null) => {
boss: false,
contentId: node.contentId || null,
description: encounter?.description || "",
rewardPool: encounter?.rewardPool || "shared.pool.battle.normal",
rewardPool: encounter?.rewardPool || (node.type === "elite" ? "shared.pool.battle.elite" : "shared.pool.battle.normal"),
allowSharedCardRewards: encounter?.allowSharedCardRewards === true,
guaranteedRelicPool: node.type === "elite" ? "shared.pool.elite.relics" : null,
rewardTitle: encounter?.rewardTitle || "战斗奖励(三选一)",
};
};