feat: v0.6.0 balance pass, quest-gated bosses, and achievement fixes

This commit is contained in:
2026-07-17 01:02:12 -05:00
parent 9bb1d01d2b
commit 5928eb73d8
49 changed files with 1836 additions and 528 deletions
+77 -46
View File
@@ -22,6 +22,7 @@ import {
import { EQUIPMENT_SETS } from "../data/equipmentSets.js";
import { EXPLORATION_AREAS } from "../data/explorations.js";
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
import { getAchievementProgress } from "./achievementProgress.js";
/**
* Checks all achievements against the current game state and returns an updated
@@ -37,45 +38,7 @@ const checkAchievements = (state: GameState): Array<Achievement> => {
}
const { condition } = achievement;
let met = false;
switch (condition.type) {
case "totalGoldEarned":
met = state.player.lifetimeGoldEarned >= condition.amount;
break;
case "totalClicks":
met = state.player.totalClicks >= condition.amount;
break;
case "bossesDefeated":
met
= state.bosses.filter((boss) => {
return boss.status === "defeated";
}).length >= condition.amount;
break;
case "questsCompleted":
met
= state.quests.filter((quest) => {
return quest.status === "completed";
}).length >= condition.amount;
break;
case "adventurerTotal":
met
= state.adventurers.reduce((sum, adventurer) => {
return sum + adventurer.count;
}, 0) >= condition.amount;
break;
case "prestigeCount":
met = state.prestige.count >= condition.amount;
break;
case "equipmentOwned":
met
= state.equipment.filter((item) => {
return item.owned;
}).length >= condition.amount;
break;
default:
/* V8 ignore next -- @preserve */ break;
}
const met = getAchievementProgress(condition, state) >= condition.amount;
return met
? { ...achievement, unlockedAt: now }
@@ -452,11 +415,32 @@ export const computePartyCombatPower = (state: GameState): number => {
const basePrestigeThreshold = 1_000_000;
const runestonesPerPrestigeLevelClient = 20;
const maxBaseRunestones = 200;
const maxBaseRunestones = 400;
const milestoneIntervalClient = 5;
const milestoneRunestonesPerIntervalClient = 25;
/**
* Computes the projected runestone reward if the player were to prestige right now.
* Mirrors the server-side calculateRunestones formula exactly.
* Computes the milestone runestone bonus the player would receive if they
* prestiged right now. Mirrors the server-side calculateMilestoneBonus formula:
* every MILESTONE_INTERVAL prestiges awards milestone_number² * MILESTONE_RUNESTONES_PER_INTERVAL stones.
* @param state - The current game state.
* @returns The milestone runestone bonus, or 0 if the next prestige isn't a milestone.
*/
export const computeProjectedMilestoneBonus = (state: GameState): number => {
const nextPrestigeCount = state.prestige.count + 1;
if (nextPrestigeCount % milestoneIntervalClient !== 0) {
return 0;
}
const milestoneNumber = nextPrestigeCount / milestoneIntervalClient;
return (
milestoneNumber * milestoneNumber * milestoneRunestonesPerIntervalClient
);
};
/**
* Computes the projected runestone reward if the player were to prestige right now,
* including any milestone bonus, so callers always see the true total they'd receive.
* Mirrors the server-side calculateRunestones + calculateMilestoneBonus formulas exactly.
* @param state - The current game state.
* @returns The number of runestones the player would earn from a prestige now.
*/
@@ -480,7 +464,10 @@ export const computeProjectedRunestones = (state: GameState): number => {
const runestoneMult = gain1Mult * gain2Mult;
const echoMult: number
= state.transcendence?.echoPrestigeRunestoneMultiplier ?? 1;
return Math.floor(base * runestoneMult * echoMult);
return (
Math.floor(base * runestoneMult * echoMult)
+ computeProjectedMilestoneBonus(state)
);
};
/**
@@ -648,8 +635,9 @@ export const applyTick = (
});
return {
...item,
equipped: slotEmpty || item.equipped,
owned: true,
equipped: slotEmpty || item.equipped,
everOwned: true,
owned: true,
};
});
}
@@ -731,7 +719,11 @@ export const applyTick = (
return zoneBoss.zoneId === boss.zoneId;
});
const [ firstBoss ] = zoneBosses;
if (firstBoss?.id === boss.id && boss.status === "locked") {
const questOk
= boss.unlockQuestId === null
|| boss.unlockQuestId === undefined
|| completedIds.has(boss.unlockQuestId);
if (firstBoss?.id === boss.id && boss.status === "locked" && questOk) {
return { ...boss, status: "available" as const };
}
}
@@ -739,6 +731,45 @@ export const applyTick = (
});
}
/*
* Activate any locked boss whose own unlock quest just became satisfied
* this tick, independent of a zone-unlock or boss-defeat event.
*/
updatedBosses = updatedBosses.map((boss) => {
if (boss.status !== "locked") {
return boss;
}
if (boss.unlockQuestId === null || boss.unlockQuestId === undefined) {
return boss;
}
if (!completedIds.has(boss.unlockQuestId)) {
return boss;
}
if (boss.prestigeRequirement > state.prestige.count) {
return boss;
}
const bossZone = updatedZones.find((zone) => {
return zone.id === boss.zoneId;
});
if (bossZone?.status !== "unlocked") {
return boss;
}
const zoneBosses = updatedBosses.filter((zoneBoss) => {
return zoneBoss.zoneId === boss.zoneId;
});
const bossIndex = zoneBosses.findIndex((zoneBoss) => {
return zoneBoss.id === boss.id;
});
const previousZoneBoss = zoneBosses[bossIndex - 1];
if (
previousZoneBoss !== undefined
&& previousZoneBoss.status !== "defeated"
) {
return boss;
}
return { ...boss, status: "available" as const };
});
// Count quests newly completed this tick and update daily challenge progress
const newlyCompletedQuestCount = updatedQuests.filter((quest, index) => {
const wasNotCompleted = state.quests[index]?.status !== "completed";