mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-15 19:50:08 +08:00
fixed # 补全部分卡牌
fixed # 修复部分已知BUG
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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]];
|
||||
|
||||
@@ -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 || "战斗奖励(三选一)",
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user