feat: add zone-scaled quest failure chance

Quests now have a random chance of failing when their timer expires.
On failure the quest resets to available (no rewards lost, just time).
Failure chance scales by zone: 10% in Verdant Vale up to 40% in the
end-game zones. The Quest type gains lastFailedAt so the UI can show
a warning on subsequent attempts.
This commit is contained in:
2026-03-07 12:13:35 -08:00
committed by Naomi Carrigan
parent 58e7000954
commit 25d4a11eeb
4 changed files with 44 additions and 0 deletions
+31
View File
@@ -46,6 +46,32 @@ const checkAchievements = (state: GameState): Achievement[] => {
/** Maximum value any resource can accumulate to. Beyond this JS floats lose all useful precision. */
export const RESOURCE_CAP = 1e300;
/**
* Probability of quest failure per zone — scales from 10% (early game) to 40% (end game).
* On failure the quest resets to "available" with no rewards; the player must wait the
* full duration again on their next attempt.
*/
const ZONE_FAILURE_CHANCE: Record<string, number> = {
verdant_vale: 0.10,
shattered_ruins: 0.12,
frozen_peaks: 0.14,
shadow_marshes: 0.16,
volcanic_depths: 0.18,
astral_void: 0.20,
celestial_reaches: 0.22,
abyssal_trench: 0.24,
infernal_court: 0.26,
crystalline_spire: 0.28,
void_sanctum: 0.30,
eternal_throne: 0.32,
primordial_chaos: 0.34,
infinite_expanse: 0.36,
reality_forge: 0.38,
cosmic_maelstrom: 0.40,
primeval_sanctum: 0.40,
the_absolute: 0.40,
};
const capResource = (value: number): number => Math.min(value, RESOURCE_CAP);
/**
@@ -128,6 +154,11 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
return quest;
}
const failureChance = ZONE_FAILURE_CHANCE[quest.zoneId] ?? 0.20;
if (Math.random() < failureChance) {
return { ...quest, status: "available" as const, startedAt: undefined, lastFailedAt: now };
}
for (const reward of quest.rewards) {
if (reward.type === "gold" && reward.amount != null) {
questGold += reward.amount;