4 Commits

Author SHA1 Message Date
hikari 7d1126e8ad fix: guarantee clicks challenge in daily set (#167)
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m7s
CI / Lint, Build & Test (pull_request) Successful in 1m12s
Players blocked on zone progression had days where all three daily
challenges (bossesDefeated, questsCompleted, prestige) required
progression they couldn't make. Always including a clicks challenge
ensures at least one challenge is completable regardless of where
the player is in the game.
2026-03-31 12:48:34 -07:00
hikari ec0763819e balance: increase runestone yield 50% per prestige (#166)
Raise runestonesPerPrestigeLevel from 10 to 15. Early-game players
were earning only 10-20 runestones per prestige, making the upgrade
shop feel out of reach. This boost helps mid-game without affecting
the cap behaviour (cbrt formula still prevents AFK windfalls).
2026-03-31 12:47:48 -07:00
hikari 4a9ecbf706 balance: improve mid-game crystal income (#165)
Add 150 crystals to shadow_mere and 500 to witch_coven quest rewards.
Double shadow_marshes boss crystal drops (700->1500, 1500->3000, 3000->6000)
to provide meaningful crystal flow for players reaching Shadow Marshes.
2026-03-31 12:46:58 -07:00
hikari 96868c4143 balance: reduce shadow_mere CP requirement 5M -> 2M (#164)
The zone unlocks at 1.5M CP (storm_citadel), making the 5M CP entry
quest unreachable for most players. 2M CP is achievable with Arcane
Scholar and Dragon Rider adventurers without being trivial.
2026-03-31 12:45:57 -07:00
6 changed files with 33 additions and 18 deletions
+3 -3
View File
@@ -122,7 +122,7 @@ export const defaultBosses: Array<Boss> = [
// ── Shadow Marshes ──────────────────────────────────────────────────────── // ── Shadow Marshes ────────────────────────────────────────────────────────
{ {
bountyRunestones: 20, bountyRunestones: 20,
crystalReward: 700, crystalReward: 1500,
currentHp: 6_000_000, currentHp: 6_000_000,
damagePerSecond: 1200, damagePerSecond: 1200,
description: description:
@@ -140,7 +140,7 @@ export const defaultBosses: Array<Boss> = [
}, },
{ {
bountyRunestones: 25, bountyRunestones: 25,
crystalReward: 1500, crystalReward: 3000,
currentHp: 12_000_000, currentHp: 12_000_000,
damagePerSecond: 2400, damagePerSecond: 2400,
description: description:
@@ -158,7 +158,7 @@ export const defaultBosses: Array<Boss> = [
}, },
{ {
bountyRunestones: 30, bountyRunestones: 30,
crystalReward: 3000, crystalReward: 6000,
currentHp: 20_000_000, currentHp: 20_000_000,
damagePerSecond: 4000, damagePerSecond: 4000,
description: description:
+3 -1
View File
@@ -223,7 +223,7 @@ export const defaultQuests: Array<Quest> = [
}, },
// ── Shadow Marshes ──────────────────────────────────────────────────────── // ── Shadow Marshes ────────────────────────────────────────────────────────
{ {
combatPowerRequired: 5_000_000, combatPowerRequired: 2_000_000,
description: description:
"A cursed lake shrouded in permanent twilight. Strange energies pulse beneath its surface.", "A cursed lake shrouded in permanent twilight. Strange energies pulse beneath its surface.",
durationSeconds: 45 * 60, durationSeconds: 45 * 60,
@@ -233,6 +233,7 @@ export const defaultQuests: Array<Quest> = [
rewards: [ rewards: [
{ amount: 5_000_000, type: "gold" }, { amount: 5_000_000, type: "gold" },
{ amount: 5000, type: "essence" }, { amount: 5000, type: "essence" },
{ amount: 150, type: "crystals" },
{ targetId: "peasant_3", type: "upgrade" }, { targetId: "peasant_3", type: "upgrade" },
], ],
status: "locked", status: "locked",
@@ -249,6 +250,7 @@ export const defaultQuests: Array<Quest> = [
rewards: [ rewards: [
{ amount: 20_000_000, type: "gold" }, { amount: 20_000_000, type: "gold" },
{ amount: 20_000, type: "essence" }, { amount: 20_000, type: "essence" },
{ amount: 500, type: "crystals" },
{ targetId: "shadow_assassin", type: "adventurer" }, { targetId: "shadow_assassin", type: "adventurer" },
], ],
status: "locked", status: "locked",
+7 -5
View File
@@ -71,8 +71,7 @@ const shuffleWithSeed = <T>(array: Array<T>, seed: number): Array<T> => {
return result; return result;
}; };
const challengeTypes: Array<DailyChallengeType> = [ const progressionChallengeTypes: Array<DailyChallengeType> = [
"clicks",
"bossesDefeated", "bossesDefeated",
"questsCompleted", "questsCompleted",
"prestige", "prestige",
@@ -80,7 +79,8 @@ const challengeTypes: Array<DailyChallengeType> = [
/** /**
* Generates 3 daily challenges for the given date string, deterministically. * Generates 3 daily challenges for the given date string, deterministically.
* Picks one challenge from 3 different randomly-selected types. * Always includes a "clicks" challenge (always completable regardless of
* progression), then picks 2 more from the remaining types.
* @param dateString - The date string (YYYY-MM-DD) to generate challenges for. * @param dateString - The date string (YYYY-MM-DD) to generate challenges for.
* @returns An array of 3 DailyChallenge objects. * @returns An array of 3 DailyChallenge objects.
*/ */
@@ -88,8 +88,10 @@ const generateDailyChallenges = (
dateString: string, dateString: string,
): Array<DailyChallenge> => { ): Array<DailyChallenge> => {
const seed = dateSeed(dateString); const seed = dateSeed(dateString);
const selectedTypes = shuffleWithSeed([ ...challengeTypes ], seed). const selectedTypes: Array<DailyChallengeType> = [
slice(0, 3); "clicks",
...shuffleWithSeed([ ...progressionChallengeTypes ], seed).slice(0, 2),
];
return selectedTypes.map((type, index) => { return selectedTypes.map((type, index) => {
const templates = dailyChallengeTemplates.filter((template) => { const templates = dailyChallengeTemplates.filter((template) => {
+1 -1
View File
@@ -15,7 +15,7 @@ import type {
} from "@elysium/types"; } from "@elysium/types";
const basePrestigeGoldThreshold = 1_000_000; const basePrestigeGoldThreshold = 1_000_000;
const runestonesPerPrestigeLevel = 10; const runestonesPerPrestigeLevel = 15;
const milestoneInterval = 5; const milestoneInterval = 5;
const milestoneRunestonesPerInterval = 25; const milestoneRunestonesPerInterval = 25;
+13 -2
View File
@@ -46,13 +46,24 @@ describe("generateDailyChallenges", () => {
expect(a.map((c) => c.id)).toEqual(b.map((c) => c.id)); expect(a.map((c) => c.id)).toEqual(b.map((c) => c.id));
}); });
it("always includes a clicks 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");
expect(day1.some((c) => c.type === "clicks")).toBe(true);
expect(day2.some((c) => c.type === "clicks")).toBe(true);
});
it("generates different challenges for different dates", async () => { it("generates different challenges for different dates", async () => {
vi.setSystemTime(LA_MIDNIGHT_2024_01_15); vi.setSystemTime(LA_MIDNIGHT_2024_01_15);
const { generateDailyChallenges } = await import("../../src/services/dailyChallenges.js"); const { generateDailyChallenges } = await import("../../src/services/dailyChallenges.js");
const day1 = generateDailyChallenges("2024-01-15"); const day1 = generateDailyChallenges("2024-01-15");
const day2 = generateDailyChallenges("2024-01-16"); const day2 = generateDailyChallenges("2024-01-16");
// They should differ in at least one challenge ID (types vary by seed) // The 2 non-clicks types should vary by seed between dates
expect(day1.map((c) => c.type)).not.toEqual(day2.map((c) => c.type)); 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);
}); });
}); });
+6 -6
View File
@@ -102,21 +102,21 @@ describe("isEligibleForPrestige", () => {
describe("calculateRunestones", () => { describe("calculateRunestones", () => {
it("calculates basic runestones formula", () => { it("calculates basic runestones formula", () => {
// floor(cbrt(4_000_000 / 1_000_000)) × 10 = floor(cbrt(4)) × 10 = 1 × 10 = 10 // floor(cbrt(4_000_000 / 1_000_000)) × 15 = floor(cbrt(4)) × 15 = 1 × 15 = 15
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [] }); const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
expect(result).toBe(10); expect(result).toBe(15);
}); });
it("applies echo runestone multiplier", () => { it("applies echo runestone multiplier", () => {
// floor(cbrt(4)) × 10 = 10; × 2 = 20 // floor(cbrt(4)) × 15 = 15; × 2 = 30
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [], echoRunestoneMultiplier: 2 }); const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [], echoRunestoneMultiplier: 2 });
expect(result).toBe(20); expect(result).toBe(30);
}); });
it("applies purchased runestone upgrade multiplier", () => { it("applies purchased runestone upgrade multiplier", () => {
// With "runestone_gain_1" purchased (multiplier 1.25): floor(10 × 1.25) = 12 // With "runestone_gain_1" purchased (multiplier 1.25): floor(15 × 1.25) = 18
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: ["runestone_gain_1"] }); const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: ["runestone_gain_1"] });
expect(result).toBe(12); expect(result).toBe(18);
}); });
it("caps base runestones before multipliers", () => { it("caps base runestones before multipliers", () => {