feat: comprehensive balance pass (#239)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m9s
CI / Lint, Build & Test (push) Successful in 1m12s

## Summary

Working through all 15 open balance tickets in a coordinated multi-pass approach.

### Pass 1 — Quest failure rates (closes #172)
- Capped all zone quest failure chances at 15% (down from up to 40%)
- Proportional scaling preserved (harder zones still fail more than easier ones)

### Pass 2 — Crystal economy (closes #165, #173, #215)
- Added `crystal_pulse` (3,000 crystals), `crystal_surge` (20,000), `crystal_tempest` (150,000) upgrades to fill the dead zone between 600 and 2M crystal sinks
- Bumped `click_deity`, `prestige_master`, and `prestige_legend` achievement crystal rewards (5K→15K, 5K→15K, 25K→75K)
- Added crystal rewards to `first_steps` (+5) and `goblin_camp` (+10) early quests

### Pass 3 — Runestone/prestige loop (closes #166, #170)
- Bumped `runestonesPerPrestigeLevel` from 15 → 20 (~33% yield increase for mid-game runs)
- Reduced `income_10` cost from 22,500 → 15,000 and `income_11` from 60,000 → 35,000
- Kept client/server parity: `runestonesPerPrestigeLevelClient` in tick.ts updated to match

### Pass 4 — Quest content (#175, #178)
- Both already resolved in commit 666a5b2: quests now reach 5e141 CP across reality_forge, cosmic_maelstrom, primeval_sanctum, and the_absolute — fully covering P60–P212

### Pass 5 — Daily challenges (closes #167)
- Added `crafting` as a new `DailyChallengeType`
- Added 3 crafting challenge templates (craft 1/2/3 recipes)
- Changed generation to guarantee: 1 clicks + 1 crafting + 1 from progression pool
- Added crafting challenge tracking in `craft.ts` (awards crystals on recipe craft)
- Stuck players now have 2/3 daily challenges always completable

### Pass 6 — Transcendence costs (#179)
- Already resolved in commit 666a5b2: echo meta costs are 15/45/100 (was 25/75/200)

### Also closed as stale
- #171 (milestone bonus already quadratic)
- #174 (production multiplier already 1.3^n)
- #176 (expanse_sovereign HP already at 3e39)
- #177 (recipe costs already in expected range)
- #178 (post-absolute quests already present)
- #179 (echo meta costs already reduced)

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #239
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #239.
This commit is contained in:
2026-04-06 18:58:04 -07:00
committed by Naomi Carrigan
parent 1195b657a0
commit e7164257c5
13 changed files with 160 additions and 41 deletions
+18 -5
View File
@@ -55,15 +55,28 @@ describe("generateDailyChallenges", () => {
expect(day2.some((c) => c.type === "clicks")).toBe(true);
});
it("generates different challenges for different dates", async () => {
it("always includes a crafting challenge regardless of date", async () => {
vi.setSystemTime(LA_MIDNIGHT_2024_01_15);
const { generateDailyChallenges } = await import("../../src/services/dailyChallenges.js");
const day1 = generateDailyChallenges("2024-01-15");
const day2 = generateDailyChallenges("2024-01-16");
// The 2 non-clicks types should vary by seed between dates
const day1NonClicks = day1.filter((c) => c.type !== "clicks").map((c) => c.type);
const day2NonClicks = day2.filter((c) => c.type !== "clicks").map((c) => c.type);
expect(day1NonClicks).not.toEqual(day2NonClicks);
expect(day1.some((c) => c.type === "crafting")).toBe(true);
expect(day2.some((c) => c.type === "crafting")).toBe(true);
});
it("progression challenge slot varies across different dates", async () => {
vi.setSystemTime(LA_MIDNIGHT_2024_01_15);
const { generateDailyChallenges } = await import("../../src/services/dailyChallenges.js");
// 2024-01-01 picks bossesDefeated, 2024-01-02 picks prestige (verified by seed)
const day1 = generateDailyChallenges("2024-01-01");
const day2 = generateDailyChallenges("2024-01-02");
const day1ProgressionType = day1.find((c) => {
return c.type !== "clicks" && c.type !== "crafting";
})?.type;
const day2ProgressionType = day2.find((c) => {
return c.type !== "clicks" && c.type !== "crafting";
})?.type;
expect(day1ProgressionType).not.toBe(day2ProgressionType);
});
});
+7 -7
View File
@@ -102,25 +102,25 @@ describe("isEligibleForPrestige", () => {
describe("calculateRunestones", () => {
it("calculates basic runestones formula", () => {
// floor(cbrt(4_000_000 / 1_000_000)) × 15 = floor(cbrt(4)) × 15 = 1 × 15 = 15
// floor(cbrt(4_000_000 / 1_000_000)) × 20 = floor(cbrt(4)) × 20 = 1 × 20 = 20
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
expect(result).toBe(15);
expect(result).toBe(20);
});
it("applies echo runestone multiplier", () => {
// floor(cbrt(4)) × 15 = 15; × 2 = 30
// floor(cbrt(4)) × 20 = 20; × 2 = 40
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [], echoRunestoneMultiplier: 2 });
expect(result).toBe(30);
expect(result).toBe(40);
});
it("applies purchased runestone upgrade multiplier", () => {
// With "runestone_gain_1" purchased (multiplier 1.25): floor(15 × 1.25) = 18
// With "runestone_gain_1" purchased (multiplier 1.25): floor(20 × 1.25) = 25
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: ["runestone_gain_1"] });
expect(result).toBe(18);
expect(result).toBe(25);
});
it("caps base runestones before multipliers", () => {
// cbrt(9_261_000_000 / 1_000_000) = cbrt(9261) = 21 → 21 × 10 = 210, capped at 200
// cbrt(9_261_000_000 / 1_000_000) = cbrt(9261) = 21 → 21 × 20 = 420, capped at 200
const result = calculateRunestones({ totalGoldEarned: 9_261_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
expect(result).toBe(200);
});