Files
noname-shuying/mengsan/relics/progression.js
T
2026-10-08 23:46:57 +08:00

137 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { relicCatalog } from "./catalog.js";
import { relicRules } from "./rules.js";
import { relicRandom, relicRandomGet } from "./random.js";
import { installRelicDeckHook } from "./hooks.js";
import { cardDefinitions, canAcquireCard } from "../cards/card-definitions.js";
import { createCardData } from "../cards/card-data.js";
import { canUpgradeCard, upgradeRandomCard } from "../cards/upgrades.js";
const ids = new Map(relicCatalog.flatMap(r => [[r.id,r.wikiId],[r.wikiId,r.wikiId],[`mengsan_${r.wikiId}_shuying`,r.wikiId]]));
export const hasRelic = (run, id) => (run?.player?.items || []).some(value => ids.get(value) === id);
export const relicState = run => run.player.relicState ||= {};
export const cardType = card => cardDefinitions[card?.name]?.cardType;
export const starterKind = card => card?.name === "sha" ? "strike" : card?.name === "mengsan_fangyu" ? "defend" : null;
export function relicCardRequirements(rule) {
return [...(rule.addCards || []), ...(rule.openingCards || []), ...(rule.openingDrawCards || []), ...(rule.oneBattleDrawCards || []), ...(rule.shuffleCard ? [rule.shuffleCard] : []), ...(rule.choice === "bite" ? ["mengsan_event_maul"] : [])].filter(name => !Object.hasOwn(cardDefinitions,name));
}
export function healRun(run, amount) {
const before = run.player.hp;
if (!Number.isFinite(before) || !(before > 0)) return 0;
run.player.hp = Math.min(run.player.maxHp ?? before, before + Math.max(0,amount));
return run.player.hp - before;
}
export function changeMaxHp(run, amount) {
if (!Number.isFinite(run.player.maxHp)) { run.player.pendingRelicMaxHp = (run.player.pendingRelicMaxHp || 0) + amount; return; }
run.player.maxHp = Math.max(1,run.player.maxHp + amount);
run.player.hp = Math.min(run.player.hp,run.player.maxHp);
}
export function grantGold(run, amount) {
if (!Number.isSafeInteger(amount) || amount < 0) throw new Error("梦三金币增量无效");
if (!amount || hasRelic(run,"ectoplasm")) return 0;
const actual = Math.floor(amount * (hasRelic(run,"bowler_hat") ? 1.25 : 1));
if (!Number.isSafeInteger((run.player.gold || 0) + actual)) throw new Error("梦三金币超出范围");
run.player.gold = (run.player.gold || 0) + actual;
run.statistics ||= {}; run.statistics.goldEarned = (run.statistics.goldEarned || 0) + actual;
if (hasRelic(run,"dragon_fruit")) changeMaxHp(run,1);
return actual;
}
export function newRelicCard(run, name, spec = {}) {
const state = relicState(run), suits = ["spade","heart","club","diamond"];
let id;
do { id = `${run.runId || "run"}_relic_card_${state.cardSerial = (state.cardSerial || 0) + 1}`; }
while (run.player.deck.some(card => card.id === id));
return createCardData({name,suit:spec.suit ?? relicRandomGet(run,suits),number:spec.number ?? 1+Math.floor(relicRandom(run)*13),upgrade:0,affixes:[],...spec,id}, {character:run.player.character,enemy:true});
}
export function relicCardPool(run, {rarity,pack,type} = {}) {
return Object.values(cardDefinitions).filter(card => canAcquireCard(run.player.character,card.name) &&
(!pack ? card.owner === run.player.character : card.deck === pack) &&
(!rarity || card.rarity === rarity) && (!type || card.cardType === type)).map(card => card.name);
}
export function upgradeCards(run, count, type) {
const result = [];
for (let i=0;i<count;i++) { const card=upgradeRandomCard(run.player.deck.filter(card=>!type || cardType(card)===type),()=>relicRandom(run)); if (!card) break; result.push(card); }
return result;
}
export function afterDeckAdd(run,card,{duplicate=true}={}) {
for (const id of new Set((run.player.items || []).map(id=>ids.get(id)))) {
const rule = relicRules[id] || {};
if (rule.upgradeAddedType === cardType(card) && canUpgradeCard(card)) card.upgrade++;
if (rule.deckAddHealEvery) { const state=relicState(run); state.deckAdds=(state.deckAdds || 0)+1; if (state.deckAdds%rule.deckAddHealEvery===0) healRun(run,rule.deckAddHeal); }
if (rule.deckAddGold) grantGold(run,rule.deckAddGold);
if (rule.addedCurseMaxHp && cardType(card)==="curse") changeMaxHp(run,rule.addedCurseMaxHp);
if (rule.duplicateAdded && duplicate) {
const copy=newRelicCard(run,card.name,{...card,affixes:[...card.affixes]});
run.player.deck.push(copy); afterDeckAdd(run,copy,{duplicate:false});
}
}
}
installRelicDeckHook(afterDeckAdd);
export function addPermanentRelicCard(run,name,spec) {
const card=newRelicCard(run,name,spec); run.player.deck.push(card); afterDeckAdd(run,card); return card;
}
const queue = (run,relic,spec) => (relicState(run).choices ||= []).push({relic:relic.name,...spec});
const transform = (run,card,upgrade=false) => {
const name=relicRandomGet(run,relicCardPool(run)); if (!name) return false;
const next=newRelicCard(run,name); next.id=card.id; if (upgrade && canUpgradeCard(next)) next.upgrade++;
Object.keys(card).forEach(key=>delete card[key]); Object.assign(card,next); return true;
};
export function applyRelicPickup(run,relic,{entries,canAcquire,grant}) {
const r=relic.rule;
const state=relicState(run);
(state.counterStarts ||= {})[relic.wikiId]={attack:state.attackCount || 0,skill:state.skillCount || 0,card:state.totalCardCount || 0,exhaust:state.exhaustCount || 0};
if (r.handLimit) run.player.handLimitBonus=(run.player.handLimitBonus || 0)+r.handLimit;
if (r.maxHp) changeMaxHp(run,r.maxHp);
if (r.fullHeal) { if (Number.isFinite(run.player.maxHp)) run.player.hp=run.player.maxHp; else run.player.pendingRelicFullHeal=true; }
if (r.gainHealFraction) healRun(run,Math.floor(run.player.maxHp*r.gainHealFraction));
if (r.gainGold) grantGold(run,r.gainGold);
if (r.gainUpgrade) upgradeCards(run,r.gainUpgrade,r.upgradeType);
for (const name of r.addCards || []) addPermanentRelicCard(run,name);
for (let i=0;i<(r.randomCurse || 0);i++) { const name=relicRandomGet(run,relicCardPool(run,{pack:"curse"})); if (name) addPermanentRelicCard(run,name); }
if (r.upgradeStarters) for (const kind of ["strike","defend"]) { const card=run.player.deck.find(card=>starterKind(card)===kind && canUpgradeCard(card)); if (card) card.upgrade++; }
if (r.transformAllStarters) for (const card of run.player.deck) if (starterKind(card) && !card.affixes.includes("eternal")) transform(run,card);
if (r.transformStarters) for (const kind of ["strike","defend"]) { const card=run.player.deck.find(card=>starterKind(card)===kind && !card.affixes.includes("eternal")); if (card) transform(run,card); }
if (r.addStarters) { addPermanentRelicCard(run,"sha"); addPermanentRelicCard(run,"mengsan_fangyu"); }
if (r.gainRandomRarity) { const name=relicRandomGet(run,relicCardPool(run,{rarity:r.gainRandomRarity})); if (name) addPermanentRelicCard(run,name); }
if (r.gainAncientCard) { const name=relicRandomGet(run,relicCardPool(run,{rarity:"ancient",pack:"event"})); if (name) addPermanentRelicCard(run,name); }
if (r.choice) queue(run,relic,{kind:r.choice,count:r.choiceCount || 1,optional:!!r.optionalChoices});
for (const rarity of r.cardChoiceRarities || Array(r.cardChoices || 0).fill(r.cardChoiceRarity)) queue(run,relic,{kind:"cardReward",count:1,rarity,pack:r.cardChoicePack,offerCount:r.choiceOfferCount || 3});
if (r.gainLoseHp) queue(run,relic,{kind:"loseHp",amount:r.gainLoseHp});
for (let i=0;i<(r.randomRelics || 0);i++) {
const candidates=entries.filter(item=>item.id!==relic.id && canAcquire(run,item.id) && (r.randomRelicAncient ? item.ancient===r.randomRelicAncient : ["普通","罕见","稀有"].includes(item.tier)));
const target=relicRandomGet(run,candidates); if (target) grant(run,target.id);
}
if (r.candleBattles) relicState(run).candleRemaining=r.candleBattles;
if (r.limitedBattles) relicState(run).emberRemaining=r.limitedBattles;
}
export async function resolveRelicChoices(run,choose,{cardName=name=>name}={}) {
while (relicState(run).choices?.length) {
const task=relicState(run).choices[0];
if (task.kind==="loseHp") { run.player.hp=Math.max(1,(run.player.hp || 1)-task.amount); relicState(run).choices.shift(); continue; }
for (let i=0;i<task.count;i++) {
let choices;
if (task.kind==="cardReward") {
const pool=relicCardPool(run,task), offers=[];
while(pool.length && offers.length<task.offerCount) offers.push(pool.splice(Math.floor(relicRandom(run)*pool.length),1)[0]);
choices=offers.map(name=>({id:name,name:cardName(name),description:"加入永久牌组"}));
} else choices=run.player.deck.filter(card=>!(task.selected || []).includes(card.id) &&
(task.kind==="upgrade" ? canUpgradeCard(card) : task.kind==="duplicate" || !card.affixes.includes("eternal")))
.map(card=>({id:card.id,name:`${cardName(card.name)}${card.upgrade ? "+" : ""}`,description:`${card.suit} ${card.number}`}));
if (!choices.length) break;
if (task.optional || task.kind==="cardReward") choices.push({id:"skip",name:"跳过"});
const selected=await choose(`遗物·${task.relic}(${i+1}/${task.count})`,choices,task.kind==="cardReward" ? "选择一张卡牌" : ({remove:"选择移除的卡牌",duplicate:"选择复制的卡牌",upgrade:"选择强化的卡牌",transform:"选择变化的卡牌",transformUpgrade:"选择变化并强化的卡牌",store:"选择暂存的卡牌"}[task.kind] || task.kind));
if (selected==="skip") break;
if (!choices.some(choice=>choice.id===selected)) throw new Error("遗物卡牌选择无效");
if (task.kind==="cardReward") { addPermanentRelicCard(run,selected); continue; }
const index=run.player.deck.findIndex(card=>card.id===selected),card=run.player.deck[index];
(task.selected ||= []).push(card.id);
if (task.kind==="upgrade") card.upgrade++;
else if (task.kind==="duplicate") addPermanentRelicCard(run,card.name,{...card,affixes:[...card.affixes]});
else if (task.kind==="remove") run.player.deck.splice(index,1);
else if (task.kind==="transform" || task.kind==="transformUpgrade") transform(run,card,task.kind==="transformUpgrade");
else if (task.kind==="bite") { const next=newRelicCard(run,"mengsan_event_maul");next.id=card.id;Object.keys(card).forEach(key=>delete card[key]);Object.assign(card,next); }
else if (task.kind==="store") (relicState(run).storedCards ||= []).push(...run.player.deck.splice(index,1));
}
relicState(run).choices.shift();
}
}