generated from nhcarrigan/template
feat: v0.6.0 balance pass, quest-gated bosses, and achievement fixes
This commit is contained in:
+77
-46
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user